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
11from pyomo.common.collections import ComponentMap, ComponentSet
12import pyomo.core.expr.numeric_expr as numeric_expr
13from pyomo.core.expr.visitor import ExpressionValueVisitor, identify_variables
14from pyomo.core.expr.numvalue import nonpyomo_leaf_types, value
15from pyomo.core.expr.numvalue import is_fixed
16import pyomo.contrib.fbbt.interval as interval
17import math
18from pyomo.core.base.block import Block
19from pyomo.core.base.constraint import Constraint
20from pyomo.core.base.var import Var
21from pyomo.gdp import Disjunct
22from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression
23import logging
24from pyomo.common.errors import InfeasibleConstraintException, PyomoException
25from pyomo.common.config import ConfigBlock, ConfigValue, In, NonNegativeFloat, NonNegativeInt
26
27logger = logging.getLogger(__name__)
28
29
30"""
31The purpose of this file is to perform feasibility based bounds
32tightening. This is a very basic implementation, but it is done
33directly with pyomo expressions. The only functions that are meant to
34be used by users are fbbt and compute_bounds_on_expr. The first set of
35functions in this file (those with names starting with
36_prop_bnds_leaf_to_root) are used for propagating bounds from the
37variables to each node in the expression tree (all the way to the
38root node). The second set of functions (those with names starting
39with _prop_bnds_root_to_leaf) are used to propagate bounds from the
40constraint back to the variables. For example, consider the constraint
41x*y + z == 1 with -1 <= x <= 1 and -2 <= y <= 2. When propagating
42bounds from the variables to the root (the root is x*y + z), we find
43that -2 <= x*y <= 2, and that -inf <= x*y + z <= inf. However,
44from the constraint, we know that 1 <= x*y + z <= 1, so we may
45propagate bounds back to the variables. Since we know that
461 <= x*y + z <= 1 and -2 <= x*y <= 2, then we must have -1 <= z <= 3.
47However, bounds cannot be improved on x*y, so bounds cannot be
48improved on either x or y.
49
50>>> import pyomo.environ as pe
51>>> m = pe.ConcreteModel()
52>>> m.x = pe.Var(bounds=(-1,1))
53>>> m.y = pe.Var(bounds=(-2,2))
54>>> m.z = pe.Var()
55>>> from pyomo.contrib.fbbt.fbbt import fbbt
56>>> m.c = pe.Constraint(expr=m.x*m.y + m.z == 1)
57>>> fbbt(m)
58>>> print(m.z.lb, m.z.ub)
59-1.0 3.0
60
61"""
62
63
64class FBBTException(PyomoException):
65    pass
66
67
68def _prop_bnds_leaf_to_root_ProductExpression(node, bnds_dict, feasibility_tol):
69    """
70
71    Parameters
72    ----------
73    node: pyomo.core.expr.numeric_expr.ProductExpression
74    bnds_dict: ComponentMap
75    feasibility_tol: float
76        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
77        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
78        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
79        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
80        is more conservative).
81    """
82    assert len(node.args) == 2
83    arg1, arg2 = node.args
84    lb1, ub1 = bnds_dict[arg1]
85    lb2, ub2 = bnds_dict[arg2]
86    if arg1 is arg2:
87        bnds_dict[node] = interval.power(lb1, ub1, 2, 2, feasibility_tol)
88    else:
89        bnds_dict[node] = interval.mul(lb1, ub1, lb2, ub2)
90
91
92def _prop_bnds_leaf_to_root_SumExpression(node, bnds_dict, feasibility_tol):
93    """
94
95    Parameters
96    ----------
97    node: pyomo.core.expr.numeric_expr.SumExpression
98    bnds_dict: ComponentMap
99    feasibility_tol: float
100        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
101        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
102        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
103        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
104        is more conservative).
105    """
106    arg0 = node.arg(0)
107    lb, ub = bnds_dict[arg0]
108    for i in range(1, node.nargs()):
109        arg = node.arg(i)
110        lb2, ub2 = bnds_dict[arg]
111        lb, ub = interval.add(lb, ub, lb2, ub2)
112    bnds_dict[node] = (lb, ub)
113
114
115def _prop_bnds_leaf_to_root_LinearExpression(node: numeric_expr.LinearExpression, bnds_dict, feasibility_tol):
116    """
117    This is very similar to sum expression
118    """
119    lb, ub = bnds_dict[node.constant]
120    for coef, v in zip(node.linear_coefs, node.linear_vars):
121        coef_bnds = bnds_dict[coef]
122        v_bnds = bnds_dict[v]
123        term_bounds = interval.mul(*coef_bnds, *v_bnds)
124        lb, ub = interval.add(lb, ub, *term_bounds)
125    bnds_dict[node] = (lb, ub)
126
127
128def _prop_bnds_leaf_to_root_DivisionExpression(node, bnds_dict, feasibility_tol):
129    """
130
131    Parameters
132    ----------
133    node: pyomo.core.expr.numeric_expr.DivisionExpression
134    bnds_dict: ComponentMap
135    feasibility_tol: float
136        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
137        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
138        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
139        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
140        is more conservative).
141    """
142    assert len(node.args) == 2
143    arg1, arg2 = node.args
144    lb1, ub1 = bnds_dict[arg1]
145    lb2, ub2 = bnds_dict[arg2]
146    bnds_dict[node] = interval.div(lb1, ub1, lb2, ub2, feasibility_tol=feasibility_tol)
147
148
149def _prop_bnds_leaf_to_root_PowExpression(node, bnds_dict, feasibility_tol):
150    """
151
152    Parameters
153    ----------
154    node: pyomo.core.expr.numeric_expr.PowExpression
155    bnds_dict: ComponentMap
156    feasibility_tol: float
157        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
158        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
159        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
160        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
161        is more conservative).
162    """
163    assert len(node.args) == 2
164    arg1, arg2 = node.args
165    lb1, ub1 = bnds_dict[arg1]
166    lb2, ub2 = bnds_dict[arg2]
167    bnds_dict[node] = interval.power(lb1, ub1, lb2, ub2, feasibility_tol=feasibility_tol)
168
169
170def _prop_bnds_leaf_to_root_ReciprocalExpression(node, bnds_dict, feasibility_tol):
171    """
172
173    Parameters
174    ----------
175    node: pyomo.core.expr.numeric_expr.ReciprocalExpression
176    bnds_dict: ComponentMap
177    feasibility_tol: float
178        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
179        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
180        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
181        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
182        is more conservative).
183    """
184    assert len(node.args) == 1
185    arg = node.args[0]
186    lb1, ub1 = bnds_dict[arg]
187    bnds_dict[node] = interval.inv(lb1, ub1, feasibility_tol)
188
189
190def _prop_bnds_leaf_to_root_NegationExpression(node, bnds_dict, feasibility_tol):
191    """
192
193    Parameters
194    ----------
195    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
196    bnds_dict: ComponentMap
197    feasibility_tol: float
198        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
199        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
200        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
201        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
202        is more conservative).
203    """
204    assert len(node.args) == 1
205    arg = node.args[0]
206    lb1, ub1 = bnds_dict[arg]
207    bnds_dict[node] = interval.sub(0, 0, lb1, ub1)
208
209
210def _prop_bnds_leaf_to_root_exp(node, bnds_dict, feasibility_tol):
211    """
212
213    Parameters
214    ----------
215    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
216    bnds_dict: ComponentMap
217    feasibility_tol: float
218        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
219        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
220        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
221        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
222        is more conservative).
223    """
224    assert len(node.args) == 1
225    arg = node.args[0]
226    lb1, ub1 = bnds_dict[arg]
227    bnds_dict[node] = interval.exp(lb1, ub1)
228
229
230def _prop_bnds_leaf_to_root_log(node, bnds_dict, feasibility_tol):
231    """
232
233    Parameters
234    ----------
235    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
236    bnds_dict: ComponentMap
237    feasibility_tol: float
238        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
239        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
240        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
241        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
242        is more conservative).
243    """
244    assert len(node.args) == 1
245    arg = node.args[0]
246    lb1, ub1 = bnds_dict[arg]
247    bnds_dict[node] = interval.log(lb1, ub1)
248
249
250def _prop_bnds_leaf_to_root_log10(node, bnds_dict, feasibility_tol):
251    """
252
253    Parameters
254    ----------
255    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
256    bnds_dict: ComponentMap
257    feasibility_tol: float
258        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
259        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
260        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
261        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
262        is more conservative).
263    """
264    assert len(node.args) == 1
265    arg = node.args[0]
266    lb1, ub1 = bnds_dict[arg]
267    bnds_dict[node] = interval.log10(lb1, ub1)
268
269
270def _prop_bnds_leaf_to_root_sin(node, bnds_dict, feasibility_tol):
271    """
272
273    Parameters
274    ----------
275    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
276    bnds_dict: ComponentMap
277    feasibility_tol: float
278        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
279        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
280        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
281        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
282        is more conservative).
283    """
284    assert len(node.args) == 1
285    arg = node.args[0]
286    lb1, ub1 = bnds_dict[arg]
287    bnds_dict[node] = interval.sin(lb1, ub1)
288
289
290def _prop_bnds_leaf_to_root_cos(node, bnds_dict, feasibility_tol):
291    """
292
293    Parameters
294    ----------
295    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
296    bnds_dict: ComponentMap
297    feasibility_tol: float
298        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
299        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
300        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
301        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
302        is more conservative).
303    """
304    assert len(node.args) == 1
305    arg = node.args[0]
306    lb1, ub1 = bnds_dict[arg]
307    bnds_dict[node] = interval.cos(lb1, ub1)
308
309
310def _prop_bnds_leaf_to_root_tan(node, bnds_dict, feasibility_tol):
311    """
312
313    Parameters
314    ----------
315    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
316    bnds_dict: ComponentMap
317    feasibility_tol: float
318        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
319        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
320        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
321        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
322        is more conservative).
323    """
324    assert len(node.args) == 1
325    arg = node.args[0]
326    lb1, ub1 = bnds_dict[arg]
327    bnds_dict[node] = interval.tan(lb1, ub1)
328
329
330def _prop_bnds_leaf_to_root_asin(node, bnds_dict, feasibility_tol):
331    """
332
333    Parameters
334    ----------
335    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
336    bnds_dict: ComponentMap
337    feasibility_tol: float
338        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
339        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
340        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
341        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
342        is more conservative).
343    """
344    assert len(node.args) == 1
345    arg = node.args[0]
346    lb1, ub1 = bnds_dict[arg]
347    bnds_dict[node] = interval.asin(lb1, ub1, -interval.inf, interval.inf, feasibility_tol)
348
349
350def _prop_bnds_leaf_to_root_acos(node, bnds_dict, feasibility_tol):
351    """
352
353    Parameters
354    ----------
355    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
356    bnds_dict: ComponentMap
357    feasibility_tol: float
358        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
359        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
360        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
361        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
362        is more conservative).
363    """
364    assert len(node.args) == 1
365    arg = node.args[0]
366    lb1, ub1 = bnds_dict[arg]
367    bnds_dict[node] = interval.acos(lb1, ub1, -interval.inf, interval.inf, feasibility_tol)
368
369
370def _prop_bnds_leaf_to_root_atan(node, bnds_dict, feasibility_tol):
371    """
372
373    Parameters
374    ----------
375    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
376    bnds_dict: ComponentMap
377    feasibility_tol: float
378        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
379        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
380        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
381        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
382        is more conservative).
383    """
384    assert len(node.args) == 1
385    arg = node.args[0]
386    lb1, ub1 = bnds_dict[arg]
387    bnds_dict[node] = interval.atan(lb1, ub1, -interval.inf, interval.inf)
388
389
390def _prop_bnds_leaf_to_root_sqrt(node, bnds_dict, feasibility_tol):
391    """
392
393    Parameters
394    ----------
395    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
396    bnds_dict: ComponentMap
397    feasibility_tol: float
398        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
399        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
400        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
401        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
402        is more conservative).
403    """
404    assert len(node.args) == 1
405    arg = node.args[0]
406    lb1, ub1 = bnds_dict[arg]
407    bnds_dict[node] = interval.power(lb1, ub1, 0.5, 0.5, feasibility_tol=feasibility_tol)
408
409
410_unary_leaf_to_root_map = dict()
411_unary_leaf_to_root_map['exp'] = _prop_bnds_leaf_to_root_exp
412_unary_leaf_to_root_map['log'] = _prop_bnds_leaf_to_root_log
413_unary_leaf_to_root_map['log10'] = _prop_bnds_leaf_to_root_log10
414_unary_leaf_to_root_map['sin'] = _prop_bnds_leaf_to_root_sin
415_unary_leaf_to_root_map['cos'] = _prop_bnds_leaf_to_root_cos
416_unary_leaf_to_root_map['tan'] = _prop_bnds_leaf_to_root_tan
417_unary_leaf_to_root_map['asin'] = _prop_bnds_leaf_to_root_asin
418_unary_leaf_to_root_map['acos'] = _prop_bnds_leaf_to_root_acos
419_unary_leaf_to_root_map['atan'] = _prop_bnds_leaf_to_root_atan
420_unary_leaf_to_root_map['sqrt'] = _prop_bnds_leaf_to_root_sqrt
421
422
423def _prop_bnds_leaf_to_root_UnaryFunctionExpression(node, bnds_dict, feasibility_tol):
424    """
425
426    Parameters
427    ----------
428    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
429    bnds_dict: ComponentMap
430    feasibility_tol: float
431        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
432        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
433        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
434        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
435        is more conservative).
436    """
437    if node.getname() in _unary_leaf_to_root_map:
438        _unary_leaf_to_root_map[node.getname()](node, bnds_dict, feasibility_tol)
439    else:
440        bnds_dict[node] = (-interval.inf, interval.inf)
441
442
443def _prop_bnds_leaf_to_root_GeneralExpression(node, bnds_dict, feasibility_tol):
444    """
445    Propagate bounds from children to parent
446
447    Parameters
448    ----------
449    node: pyomo.core.base.expression._GeneralExpressionData
450    bnds_dict: ComponentMap
451    feasibility_tol: float
452        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
453        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
454        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
455        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
456        is more conservative).
457    """
458    expr_lb, expr_ub = bnds_dict[node.expr]
459    bnds_dict[node] = (expr_lb, expr_ub)
460
461
462_prop_bnds_leaf_to_root_map = dict()
463_prop_bnds_leaf_to_root_map[numeric_expr.ProductExpression] = _prop_bnds_leaf_to_root_ProductExpression
464_prop_bnds_leaf_to_root_map[numeric_expr.DivisionExpression] = _prop_bnds_leaf_to_root_DivisionExpression
465_prop_bnds_leaf_to_root_map[numeric_expr.ReciprocalExpression] = _prop_bnds_leaf_to_root_ReciprocalExpression
466_prop_bnds_leaf_to_root_map[numeric_expr.PowExpression] = _prop_bnds_leaf_to_root_PowExpression
467_prop_bnds_leaf_to_root_map[numeric_expr.SumExpression] = _prop_bnds_leaf_to_root_SumExpression
468_prop_bnds_leaf_to_root_map[numeric_expr.MonomialTermExpression] = _prop_bnds_leaf_to_root_ProductExpression
469_prop_bnds_leaf_to_root_map[numeric_expr.NegationExpression] = _prop_bnds_leaf_to_root_NegationExpression
470_prop_bnds_leaf_to_root_map[numeric_expr.UnaryFunctionExpression] = _prop_bnds_leaf_to_root_UnaryFunctionExpression
471_prop_bnds_leaf_to_root_map[numeric_expr.LinearExpression] = _prop_bnds_leaf_to_root_LinearExpression
472
473_prop_bnds_leaf_to_root_map[numeric_expr.NPV_ProductExpression] = _prop_bnds_leaf_to_root_ProductExpression
474_prop_bnds_leaf_to_root_map[numeric_expr.NPV_DivisionExpression] = _prop_bnds_leaf_to_root_DivisionExpression
475_prop_bnds_leaf_to_root_map[numeric_expr.NPV_ReciprocalExpression] = _prop_bnds_leaf_to_root_ReciprocalExpression
476_prop_bnds_leaf_to_root_map[numeric_expr.NPV_PowExpression] = _prop_bnds_leaf_to_root_PowExpression
477_prop_bnds_leaf_to_root_map[numeric_expr.NPV_SumExpression] = _prop_bnds_leaf_to_root_SumExpression
478_prop_bnds_leaf_to_root_map[numeric_expr.NPV_NegationExpression] = _prop_bnds_leaf_to_root_NegationExpression
479_prop_bnds_leaf_to_root_map[numeric_expr.NPV_UnaryFunctionExpression] = _prop_bnds_leaf_to_root_UnaryFunctionExpression
480
481_prop_bnds_leaf_to_root_map[_GeneralExpressionData] = _prop_bnds_leaf_to_root_GeneralExpression
482_prop_bnds_leaf_to_root_map[ScalarExpression] = _prop_bnds_leaf_to_root_GeneralExpression
483
484
485def _prop_bnds_root_to_leaf_ProductExpression(node, bnds_dict, feasibility_tol):
486    """
487
488    Parameters
489    ----------
490    node: pyomo.core.expr.numeric_expr.ProductExpression
491    bnds_dict: ComponentMap
492    feasibility_tol: float
493        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
494        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
495        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
496        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
497        is more conservative).
498    """
499    assert len(node.args) == 2
500    arg1, arg2 = node.args
501    lb0, ub0 = bnds_dict[node]
502    lb1, ub1 = bnds_dict[arg1]
503    lb2, ub2 = bnds_dict[arg2]
504    if arg1 is arg2:
505        _lb1, _ub1 = interval._inverse_power1(lb0, ub0, 2, 2, orig_xl=lb1, orig_xu=ub1, feasibility_tol=feasibility_tol)
506        _lb2, _ub2 = _lb1, _ub1
507    else:
508        _lb1, _ub1 = interval.div(lb0, ub0, lb2, ub2, feasibility_tol)
509        _lb2, _ub2 = interval.div(lb0, ub0, lb1, ub1, feasibility_tol)
510    if _lb1 > lb1:
511        lb1 = _lb1
512    if _ub1 < ub1:
513        ub1 = _ub1
514    if _lb2 > lb2:
515        lb2 = _lb2
516    if _ub2 < ub2:
517        ub2 = _ub2
518    bnds_dict[arg1] = (lb1, ub1)
519    bnds_dict[arg2] = (lb2, ub2)
520
521
522def _prop_bnds_root_to_leaf_SumExpression(node, bnds_dict, feasibility_tol):
523    """
524    This function is a bit complicated. A simpler implementation
525    would loop through each argument in the sum and do the following:
526
527    bounds_on_arg_i = bounds_on_entire_sum - bounds_on_sum_of_args_excluding_arg_i
528
529    and the bounds_on_sum_of_args_excluding_arg_i could be computed
530    for each argument. However, the computational expense would grow
531    approximately quadratically with the length of the sum. Thus,
532    we do the following. Consider the expression
533
534    y = x1 + x2 + x3 + x4
535
536    and suppose we have bounds on y. We first accumulate bounds to
537    obtain a list like the following
538
539    [(x1)_bounds, (x1+x2)_bounds, (x1+x2+x3)_bounds, (x1+x2+x3+x4)_bounds]
540
541    Then we can propagate bounds back to x1, x2, x3, and x4 with the
542    following
543
544    (x4)_bounds = (x1+x2+x3+x4)_bounds - (x1+x2+x3)_bounds
545    (x3)_bounds = (x1+x2+x3)_bounds - (x1+x2)_bounds
546    (x2)_bounds = (x1+x2)_bounds - (x1)_bounds
547
548    Parameters
549    ----------
550    node: pyomo.core.expr.numeric_expr.ProductExpression
551    bnds_dict: ComponentMap
552    feasibility_tol: float
553        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
554        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
555        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
556        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
557        is more conservative).
558    """
559    # first accumulate bounds
560    accumulated_bounds = list()
561    accumulated_bounds.append(bnds_dict[node.arg(0)])
562    lb0, ub0 = bnds_dict[node]
563    for i in range(1, node.nargs()):
564        _lb0, _ub0 = accumulated_bounds[i-1]
565        _lb1, _ub1 = bnds_dict[node.arg(i)]
566        accumulated_bounds.append(interval.add(_lb0, _ub0, _lb1, _ub1))
567    if lb0 > accumulated_bounds[node.nargs() - 1][0]:
568        accumulated_bounds[node.nargs() - 1] = (lb0, accumulated_bounds[node.nargs()-1][1])
569    if ub0 < accumulated_bounds[node.nargs() - 1][1]:
570        accumulated_bounds[node.nargs() - 1] = (accumulated_bounds[node.nargs()-1][0], ub0)
571
572    for i in reversed(range(1, node.nargs())):
573        lb0, ub0 = accumulated_bounds[i]
574        lb1, ub1 = accumulated_bounds[i-1]
575        lb2, ub2 = bnds_dict[node.arg(i)]
576        _lb1, _ub1 = interval.sub(lb0, ub0, lb2, ub2)
577        _lb2, _ub2 = interval.sub(lb0, ub0, lb1, ub1)
578        if _lb1 > lb1:
579            lb1 = _lb1
580        if _ub1 < ub1:
581            ub1 = _ub1
582        if _lb2 > lb2:
583            lb2 = _lb2
584        if _ub2 < ub2:
585            ub2 = _ub2
586        accumulated_bounds[i-1] = (lb1, ub1)
587        bnds_dict[node.arg(i)] = (lb2, ub2)
588    lb, ub = bnds_dict[node.arg(0)]
589    _lb, _ub = accumulated_bounds[0]
590    if _lb > lb:
591        lb = _lb
592    if _ub < ub:
593        ub = _ub
594    bnds_dict[node.arg(0)] = (lb, ub)
595
596
597def _prop_bnds_root_to_leaf_LinearExpression(node: numeric_expr.LinearExpression,
598                                             bnds_dict: ComponentMap,
599                                             feasibility_tol: float):
600    """
601    This is very similar to SumExpression.
602    """
603    # first accumulate bounds
604    accumulated_bounds = list()
605    accumulated_bounds.append(bnds_dict[node.constant])
606    lb0, ub0 = bnds_dict[node]
607    for coef, v in zip(node.linear_coefs, node.linear_vars):
608        _lb0, _ub0 = accumulated_bounds[-1]
609        _lb_coef, _ub_coef = bnds_dict[coef]
610        _lb_v, _ub_v = bnds_dict[v]
611        _lb_term, _ub_term = interval.mul(_lb_coef, _ub_coef, _lb_v, _ub_v)
612        accumulated_bounds.append(interval.add(_lb0, _ub0, _lb_term, _ub_term))
613    if lb0 > accumulated_bounds[-1][0]:
614        accumulated_bounds[-1] = (lb0, accumulated_bounds[-1][1])
615    if ub0 < accumulated_bounds[-1][1]:
616        accumulated_bounds[-1] = (accumulated_bounds[-1][0], ub0)
617
618    for i in reversed(range(len(node.linear_coefs))):
619        lb0, ub0 = accumulated_bounds[i + 1]
620        lb1, ub1 = accumulated_bounds[i]
621        coef = node.linear_coefs[i]
622        v = node.linear_vars[i]
623        coef_bnds = bnds_dict[coef]
624        v_bnds = bnds_dict[v]
625        lb2, ub2 = interval.mul(*coef_bnds, *v_bnds)
626        _lb1, _ub1 = interval.sub(lb0, ub0, lb2, ub2)
627        _lb2, _ub2 = interval.sub(lb0, ub0, lb1, ub1)
628        if _lb1 > lb1:
629            lb1 = _lb1
630        if _ub1 < ub1:
631            ub1 = _ub1
632        if _lb2 > lb2:
633            lb2 = _lb2
634        if _ub2 < ub2:
635            ub2 = _ub2
636        accumulated_bounds[i] = (lb1, ub1)
637        bnds_dict[v] = interval.div(lb2, ub2, *coef_bnds, feasibility_tol=feasibility_tol)
638
639
640def _prop_bnds_root_to_leaf_DivisionExpression(node, bnds_dict, feasibility_tol):
641    """
642
643    Parameters
644    ----------
645    node: pyomo.core.expr.numeric_expr.DivisionExpression
646    bnds_dict: ComponentMap
647    feasibility_tol: float
648        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
649        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
650        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
651        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
652        is more conservative).
653    """
654    assert len(node.args) == 2
655    arg1, arg2 = node.args
656    lb0, ub0 = bnds_dict[node]
657    lb1, ub1 = bnds_dict[arg1]
658    lb2, ub2 = bnds_dict[arg2]
659    _lb1, _ub1 = interval.mul(lb0, ub0, lb2, ub2)
660    _lb2, _ub2 = interval.div(lb1, ub1, lb0, ub0, feasibility_tol=feasibility_tol)
661    if _lb1 > lb1:
662        lb1 = _lb1
663    if _ub1 < ub1:
664        ub1 = _ub1
665    if _lb2 > lb2:
666        lb2 = _lb2
667    if _ub2 < ub2:
668        ub2 = _ub2
669    bnds_dict[arg1] = (lb1, ub1)
670    bnds_dict[arg2] = (lb2, ub2)
671
672
673def _prop_bnds_root_to_leaf_PowExpression(node, bnds_dict, feasibility_tol):
674    """
675
676    Parameters
677    ----------
678    node: pyomo.core.expr.numeric_expr.PowExpression
679    bnds_dict: ComponentMap
680    feasibility_tol: float
681        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
682        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
683        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
684        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
685        is more conservative).
686    """
687    assert len(node.args) == 2
688    arg1, arg2 = node.args
689    lb0, ub0 = bnds_dict[node]
690    lb1, ub1 = bnds_dict[arg1]
691    lb2, ub2 = bnds_dict[arg2]
692    _lb1, _ub1 = interval._inverse_power1(lb0, ub0, lb2, ub2, orig_xl=lb1, orig_xu=ub1, feasibility_tol=feasibility_tol)
693    if _lb1 > lb1:
694        lb1 = _lb1
695    if _ub1 < ub1:
696        ub1 = _ub1
697    bnds_dict[arg1] = (lb1, ub1)
698
699    if is_fixed(arg2) and lb2 == ub2:  # No need to tighten the bounds on arg2 if arg2 is fixed
700        pass
701    else:
702        _lb2, _ub2 = interval._inverse_power2(lb0, ub0, lb1, ub1, feasiblity_tol=feasibility_tol)
703        if _lb2 > lb2:
704            lb2 = _lb2
705        if _ub2 < ub2:
706            ub2 = _ub2
707        bnds_dict[arg2] = (lb2, ub2)
708
709
710def _prop_bnds_root_to_leaf_sqrt(node, bnds_dict, feasibility_tol):
711    """
712
713    Parameters
714    ----------
715    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
716    bnds_dict: ComponentMap
717    feasibility_tol: float
718        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
719        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
720        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
721        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
722        is more conservative).
723    """
724    assert len(node.args) == 1
725    arg1 = node.args[0]
726    lb0, ub0 = bnds_dict[node]
727    lb1, ub1 = bnds_dict[arg1]
728    lb2, ub2 = (0.5, 0.5)
729    _lb1, _ub1 = interval._inverse_power1(lb0, ub0, lb2, ub2, orig_xl=lb1, orig_xu=ub1, feasibility_tol=feasibility_tol)
730    if _lb1 > lb1:
731        lb1 = _lb1
732    if _ub1 < ub1:
733        ub1 = _ub1
734    bnds_dict[arg1] = (lb1, ub1)
735
736
737def _prop_bnds_root_to_leaf_ReciprocalExpression(node, bnds_dict, feasibility_tol):
738    """
739
740    Parameters
741    ----------
742    node: pyomo.core.expr.numeric_expr.ReciprocalExpression
743    bnds_dict: ComponentMap
744    feasibility_tol: float
745        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
746        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
747        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
748        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
749        is more conservative).
750    """
751    assert len(node.args) == 1
752    arg = node.args[0]
753    lb0, ub0 = bnds_dict[node]
754    lb1, ub1 = bnds_dict[arg]
755    _lb1, _ub1 = interval.inv(lb0, ub0, feasibility_tol)
756    if _lb1 > lb1:
757        lb1 = _lb1
758    if _ub1 < ub1:
759        ub1 = _ub1
760    bnds_dict[arg] = (lb1, ub1)
761
762
763def _prop_bnds_root_to_leaf_NegationExpression(node, bnds_dict, feasibility_tol):
764    """
765
766    Parameters
767    ----------
768    node: pyomo.core.expr.numeric_expr.NegationExpression
769    bnds_dict: ComponentMap
770    feasibility_tol: float
771        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
772        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
773        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
774        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
775        is more conservative).
776    """
777    assert len(node.args) == 1
778    arg = node.args[0]
779    lb0, ub0 = bnds_dict[node]
780    lb1, ub1 = bnds_dict[arg]
781    _lb1, _ub1 = interval.sub(0, 0, lb0, ub0)
782    if _lb1 > lb1:
783        lb1 = _lb1
784    if _ub1 < ub1:
785        ub1 = _ub1
786    bnds_dict[arg] = (lb1, ub1)
787
788
789def _prop_bnds_root_to_leaf_exp(node, bnds_dict, feasibility_tol):
790    """
791
792    Parameters
793    ----------
794    node: pyomo.core.expr.numeric_expr.ProductExpression
795    bnds_dict: ComponentMap
796    feasibility_tol: float
797        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
798        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
799        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
800        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
801        is more conservative).
802    """
803    assert len(node.args) == 1
804    arg = node.args[0]
805    lb0, ub0 = bnds_dict[node]
806    lb1, ub1 = bnds_dict[arg]
807    _lb1, _ub1 = interval.log(lb0, ub0)
808    if _lb1 > lb1:
809        lb1 = _lb1
810    if _ub1 < ub1:
811        ub1 = _ub1
812    bnds_dict[arg] = (lb1, ub1)
813
814
815def _prop_bnds_root_to_leaf_log(node, bnds_dict, feasibility_tol):
816    """
817
818    Parameters
819    ----------
820    node: pyomo.core.expr.numeric_expr.ProductExpression
821    bnds_dict: ComponentMap
822    feasibility_tol: float
823        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
824        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
825        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
826        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
827        is more conservative).
828    """
829    assert len(node.args) == 1
830    arg = node.args[0]
831    lb0, ub0 = bnds_dict[node]
832    lb1, ub1 = bnds_dict[arg]
833    _lb1, _ub1 = interval.exp(lb0, ub0)
834    if _lb1 > lb1:
835        lb1 = _lb1
836    if _ub1 < ub1:
837        ub1 = _ub1
838    bnds_dict[arg] = (lb1, ub1)
839
840
841def _prop_bnds_root_to_leaf_log10(node, bnds_dict, feasibility_tol):
842    """
843
844    Parameters
845    ----------
846    node: pyomo.core.expr.numeric_expr.ProductExpression
847    bnds_dict: ComponentMap
848    feasibility_tol: float
849        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
850        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
851        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
852        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
853        is more conservative).
854    """
855    assert len(node.args) == 1
856    arg = node.args[0]
857    lb0, ub0 = bnds_dict[node]
858    lb1, ub1 = bnds_dict[arg]
859    _lb1, _ub1 = interval.power(10, 10, lb0, ub0, feasibility_tol=feasibility_tol)
860    if _lb1 > lb1:
861        lb1 = _lb1
862    if _ub1 < ub1:
863        ub1 = _ub1
864    bnds_dict[arg] = (lb1, ub1)
865
866
867def _prop_bnds_root_to_leaf_sin(node, bnds_dict, feasibility_tol):
868    """
869
870    Parameters
871    ----------
872    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
873    bnds_dict: ComponentMap
874    feasibility_tol: float
875        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
876        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
877        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
878        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
879        is more conservative).
880    """
881    assert len(node.args) == 1
882    arg = node.args[0]
883    lb0, ub0 = bnds_dict[node]
884    lb1, ub1 = bnds_dict[arg]
885    _lb1, _ub1 = interval.asin(lb0, ub0, lb1, ub1, feasibility_tol)
886    if _lb1 > lb1:
887        lb1 = _lb1
888    if _ub1 < ub1:
889        ub1 = _ub1
890    bnds_dict[arg] = (lb1, ub1)
891
892
893def _prop_bnds_root_to_leaf_cos(node, bnds_dict, feasibility_tol):
894    """
895
896    Parameters
897    ----------
898    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
899    bnds_dict: ComponentMap
900    feasibility_tol: float
901        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
902        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
903        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
904        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
905        is more conservative).
906    """
907    assert len(node.args) == 1
908    arg = node.args[0]
909    lb0, ub0 = bnds_dict[node]
910    lb1, ub1 = bnds_dict[arg]
911    _lb1, _ub1 = interval.acos(lb0, ub0, lb1, ub1, feasibility_tol)
912    if _lb1 > lb1:
913        lb1 = _lb1
914    if _ub1 < ub1:
915        ub1 = _ub1
916    bnds_dict[arg] = (lb1, ub1)
917
918
919def _prop_bnds_root_to_leaf_tan(node, bnds_dict, feasibility_tol):
920    """
921
922    Parameters
923    ----------
924    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
925    bnds_dict: ComponentMap
926    feasibility_tol: float
927        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
928        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
929        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
930        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
931        is more conservative).
932    """
933    assert len(node.args) == 1
934    arg = node.args[0]
935    lb0, ub0 = bnds_dict[node]
936    lb1, ub1 = bnds_dict[arg]
937    _lb1, _ub1 = interval.atan(lb0, ub0, lb1, ub1)
938    if _lb1 > lb1:
939        lb1 = _lb1
940    if _ub1 < ub1:
941        ub1 = _ub1
942    bnds_dict[arg] = (lb1, ub1)
943
944
945def _prop_bnds_root_to_leaf_asin(node, bnds_dict, feasibility_tol):
946    """
947
948    Parameters
949    ----------
950    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
951    bnds_dict: ComponentMap
952    feasibility_tol: float
953        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
954        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
955        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
956        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
957        is more conservative).
958    """
959    assert len(node.args) == 1
960    arg = node.args[0]
961    lb0, ub0 = bnds_dict[node]
962    lb1, ub1 = bnds_dict[arg]
963    _lb1, _ub1 = interval.sin(lb0, ub0)
964    if _lb1 > lb1:
965        lb1 = _lb1
966    if _ub1 < ub1:
967        ub1 = _ub1
968    bnds_dict[arg] = (lb1, ub1)
969
970
971def _prop_bnds_root_to_leaf_acos(node, bnds_dict, feasibility_tol):
972    """
973
974    Parameters
975    ----------
976    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
977    bnds_dict: ComponentMap
978    feasibility_tol: float
979        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
980        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
981        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
982        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
983        is more conservative).
984    """
985    assert len(node.args) == 1
986    arg = node.args[0]
987    lb0, ub0 = bnds_dict[node]
988    lb1, ub1 = bnds_dict[arg]
989    _lb1, _ub1 = interval.cos(lb0, ub0)
990    if _lb1 > lb1:
991        lb1 = _lb1
992    if _ub1 < ub1:
993        ub1 = _ub1
994    bnds_dict[arg] = (lb1, ub1)
995
996
997def _prop_bnds_root_to_leaf_atan(node, bnds_dict, feasibility_tol):
998    """
999
1000    Parameters
1001    ----------
1002    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
1003    bnds_dict: ComponentMap
1004    feasibility_tol: float
1005        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
1006        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
1007        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
1008        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
1009        is more conservative).
1010    """
1011    assert len(node.args) == 1
1012    arg = node.args[0]
1013    lb0, ub0 = bnds_dict[node]
1014    lb1, ub1 = bnds_dict[arg]
1015    _lb1, _ub1 = interval.tan(lb0, ub0)
1016    if _lb1 > lb1:
1017        lb1 = _lb1
1018    if _ub1 < ub1:
1019        ub1 = _ub1
1020    bnds_dict[arg] = (lb1, ub1)
1021
1022
1023_unary_root_to_leaf_map = dict()
1024_unary_root_to_leaf_map['exp'] = _prop_bnds_root_to_leaf_exp
1025_unary_root_to_leaf_map['log'] = _prop_bnds_root_to_leaf_log
1026_unary_root_to_leaf_map['log10'] = _prop_bnds_root_to_leaf_log10
1027_unary_root_to_leaf_map['sin'] = _prop_bnds_root_to_leaf_sin
1028_unary_root_to_leaf_map['cos'] = _prop_bnds_root_to_leaf_cos
1029_unary_root_to_leaf_map['tan'] = _prop_bnds_root_to_leaf_tan
1030_unary_root_to_leaf_map['asin'] = _prop_bnds_root_to_leaf_asin
1031_unary_root_to_leaf_map['acos'] = _prop_bnds_root_to_leaf_acos
1032_unary_root_to_leaf_map['atan'] = _prop_bnds_root_to_leaf_atan
1033_unary_root_to_leaf_map['sqrt'] = _prop_bnds_root_to_leaf_sqrt
1034
1035
1036def _prop_bnds_root_to_leaf_UnaryFunctionExpression(node, bnds_dict, feasibility_tol):
1037    """
1038
1039    Parameters
1040    ----------
1041    node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression
1042    bnds_dict: ComponentMap
1043    feasibility_tol: float
1044        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
1045        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
1046        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
1047        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
1048        is more conservative).
1049    """
1050    if node.getname() in _unary_root_to_leaf_map:
1051        _unary_root_to_leaf_map[node.getname()](node, bnds_dict, feasibility_tol)
1052    else:
1053        logger.warning('Unsupported expression type for FBBT: {0}. Bounds will not be improved in this part of '
1054                       'the tree.'
1055                       ''.format(node.getname()))
1056
1057
1058def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol):
1059    """
1060    Propagate bounds from parent to children.
1061
1062    Parameters
1063    ----------
1064    node: pyomo.core.base.expression._GeneralExpressionData
1065    bnds_dict: ComponentMap
1066    feasibility_tol: float
1067        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
1068        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
1069        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
1070        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
1071        is more conservative).
1072    """
1073    expr_lb, expr_ub = bnds_dict[node]
1074    bnds_dict[node.expr] = (expr_lb, expr_ub)
1075
1076
1077_prop_bnds_root_to_leaf_map = dict()
1078_prop_bnds_root_to_leaf_map[numeric_expr.ProductExpression] = _prop_bnds_root_to_leaf_ProductExpression
1079_prop_bnds_root_to_leaf_map[numeric_expr.DivisionExpression] = _prop_bnds_root_to_leaf_DivisionExpression
1080_prop_bnds_root_to_leaf_map[numeric_expr.ReciprocalExpression] = _prop_bnds_root_to_leaf_ReciprocalExpression
1081_prop_bnds_root_to_leaf_map[numeric_expr.PowExpression] = _prop_bnds_root_to_leaf_PowExpression
1082_prop_bnds_root_to_leaf_map[numeric_expr.SumExpression] = _prop_bnds_root_to_leaf_SumExpression
1083_prop_bnds_root_to_leaf_map[numeric_expr.MonomialTermExpression] = _prop_bnds_root_to_leaf_ProductExpression
1084_prop_bnds_root_to_leaf_map[numeric_expr.NegationExpression] = _prop_bnds_root_to_leaf_NegationExpression
1085_prop_bnds_root_to_leaf_map[numeric_expr.UnaryFunctionExpression] = _prop_bnds_root_to_leaf_UnaryFunctionExpression
1086_prop_bnds_root_to_leaf_map[numeric_expr.LinearExpression] = _prop_bnds_root_to_leaf_LinearExpression
1087
1088_prop_bnds_root_to_leaf_map[numeric_expr.NPV_ProductExpression] = _prop_bnds_root_to_leaf_ProductExpression
1089_prop_bnds_root_to_leaf_map[numeric_expr.NPV_DivisionExpression] = _prop_bnds_root_to_leaf_DivisionExpression
1090_prop_bnds_root_to_leaf_map[numeric_expr.NPV_ReciprocalExpression] = _prop_bnds_root_to_leaf_ReciprocalExpression
1091_prop_bnds_root_to_leaf_map[numeric_expr.NPV_PowExpression] = _prop_bnds_root_to_leaf_PowExpression
1092_prop_bnds_root_to_leaf_map[numeric_expr.NPV_SumExpression] = _prop_bnds_root_to_leaf_SumExpression
1093_prop_bnds_root_to_leaf_map[numeric_expr.NPV_NegationExpression] = _prop_bnds_root_to_leaf_NegationExpression
1094_prop_bnds_root_to_leaf_map[numeric_expr.NPV_UnaryFunctionExpression] = _prop_bnds_root_to_leaf_UnaryFunctionExpression
1095
1096_prop_bnds_root_to_leaf_map[_GeneralExpressionData] = _prop_bnds_root_to_leaf_GeneralExpression
1097_prop_bnds_root_to_leaf_map[ScalarExpression] = _prop_bnds_root_to_leaf_GeneralExpression
1098
1099
1100def _check_and_reset_bounds(var, lb, ub):
1101    """
1102    This function ensures that lb is not less than var.lb and that ub is not greater than var.ub.
1103    """
1104    orig_lb = value(var.lb)
1105    orig_ub = value(var.ub)
1106    if orig_lb is None:
1107        orig_lb = -interval.inf
1108    if orig_ub is None:
1109        orig_ub = interval.inf
1110    if lb < orig_lb:
1111        lb = orig_lb
1112    if ub > orig_ub:
1113        ub = orig_ub
1114    return lb, ub
1115
1116
1117class _FBBTVisitorLeafToRoot(ExpressionValueVisitor):
1118    """
1119    This walker propagates bounds from the variables to each node in
1120    the expression tree (all the way to the root node).
1121    """
1122    def __init__(self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8):
1123        """
1124        Parameters
1125        ----------
1126        bnds_dict: ComponentMap
1127        integer_tol: float
1128        feasibility_tol: float
1129            If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
1130            feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
1131            is also used when performing certain interval arithmetic operations to ensure that none of the feasible
1132            region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
1133            is more conservative).
1134        """
1135        self.bnds_dict = bnds_dict
1136        self.integer_tol = integer_tol
1137        self.feasibility_tol = feasibility_tol
1138
1139    def visit(self, node, values):
1140        if node.__class__ in _prop_bnds_leaf_to_root_map:
1141            _prop_bnds_leaf_to_root_map[node.__class__](node, self.bnds_dict, self.feasibility_tol)
1142        else:
1143            self.bnds_dict[node] = (-interval.inf, interval.inf)
1144        return None
1145
1146    def visiting_potential_leaf(self, node):
1147        if node.__class__ in nonpyomo_leaf_types:
1148            self.bnds_dict[node] = (node, node)
1149            return True, None
1150
1151        if node.is_variable_type():
1152            if node in self.bnds_dict:
1153                return True, None
1154            if node.is_fixed():
1155                lb = value(node.value)
1156                ub = lb
1157            else:
1158                lb = value(node.lb)
1159                ub = value(node.ub)
1160                if lb is None:
1161                    lb = -interval.inf
1162                if ub is None:
1163                    ub = interval.inf
1164                if lb - self.feasibility_tol > ub:
1165                    raise InfeasibleConstraintException('Variable has a lower bound which is larger than its upper bound: {0}'.format(str(node)))
1166            self.bnds_dict[node] = (lb, ub)
1167            return True, None
1168
1169        if node.__class__ is numeric_expr.LinearExpression:
1170            const_val = value(node.constant)
1171            self.bnds_dict[node.constant] = (const_val, const_val)
1172            for coef in node.linear_coefs:
1173                coef_val = value(coef)
1174                self.bnds_dict[coef] = (coef_val, coef_val)
1175            for v in node.linear_vars:
1176                self.visiting_potential_leaf(v)
1177            _prop_bnds_leaf_to_root_LinearExpression(node, self.bnds_dict, self.feasibility_tol)
1178            return True, None
1179
1180        if not node.is_expression_type():
1181            assert is_fixed(node)
1182            val = value(node)
1183            self.bnds_dict[node] = (val, val)
1184            return True, None
1185
1186        return False, None
1187
1188
1189class _FBBTVisitorRootToLeaf(ExpressionValueVisitor):
1190    """
1191    This walker propagates bounds from the constraint back to the
1192    variables. Note that the bounds on every node in the tree must
1193    first be computed with _FBBTVisitorLeafToRoot.
1194    """
1195    def __init__(self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8):
1196        """
1197        Parameters
1198        ----------
1199        bnds_dict: ComponentMap
1200        integer_tol: float
1201        feasibility_tol: float
1202            If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
1203            feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
1204            is also used when performing certain interval arithmetic operations to ensure that none of the feasible
1205            region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
1206            is more conservative).
1207        """
1208        self.bnds_dict = bnds_dict
1209        self.integer_tol = integer_tol
1210        self.feasibility_tol = feasibility_tol
1211
1212    def visit(self, node, values):
1213        pass
1214
1215    def visiting_potential_leaf(self, node):
1216        if node.__class__ in nonpyomo_leaf_types:
1217            lb, ub = self.bnds_dict[node]
1218            if abs(lb - value(node)) > self.feasibility_tol:
1219                raise InfeasibleConstraintException('Detected an infeasible constraint.')
1220            if abs(ub - value(node)) > self.feasibility_tol:
1221                raise InfeasibleConstraintException('Detected an infeasible constraint.')
1222            return True, None
1223
1224        if node.is_variable_type():
1225            lb, ub = self.bnds_dict[node]
1226
1227            lb, ub = self.bnds_dict[node]
1228            if lb > ub:
1229                if lb - self.feasibility_tol > ub:
1230                    raise InfeasibleConstraintException('Lower bound ({1}) computed for variable {0} is larger than the computed upper bound ({2}).'.format(node, lb, ub))
1231                else:
1232                    """
1233                    If we reach this code, then lb > ub, but not by more than feasibility_tol.
1234                    Now we want to decrease lb slightly and increase ub slightly so that lb <= ub.
1235                    However, we also have to make sure we do not make lb lower than the original lower bound
1236                    and make sure we do not make ub larger than the original upper bound. This is what
1237                    _check_and_reset_bounds is for.
1238                    """
1239                    lb -= self.feasibility_tol
1240                    ub += self.feasibility_tol
1241                    lb, ub = _check_and_reset_bounds(node, lb, ub)
1242                    self.bnds_dict[node] = (lb, ub)
1243            if lb == interval.inf:
1244                raise InfeasibleConstraintException('Computed a lower bound of +inf for variable {0}'.format(node))
1245            if ub == -interval.inf:
1246                raise InfeasibleConstraintException('Computed an upper bound of -inf for variable {0}'.format(node))
1247
1248            if node.is_binary() or node.is_integer():
1249                """
1250                This bit of code has two purposes:
1251                1) Improve the bounds on binary and integer variables with the fact that they are integer.
1252                2) Account for roundoff error. If the lower bound of a binary variable comes back as
1253                   1e-16, the lower bound may actually be 0. This could potentially cause problems when
1254                   handing the problem to a MIP solver. Some solvers are robust to this, but some may not be
1255                   and may give the wrong solution. Even if the correct solution is found, this could
1256                   introduce numerical problems.
1257                """
1258                if lb > -interval.inf:
1259                    lb = max(math.floor(lb), math.ceil(lb - self.integer_tol))
1260                if ub < interval.inf:
1261                    ub = min(math.ceil(ub), math.floor(ub + self.integer_tol))
1262                """
1263                We have to make sure we do not make lb lower than the original lower bound
1264                and make sure we do not make ub larger than the original upper bound. This is what
1265                _check_and_reset_bounds is for.
1266                """
1267                lb, ub = _check_and_reset_bounds(node, lb, ub)
1268                self.bnds_dict[node] = (lb, ub)
1269
1270            if lb != -interval.inf:
1271                node.setlb(lb)
1272            if ub != interval.inf:
1273                node.setub(ub)
1274            return True, None
1275
1276        if node.__class__ is numeric_expr.LinearExpression:
1277            _prop_bnds_root_to_leaf_LinearExpression(node, self.bnds_dict, self.feasibility_tol)
1278            for v in node.linear_vars:
1279                self.visiting_potential_leaf(v)
1280            return True, None
1281
1282        if not node.is_expression_type():
1283            lb, ub = self.bnds_dict[node]
1284            if abs(lb - value(node)) > self.feasibility_tol:
1285                raise InfeasibleConstraintException('Detected an infeasible constraint.')
1286            if abs(ub - value(node)) > self.feasibility_tol:
1287                raise InfeasibleConstraintException('Detected an infeasible constraint.')
1288            return True, None
1289
1290        if node.__class__ in _prop_bnds_root_to_leaf_map:
1291            _prop_bnds_root_to_leaf_map[node.__class__](node, self.bnds_dict, self.feasibility_tol)
1292        else:
1293            logger.warning('Unsupported expression type for FBBT: {0}. Bounds will not be improved in this part of '
1294                           'the tree.'
1295                           ''.format(str(type(node))))
1296
1297        return False, None
1298
1299
1300def _fbbt_con(con, config):
1301    """
1302    Feasibility based bounds tightening for a constraint. This function attempts to improve the bounds of each variable
1303    in the constraint based on the bounds of the constraint and the bounds of the other variables in the constraint.
1304    For example:
1305
1306    >>> import pyomo.environ as pe
1307    >>> from pyomo.contrib.fbbt.fbbt import fbbt
1308    >>> m = pe.ConcreteModel()
1309    >>> m.x = pe.Var(bounds=(-1,1))
1310    >>> m.y = pe.Var(bounds=(-2,2))
1311    >>> m.z = pe.Var()
1312    >>> m.c = pe.Constraint(expr=m.x*m.y + m.z == 1)
1313    >>> fbbt(m.c)
1314    >>> print(m.z.lb, m.z.ub)
1315    -1.0 3.0
1316
1317    Parameters
1318    ----------
1319    con: pyomo.core.base.constraint.Constraint
1320        constraint on which to perform fbbt
1321    config: ConfigBlock
1322        see documentation for fbbt
1323
1324    Returns
1325    -------
1326    new_var_bounds: ComponentMap
1327        A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed
1328        from FBBT.
1329    """
1330    if not con.active:
1331        return ComponentMap()
1332
1333    bnds_dict = ComponentMap()  # a dictionary to store the bounds of every node in the tree
1334
1335    # a walker to propagate bounds from the variables to the root
1336    visitorA = _FBBTVisitorLeafToRoot(bnds_dict, feasibility_tol=config.feasibility_tol)
1337    visitorA.dfs_postorder_stack(con.body)
1338
1339    # Now we need to replace the bounds in bnds_dict for the root
1340    # node with the bounds on the constraint (if those bounds are
1341    # better).
1342    _lb = value(con.lower)
1343    _ub = value(con.upper)
1344    if _lb is None:
1345        _lb = -interval.inf
1346    if _ub is None:
1347        _ub = interval.inf
1348
1349    lb, ub = bnds_dict[con.body]
1350
1351    # check if the constraint is infeasible
1352    if lb > _ub + config.feasibility_tol or ub < _lb - config.feasibility_tol:
1353        raise InfeasibleConstraintException('Detected an infeasible constraint during FBBT: {0}'.format(str(con)))
1354
1355    # check if the constraint is always satisfied
1356    if config.deactivate_satisfied_constraints:
1357        if lb >= _lb - config.feasibility_tol and ub <= _ub + config.feasibility_tol:
1358            con.deactivate()
1359
1360    if _lb > lb:
1361        lb = _lb
1362    if _ub < ub:
1363        ub = _ub
1364    bnds_dict[con.body] = (lb, ub)
1365
1366    # Now, propagate bounds back from the root to the variables
1367    visitorB = _FBBTVisitorRootToLeaf(bnds_dict, integer_tol=config.integer_tol, feasibility_tol=config.feasibility_tol)
1368    visitorB.dfs_postorder_stack(con.body)
1369
1370    new_var_bounds = ComponentMap()
1371    for _node, _bnds in bnds_dict.items():
1372        if _node.__class__ in nonpyomo_leaf_types:
1373            continue
1374        if _node.is_variable_type():
1375            lb, ub = bnds_dict[_node]
1376            if lb == -interval.inf:
1377                lb = None
1378            if ub == interval.inf:
1379                ub = None
1380            new_var_bounds[_node] = (lb, ub)
1381    return new_var_bounds
1382
1383
1384def _fbbt_block(m, config):
1385    """
1386    Feasibility based bounds tightening (FBBT) for a block or model. This
1387    loops through all of the constraints in the block and performs
1388    FBBT on each constraint (see the docstring for _fbbt_con()).
1389    Through this processes, any variables whose bounds improve
1390    by more than tol are collected, and FBBT is
1391    performed again on all constraints involving those variables.
1392    This process is continued until no variable bounds are improved
1393    by more than tol.
1394
1395    Parameters
1396    ----------
1397    m: pyomo.core.base.block.Block or pyomo.core.base.PyomoModel.ConcreteModel
1398    config: ConfigBlock
1399        See the docs for fbbt
1400
1401    Returns
1402    -------
1403    new_var_bounds: ComponentMap
1404        A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed
1405        from FBBT.
1406    """
1407    new_var_bounds = ComponentMap()
1408    var_to_con_map = ComponentMap()
1409    var_lbs = ComponentMap()
1410    var_ubs = ComponentMap()
1411    n_cons = 0
1412    for c in m.component_data_objects(ctype=Constraint, active=True,
1413                                      descend_into=config.descend_into, sort=True):
1414        for v in identify_variables(c.body):
1415            if v not in var_to_con_map:
1416                var_to_con_map[v] = list()
1417            if v.lb is None:
1418                var_lbs[v] = -interval.inf
1419            else:
1420                var_lbs[v] = value(v.lb)
1421            if v.ub is None:
1422                var_ubs[v] = interval.inf
1423            else:
1424                var_ubs[v] = value(v.ub)
1425            var_to_con_map[v].append(c)
1426        n_cons += 1
1427
1428    for _v in m.component_data_objects(ctype=Var, active=True, descend_into=True, sort=True):
1429        if _v.is_fixed():
1430            _v.setlb(_v.value)
1431            _v.setub(_v.value)
1432            new_var_bounds[_v] = (_v.value, _v.value)
1433
1434    n_fbbt = 0
1435
1436    improved_vars = ComponentSet()
1437    for c in m.component_data_objects(ctype=Constraint, active=True,
1438                                      descend_into=config.descend_into, sort=True):
1439        _new_var_bounds = _fbbt_con(c, config)
1440        n_fbbt += 1
1441        new_var_bounds.update(_new_var_bounds)
1442        for v, bnds in _new_var_bounds.items():
1443            vlb, vub = bnds
1444            if vlb is not None:
1445                if vlb > var_lbs[v] + config.improvement_tol:
1446                    improved_vars.add(v)
1447                    var_lbs[v] = vlb
1448            if vub is not None:
1449                if vub < var_ubs[v] - config.improvement_tol:
1450                    improved_vars.add(v)
1451                    var_ubs[v] = vub
1452
1453    while len(improved_vars) > 0:
1454        if n_fbbt >= n_cons * config.max_iter:
1455            break
1456        v = improved_vars.pop()
1457        for c in var_to_con_map[v]:
1458            _new_var_bounds = _fbbt_con(c, config)
1459            n_fbbt += 1
1460            new_var_bounds.update(_new_var_bounds)
1461            for _v, bnds in _new_var_bounds.items():
1462                _vlb, _vub = bnds
1463                if _vlb is not None:
1464                    if _vlb > var_lbs[_v] + config.improvement_tol:
1465                        improved_vars.add(_v)
1466                        var_lbs[_v] = _vlb
1467                if _vub is not None:
1468                    if _vub < var_ubs[_v] - config.improvement_tol:
1469                        improved_vars.add(_v)
1470                        var_ubs[_v] = _vub
1471
1472    return new_var_bounds
1473
1474
1475def fbbt(comp, deactivate_satisfied_constraints=False, integer_tol=1e-5, feasibility_tol=1e-8, max_iter=10,
1476         improvement_tol=1e-4, descend_into=True):
1477    """
1478    Perform FBBT on a constraint, block, or model. For more control,
1479    use _fbbt_con and _fbbt_block. For detailed documentation, see
1480    the docstrings for _fbbt_con and _fbbt_block.
1481
1482    Parameters
1483    ----------
1484    comp: pyomo.core.base.constraint.Constraint or pyomo.core.base.block.Block or pyomo.core.base.PyomoModel.ConcreteModel
1485    deactivate_satisfied_constraints: bool
1486        If deactivate_satisfied_constraints is True and a constraint is always satisfied, then the constranit
1487        will be deactivated
1488    integer_tol: float
1489        If the lower bound computed on a binary variable is less than or equal to integer_tol, then the
1490        lower bound is left at 0. Otherwise, the lower bound is increased to 1. If the upper bound computed
1491        on a binary variable is greater than or equal to 1-integer_tol, then the upper bound is left at 1.
1492        Otherwise the upper bound is decreased to 0.
1493    feasibility_tol: float
1494        If the bounds computed on the body of a constraint violate the bounds of the constraint by more than
1495        feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance
1496        is also used when performing certain interval arithmetic operations to ensure that none of the feasible
1497        region is removed due to floating point arithmetic and to prevent math domain errors (a larger value
1498        is more conservative).
1499    max_iter: int
1500        Used for Blocks only (i.e., comp.ctype == Block). When performing FBBT on a Block, we first perform FBBT on
1501        every constraint in the Block. We then attempt to identify which constraints to repeat FBBT on based on the
1502        improvement in variable bounds. If the bounds on a variable improve by more than improvement_tol, then FBBT
1503        is performed on the constraints using that Var. However, this algorithm is not guaranteed to converge, so
1504        max_iter limits the total number of times FBBT is performed to max_iter times the number of constraints
1505        in the Block.
1506    improvement_tol: float
1507        Used for Blocks only (i.e., comp.ctype == Block). When performing FBBT on a Block, we first perform FBBT on
1508        every constraint in the Block. We then attempt to identify which constraints to repeat FBBT on based on the
1509        improvement in variable bounds. If the bounds on a variable improve by more than improvement_tol, then FBBT
1510        is performed on the constraints using that Var.
1511
1512    Returns
1513    -------
1514    new_var_bounds: ComponentMap
1515        A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed
1516        from FBBT.
1517    """
1518    config = ConfigBlock()
1519    dsc_config = ConfigValue(default=deactivate_satisfied_constraints, domain=In({True, False}))
1520    integer_tol_config = ConfigValue(default=integer_tol, domain=NonNegativeFloat)
1521    ft_config = ConfigValue(default=feasibility_tol, domain=NonNegativeFloat)
1522    mi_config = ConfigValue(default=max_iter, domain=NonNegativeInt)
1523    improvement_tol_config = ConfigValue(default=improvement_tol, domain=NonNegativeFloat)
1524    descend_into_config = ConfigValue(default=descend_into)
1525    config.declare('deactivate_satisfied_constraints', dsc_config)
1526    config.declare('integer_tol', integer_tol_config)
1527    config.declare('feasibility_tol', ft_config)
1528    config.declare('max_iter', mi_config)
1529    config.declare('improvement_tol', improvement_tol_config)
1530    config.declare('descend_into', descend_into_config)
1531
1532    new_var_bounds = ComponentMap()
1533    if comp.ctype == Constraint:
1534        if comp.is_indexed():
1535            for _c in comp.values():
1536                _new_var_bounds = _fbbt_con(comp, config)
1537                new_var_bounds.update(_new_var_bounds)
1538        else:
1539            _new_var_bounds = _fbbt_con(comp, config)
1540            new_var_bounds.update(_new_var_bounds)
1541    elif comp.ctype in {Block, Disjunct}:
1542        _new_var_bounds = _fbbt_block(comp, config)
1543        new_var_bounds.update(_new_var_bounds)
1544    else:
1545        raise FBBTException('Cannot perform FBBT on objects of type {0}'.format(type(comp)))
1546
1547    return new_var_bounds
1548
1549
1550def compute_bounds_on_expr(expr):
1551    """
1552    Compute bounds on an expression based on the bounds on the variables in the expression.
1553
1554    Parameters
1555    ----------
1556    expr: pyomo.core.expr.numeric_expr.ExpressionBase
1557
1558    Returns
1559    -------
1560    lb: float
1561    ub: float
1562    """
1563    bnds_dict = ComponentMap()
1564    visitor = _FBBTVisitorLeafToRoot(bnds_dict)
1565    visitor.dfs_postorder_stack(expr)
1566    lb, ub = bnds_dict[expr]
1567    if lb == -interval.inf:
1568        lb = None
1569    if ub == interval.inf:
1570        ub = None
1571
1572    return lb, ub
1573
1574
1575class BoundsManager(object):
1576    def __init__(self, comp):
1577        self._vars = ComponentSet()
1578        self._saved_bounds = list()
1579
1580        if comp.ctype == Constraint:
1581            if comp.is_indexed():
1582                for c in comp.values():
1583                    self._vars.update(identify_variables(c.body))
1584            else:
1585                self._vars.update(identify_variables(comp.body))
1586        else:
1587            for c in comp.component_data_objects(Constraint, descend_into=True, active=True, sort=True):
1588                self._vars.update(identify_variables(c.body))
1589
1590    def save_bounds(self):
1591        bnds = ComponentMap()
1592        for v in self._vars:
1593            bnds[v] = (v.lb, v.ub)
1594        self._saved_bounds.append(bnds)
1595
1596    def pop_bounds(self, ndx=-1):
1597        bnds = self._saved_bounds.pop(ndx)
1598        for v, _bnds in bnds.items():
1599            lb, ub = _bnds
1600            v.setlb(lb)
1601            v.setub(ub)
1602
1603    def load_bounds(self, bnds, save_current_bounds=True):
1604        if save_current_bounds:
1605            self.save_bounds()
1606        for v, _bnds in bnds.items():
1607            if v in self._vars:
1608                lb, ub = _bnds
1609                v.setlb(lb)
1610                v.setub(ub)
1611