# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2022
# --------------------------------------------------------------------------
from docplex.mp.utils import is_iterable
from docplex.mp.basic import ModelingObjectBase
from docplex.mp.utils import DOcplexException, is_number
from docplex.mp.sttck import StaticTypeChecker
import copy
[docs]class PwlFunction(ModelingObjectBase):
"""
This class models piecewise linear (PWL) functions. This class is not intended to be instantiated:
piecewise linear functions are defined by invoking :func:`docplex.mp.model.Model.piecewise`,
or :func:`docplex.mp.model.Model.piecewise_as_slopes`.
Piecewise-linear functions are important in many applications.
They are often specified either:
* by giving a set of slopes, a set of breakpoints at which the slopes change, and the value of the functions at
a given point, or
* by giving an ordered list of (x,y) points that are linearly connected, along with the slope before the first
point and the slope after the last point.
Note that a piecewise-linear function may be discontinuous.
"""
@staticmethod
def check_number(logger, arg, caller=None):
StaticTypeChecker.typecheck_num_nan_inf(logger, arg, caller)
@staticmethod
def check_list_pair_breaksxy(logger, arg):
if not is_iterable(arg):
logger.fatal("argument 'breaksxy' expects iterable, {0!r} was passed".format(arg))
if isinstance(arg, tuple):
# Encapsulate tuple argument into a list: this allows defining a PWL with a tuple if there is only
# one element in its definition
arg = [arg]
if len(arg) == 0:
logger.fatal("argument 'breaksxy' must be a non-empty list of (x, y) tuples.")
prev_pair = None
pprev_pair = None
for pair in arg:
if isinstance(pair, tuple):
if len(pair) != 2:
logger.fatal("invalid tuple in 'breaksxy': {0!s}. Each tuple must have 2 items.".format(pair))
PwlFunction.check_number(logger, pair[0])
PwlFunction.check_number(logger, pair[1])
else:
logger.fatal("invalid item in 'breaksxy': {0!s}. Each item must be a (x, y) tuple.".format(pair))
if prev_pair is not None:
if pair[0] < prev_pair[0]:
logger.fatal("X coordinate in: {0!s} cannot be smaller than previous break abscisse: {1!s}.".
format(pair, prev_pair))
if pprev_pair is not None and pair[0] == prev_pair[0] and prev_pair[0] == pprev_pair[0]:
logger.fatal(
"invalid break: {0!s}. There cannot be more than 2 consecutive breaks with same abscisse.".
format(pair))
pprev_pair = prev_pair
prev_pair = pair
@staticmethod
def check_number_pair(logger, arg):
if arg is None:
logger.fatal("argument 'anchor' must be defined")
if isinstance(arg, tuple):
if len(arg) != 2:
logger.fatal("invalid tuple for 'anchor': {0!s}. Anchor argument must have 2 items.".format(arg))
PwlFunction.check_number(logger, arg[0])
PwlFunction.check_number(logger, arg[1])
else:
logger.fatal("invalid value for 'anchor': {0!s}. Anchor argument must be a (x, y) tuple.".format(arg))
@staticmethod
def check_list_pair_slope_breakx(logger, arg, anchor):
if arg is None:
logger.fatal("argument 'slopebreaksx' must be defined")
if not is_iterable(arg):
logger.fatal("not an iterable: {0!s}".format(arg))
if len(arg) == 0:
return
if isinstance(arg, tuple):
# Encapsulate tuple argument into a list: this allows defining a PWL with a tuple if there is only
# one element in its definition
arg = [arg]
prev_pair = None
pprev_pair = None
for pair in arg:
if isinstance(pair, tuple):
if len(pair) != 2:
logger.fatal("invalid tuple in 'slopebreaksx': {0!s}. Each tuple must have 2 items.".
format(pair))
PwlFunction.check_number(logger, pair[0])
PwlFunction.check_number(logger, pair[1])
else:
logger.fatal("invalid item in 'slopebreaksx': {0!s}. Each item must be a (x, y) tuple.".format(pair))
if prev_pair is not None:
if pair[1] < prev_pair[1]:
logger.fatal("X coordinate in: {0!s} cannot be smaller than previous break abscisse: {1!s}.".
format(pair, prev_pair))
if pprev_pair is not None and pair[1] == prev_pair[1] and prev_pair[1] == pprev_pair[1]:
logger.fatal(
"invalid break: {0!s}. There cannot be more than 2 consecutive breaks with same abscisse.".
format(pair))
if pair[1] == prev_pair[1] and anchor[0] == pair[1]:
logger.fatal("anchor {0!s} cannot be defined at discontinuity point: {1!s}".
format(anchor, pair))
pprev_pair = prev_pair
prev_pair = pair
class _PwlAsBreaks:
"""
When using this class, the piecewise linear function is specified by:
- Breakpoints defined as a list of coordinate pairs `(x[i], y[i])` defining the segments of the PWL function.
- Before the first segment of the PWL function there is a half-line; its slope is specified by `preslope`.
- After the last segment of the the PWL function there is a half-line; its slope is specified by `postslope`.
Two consecutive breakpoints may have the same x-coordinate; in such cases there is a discontinuity in the
PWL function. Three consecutive breakpoints may not have the same x-coordinate.
"""
def __init__(self, preslope, breaksxy, postslope):
self._preslope = preslope
self._breaksxy = self._reformulate_breaksxy(breaksxy)
self._postslope = postslope
@property
def preslope(self):
return self._preslope
@property
def breaksxy(self):
return self._breaksxy
@property
def postslope(self):
return self._postslope
def deepcopy(self):
breaksxy_copy = copy.deepcopy(self.breaksxy)
return PwlFunction._PwlAsBreaks(self.preslope, breaksxy_copy, self.postslope)
@staticmethod
def _reformulate_breaksxy(breaksxy):
if isinstance(breaksxy, tuple):
return [] if len(breaksxy) == 0 else [breaksxy]
return breaksxy
@staticmethod
def _remove_useless_intermediate_breaks(preslope, breaksxy, postslope):
result_breaksxy = []
current_slope = preslope
prev_break = None
for br in breaksxy:
if prev_break is None:
pass
else:
if br[0] == prev_break[0]:
# Check discontinuity
if br[1] != prev_break[1]:
result_breaksxy.append(prev_break)
result_breaksxy.append(br)
current_slope = None
else:
slope = (br[1] - prev_break[1]) / (br[0] - prev_break[0])
if current_slope is not None and current_slope != slope:
# Add prev_break in list
result_breaksxy.append(prev_break)
current_slope = slope
prev_break = br
# Handle last break
if not result_breaksxy:
# Set result breaks = first break
result_breaksxy = breaksxy[0]
elif current_slope is not None and current_slope != postslope:
result_breaksxy.append(prev_break)
return preslope, result_breaksxy, postslope
def _get_break_at_index(self, index):
if len(self.breaksxy) <= index:
return None, None, index
break_1 = self.breaksxy[index]
if len(self.breaksxy) > (index + 1):
break_2 = self.breaksxy[index + 1]
if break_1[0] == break_2[0]:
# Discontinuity
return break_1, break_2, index + 1
return break_1, None, index
def _get_y_value(self, x_coord, prev_break_index=-1):
"""
:param x_coord:
:param prev_break_index: this parameter is mandatory if a breakxy tuple does exist before x_coord. Otherwise
an exception is raised.
:return:
"""
if prev_break_index < 0:
break_1, break_2, last_ind = self._get_break_at_index(0)
if break_1[0] < x_coord:
raise DOcplexException("Invalid arguments passed to PwlAsBreaks._get_y_value()")
if break_1[0] == x_coord:
y_coord_1 = break_1[1]
y_coord_2 = None if break_2 is None else break_2[1]
return y_coord_1, y_coord_2, last_ind
y_coord_1 = break_1[1] - self.preslope * (break_1[0] - x_coord)
return y_coord_1, None, -1
break_1, break_2, last_ind = self._get_break_at_index(prev_break_index)
next_break_1, next_break_2, next_last_ind = self._get_break_at_index(last_ind + 1)
if next_break_1 is None:
# x-coord is after last break
last_break = break_1 if break_2 is None else break_2
y_coord_1 = last_break[1] + self.postslope * (x_coord - last_break[0])
return y_coord_1, None, last_ind
else:
if x_coord == break_1[0]:
# Here, one must have: x_coord > break_1[0]
raise DOcplexException("Invalid arguments passed to PwlAsBreaks._get_y_value()")
if x_coord == next_break_1[0]:
y_coord_1 = next_break_1[1]
y_coord_2 = None if next_break_2 is None else next_break_2[1]
return y_coord_1, y_coord_2, next_last_ind
y_coord_prev = break_1[1] if break_2 is None else break_2[1]
y_coord_next = next_break_1[1]
slope = (y_coord_next - y_coord_prev) / (next_break_1[0] - break_1[0])
y_coord_1 = y_coord_prev + slope * (x_coord - break_1[0])
return y_coord_1, None, last_ind
def evaluate(self, x_val):
""" Evaluates the breaks-based PWL function at the point whose x-coordinate is `x_val`.
Args:
x_val: The x value for which we want to compute the value of the function.
Returns:
The value of the PWL function at point `x_val`
A DOcplexException exception is raised when evaluating at a discontinuity of the PWL function.
"""
prev_break_index, index = -1, 0
while index < len(self.breaksxy):
break_1, break_2, index = self._get_break_at_index(index)
if break_1 is None:
raise DOcplexException("Invalid PWL definition: no break point is defined")
if break_1[0] < x_val:
prev_break_index = index
else:
if break_1[0] == x_val and break_2 is not None:
raise DOcplexException("Cannot evaluate PWL at a discontinuity")
break
index += 1
y_val, _, _ = self._get_y_value(x_val, prev_break_index)
return y_val
def _get_all_breaks(self, all_x_coord):
all_breaks = []
prev_break_ind = -1
for x_coord in all_x_coord:
y_coord_1, y_coord_2, prev_break_ind = self._get_y_value(x_coord, prev_break_ind)
all_breaks.append((x_coord, y_coord_1) if y_coord_2 is None else
[(x_coord, y_coord_1), (x_coord, y_coord_2)])
return all_breaks
def get_nb_intervals(self):
nb_discontinuities = 0
prev_br = None
for br in iter(self.breaksxy):
if prev_br is not None and prev_br[0] == br[0]:
nb_discontinuities += 1
prev_br = br
return len(self.breaksxy) - nb_discontinuities - 1
def __add__(self, arg):
if isinstance(arg, PwlFunction._PwlAsBreaks):
all_x_coord = sorted({br[0] for br in self.breaksxy + arg.breaksxy})
all_breaks_left = self._get_all_breaks(all_x_coord)
all_breaks_right = arg._get_all_breaks(all_x_coord)
result_breaksxy = []
# Both lists have same size, with same x-coord for breaks ==> perform the addition on each break
for br_l, br_r in zip(all_breaks_left, all_breaks_right):
if isinstance(br_l, tuple) and isinstance(br_r, tuple):
result_breaksxy.append((br_l[0], br_l[1] + br_r[1]))
else:
if isinstance(br_l, tuple):
# br_r is a list containing 2 tuple pairs
result_breaksxy.append((br_l[0], br_l[1] + br_r[0][1]))
result_breaksxy.append((br_l[0], br_l[1] + br_r[1][1]))
elif isinstance(br_r, tuple):
# br_l is a list containing 2 tuple pairs
result_breaksxy.append((br_r[0], br_l[0][1] + br_r[1]))
result_breaksxy.append((br_r[0], br_l[1][1] + br_r[1]))
else:
# br_l and br_r are two lists, each containing 2 tuple pairs
result_breaksxy.append((br_l[0][0], br_l[0][1] + br_r[0][1]))
result_breaksxy.append((br_l[0][0], br_l[1][1] + br_r[1][1]))
result_preslope = self.preslope + arg.preslope
result_postslope = self.postslope + arg.postslope
return PwlFunction._PwlAsBreaks(*self._remove_useless_intermediate_breaks(
result_preslope, result_breaksxy, result_postslope))
elif is_number(arg):
return PwlFunction._PwlAsBreaks(
self.preslope, [(br[0], br[1] + arg) for br in self.breaksxy], self.postslope)
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
def __sub__(self, arg):
if isinstance(arg, PwlFunction._PwlAsBreaks):
return self + arg * (-1)
elif is_number(arg):
return PwlFunction._PwlAsBreaks(
self.preslope, [(br[0], br[1] - arg) for br in self.breaksxy], self.postslope)
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
def __mul__(self, arg):
if is_number(arg):
return PwlFunction._PwlAsBreaks(*self._remove_useless_intermediate_breaks(
self.preslope * arg, [(br[0], br[1] * arg) for br in self.breaksxy], self.postslope * arg))
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
def translate(self, arg):
if is_number(arg):
return PwlFunction._PwlAsBreaks(
self.preslope, [(br[0] + arg, br[1]) for br in self.breaksxy], self.postslope)
else:
raise DOcplexException("Invalid type for argument: {0!s}.".format(arg))
def __str__(self):
return self.to_string()
def to_string(self):
return '({0}, {1}, {2})'.format(self.preslope, self.breaksxy, self.postslope)
def repr_string(self):
return 'preslope={0},breaksxy={1},postslope={2}'.format(self.preslope, self.breaksxy, self.postslope)
class _PwlAsSlopes:
"""
When using this class, the piecewise linear function is specified by:
- a list of tuple pairs `(slope[i], breakx[i])` of slopes and x-coordinates defining the slope of the piecewise
function between the previous breakpoint (or minus infinity if there is none) and the breakpoint with
x-coordinate `breakx[i]`,
- the slope after the last specified breakpoint, and
- the coordinates of the 'anchor point'. The purpose of the anchor point is to ground the piecewise-linear
function specified by the list of slopes and breakpoints.
Note that:
- The `breakx[i]` values must be increasing. If two consecutive `breakx` values have the same value, a
discontinuity is defined and the value associated with the second argument is considered to be a "step".
- The list of tuple pairs `(slope[i], breakx[i])` may be empty.
- The default value for the anchor point is the origin (point with coordinates (0, 0)).
- If the piecewise linear function defines some discontinuities, the anchor must not reside at one of
these discontinuities, since the function would not be uniquely defined.
"""
def __init__(self, slopebreaksx, lastslope, anchor=(0, 0)):
self._slopebreaksx = self._reformulate_slopebreaksx(slopebreaksx)
self._lastslope = lastslope
self._anchor = anchor
@property
def slopebreaksx(self):
return self._slopebreaksx
@property
def lastslope(self):
return self._lastslope
@property
def anchor(self):
return self._anchor
def deepcopy(self):
slopebreaksx_copy = copy.deepcopy(self.slopebreaksx)
anchor_copy = copy.deepcopy(self.anchor)
return PwlFunction._PwlAsSlopes(slopebreaksx_copy, self.lastslope, anchor_copy)
@staticmethod
def _reformulate_slopebreaksx(slopebreaksx):
if isinstance(slopebreaksx, tuple):
return [] if len(slopebreaksx) == 0 else [slopebreaksx]
return slopebreaksx
@staticmethod
def _compute_breaksxy_after(slope_breaks, anchor):
breaks_xy = []
start_x, start_y = anchor[0], anchor[1]
for (slope, break_x) in slope_breaks:
delta_x = break_x - start_x
start_x = break_x
if delta_x > 0:
start_y = start_y + slope * delta_x
else:
# Discontinuity: slope is considered to be a "step"
start_y += slope
breaks_xy.append((start_x, start_y))
return breaks_xy
@staticmethod
def _compute_breaksxy_before(start_slope, slope_breaks, anchor):
breaks_xy = []
start_x, start_y = anchor[0], anchor[1]
last_slope = start_slope
for (slope, break_x) in slope_breaks:
delta_x = break_x - start_x
start_x = break_x
if delta_x < 0 or anchor[0] == break_x:
start_y = start_y + last_slope * delta_x
else:
# Discontinuity: slope is considered to be a "step"
start_y -= last_slope
last_slope = slope
breaks_xy.append((start_x, start_y))
return breaks_xy, last_slope
def convert_to_pwl_as_breaks(self):
breaks_before = [(s, b) for (s, b) in self.slopebreaksx if b <= self.anchor[0]]
breaks_after = [(s, b) for (s, b) in self.slopebreaksx if b > self.anchor[0]]
# Compute y value at each break point
anchor_slope = breaks_after[0][0] if len(breaks_after) > 0 else self.lastslope
breaks_before.reverse()
breaks_xy_before, preslope = self._compute_breaksxy_before(anchor_slope, breaks_before, self.anchor)
breaks_xy_before.reverse()
breaks_xy_after = self._compute_breaksxy_after(breaks_after, self.anchor)
# Now, we can build the PWL as breaks
breaksxy = breaks_xy_before + breaks_xy_after
if len(breaksxy) > 0:
return PwlFunction._PwlAsBreaks(preslope, breaksxy, self.lastslope)
else:
# No breakpoint is defined
return PwlFunction._PwlAsBreaks(self.lastslope, [self.anchor], self.lastslope)
def _get_safe_xy_anchor(self):
"""
Return an anchor point that is on or after (if last break corresponds to a discontinuity) the largest
x-coord corresponding to a break or the anchor.
:return:
"""
breaks_after = [(s, b) for (s, b) in self.slopebreaksx if b > self.anchor[0]]
breaks_xy_after = self._compute_breaksxy_after(breaks_after, self.anchor)
if len(breaks_xy_after) > 0:
# Check if last break corresponds to a discontinuity
if len(breaks_xy_after) > 1 and breaks_xy_after[-2][0] == breaks_xy_after[-1][0]:
# Returns point with x-coord = last_x_coord + 1
return breaks_xy_after[-1][0] + 1, breaks_xy_after[-1][1] + self.lastslope
return breaks_xy_after[-1]
return self.anchor
@staticmethod
def _remove_useless_intermediate_slopes(slopebreaksx, lastslope, anchor):
result_slopebreaksx = []
prev_sbr = None
for sbr in slopebreaksx:
if prev_sbr is not None:
if sbr[0] != prev_sbr[0]:
result_slopebreaksx.append(prev_sbr)
prev_sbr = sbr
if prev_sbr is not None and prev_sbr[0] != lastslope:
result_slopebreaksx.append(prev_sbr)
return result_slopebreaksx, lastslope, anchor
def _get_all_slopebreaks(self, all_x_coord):
all_slopebreaks = []
iter_slopebreakx = iter(self.slopebreaksx)
current_slopebreakx = next(iter_slopebreakx, None)
for x_coord in all_x_coord:
if current_slopebreakx is None:
all_slopebreaks.append((self.lastslope, x_coord))
else:
while current_slopebreakx is not None and x_coord > current_slopebreakx[1]:
prev_slopebreakx = current_slopebreakx
current_slopebreakx = next(iter_slopebreakx, None)
if current_slopebreakx is not None and current_slopebreakx[1] == prev_slopebreakx[1]:
# Case of a discontinuity ==> update last item in result list to a list containing 2 tuples
all_slopebreaks[-1] = [(prev_slopebreakx[0], prev_slopebreakx[1]),
(current_slopebreakx[0], current_slopebreakx[1])]
if current_slopebreakx is None:
all_slopebreaks.append((self.lastslope, x_coord))
else:
all_slopebreaks.append((current_slopebreakx[0], x_coord))
# Handle case where last break is a discontinuity
prev_slopebreakx = current_slopebreakx
current_slopebreakx = next(iter_slopebreakx, None)
if current_slopebreakx is not None:
# Case of a discontinuity ==> update last item in result list to a list containing 2 tuples
all_slopebreaks[-1] = [(prev_slopebreakx[0], prev_slopebreakx[1]),
(current_slopebreakx[0], current_slopebreakx[1])]
return all_slopebreaks
def __add__(self, arg):
if isinstance(arg, PwlFunction._PwlAsSlopes):
all_x_coord = sorted({sbr[1] for sbr in self.slopebreaksx + arg.slopebreaksx})
all_slopebreaks_left = self._get_all_slopebreaks(all_x_coord)
all_slopebreaks_right = arg._get_all_slopebreaks(all_x_coord)
result_slopebreaksxy = []
# Both lists have same size, with same x-coord for slopebreaks
# ==> perform the addition of slopes on each break
for sbr_l, sbr_r in zip(all_slopebreaks_left, all_slopebreaks_right):
if isinstance(sbr_l, tuple) and isinstance(sbr_r, tuple):
result_slopebreaksxy.append((sbr_l[0] + sbr_r[0], sbr_l[1]))
else:
if isinstance(sbr_l, tuple):
# sbr_r is a list containing 2 tuple pairs
result_slopebreaksxy.append((sbr_l[0] + sbr_r[0][0], sbr_l[1]))
result_slopebreaksxy.append((sbr_r[1][0], sbr_l[1]))
elif isinstance(sbr_r, tuple):
# sbr_l is a list containing 2 tuple pairs
result_slopebreaksxy.append((sbr_l[0][0] + sbr_r[0], sbr_r[1]))
result_slopebreaksxy.append((sbr_l[1][0], sbr_r[1]))
else:
# sbr_l and sbr_r are two lists, each containing 2 tuple pairs
result_slopebreaksxy.append((sbr_l[0][0] + sbr_r[0][0], sbr_l[0][1]))
result_slopebreaksxy.append((sbr_l[1][0] + sbr_r[1][0], sbr_l[0][1]))
result_lastslope = self.lastslope + arg.lastslope
if self.anchor[0] == arg.anchor[0]:
result_anchor = (self.anchor[0], self.anchor[1] + arg.anchor[1])
else:
# Compute a new anchor based on the last x-coord in the slopebreakx list + anchor point
anchor_l = self._get_safe_xy_anchor()
anchor_r = arg._get_safe_xy_anchor()
delta = anchor_r[0] - anchor_l[0]
if anchor_l[0] < anchor_r[0]:
result_anchor = (anchor_r[0], anchor_l[1] + anchor_r[1] + delta * self.lastslope)
else:
result_anchor = (anchor_l[0], anchor_l[1] + anchor_r[1] - delta * arg.lastslope)
return PwlFunction._PwlAsSlopes(*self._remove_useless_intermediate_slopes(
result_slopebreaksxy, result_lastslope, result_anchor))
elif is_number(arg):
return PwlFunction._PwlAsSlopes(copy.deepcopy(self.slopebreaksx),
self.lastslope, (self.anchor[0], self.anchor[1] + arg))
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
def __sub__(self, arg):
if isinstance(arg, PwlFunction._PwlAsSlopes):
return self + arg * (-1)
elif is_number(arg):
return PwlFunction._PwlAsSlopes(copy.deepcopy(self.slopebreaksx),
self.lastslope, (self.anchor[0], self.anchor[1] - arg))
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
def __mul__(self, arg):
if is_number(arg):
return PwlFunction._PwlAsSlopes(*self._remove_useless_intermediate_slopes(
[(br[0] * arg, br[1]) for br in self.slopebreaksx],
self.lastslope * arg, (self.anchor[0], self.anchor[1] * arg)))
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
def translate(self, arg):
if is_number(arg):
return PwlFunction._PwlAsSlopes(
[(br[0], br[1] + arg) for br in self.slopebreaksx], self.lastslope,
(self.anchor[0] + arg, self.anchor[1]))
else:
raise DOcplexException("Invalid type for argument: {0!s}.".format(arg))
def __str__(self):
return self.to_string()
def to_string(self):
return '{' + ''.join(
repr(slope) + ' -> ' + repr(break_x) + ';' for (slope, break_x) in self._slopebreaksx) + \
repr(self._lastslope) + '}(' + repr(self.anchor[0]) + ', ' + repr(self.anchor[1]) + ')'
# _name_generator = _AutomaticSymbolGenerator(pattern="pwl", offset=1)
def __init__(self, model, pwl_def, name=None):
ModelingObjectBase.__init__(self, model, name=name)
self._pwl_def = pwl_def
self._pwl_def_as_breaks = None
self._set_pwl_definition(pwl_def)
def _set_pwl_definition(self, pwl_def):
# INTERNAL
if isinstance(pwl_def, PwlFunction._PwlAsBreaks):
# Use the same data structure as input for internal representation (do not duplicate)
self._pwl_def_as_breaks = pwl_def
elif isinstance(pwl_def, PwlFunction._PwlAsSlopes):
pwl_def_as_breaks = pwl_def.convert_to_pwl_as_breaks()
self._set_pwl_as_breaks(pwl_def_as_breaks.preslope, pwl_def_as_breaks.breaksxy,
pwl_def_as_breaks.postslope)
else:
self.model._checker.fatal("Invalid definition for Piecewise Linear Function: {0!s}.".format(pwl_def))
def _set_pwl_as_breaks(self, preslope, breaksxy=None, postslope=None):
"""Internal format to represent a piecewise linear function is based on the Cplex representation"""
self._pwl_def_as_breaks = self._PwlAsBreaks(preslope, breaksxy, postslope)
def copy(self, target_model, _):
pwl_def_copy = self.pwl_def.deepcopy()
return target_model._piecewise(pwl_def_copy, self.name)
@property
def pwl_def(self):
return self._pwl_def
@property
def pwl_def_as_breaks(self):
return self._pwl_def_as_breaks
# __call__ builds an expression equal to the piecewise linear value of its argument, based
# on the definition of the PWL function.
#
# Args:
# e: Accepts any object that can be transformed into an expression:
# decision variables, expressions, or numbers.
#
# Returns:
# An expression that can be used in arithmetic operators and constraints.
#
# Note:
# Building the expression generates one auxiliary decision variable.
def __call__(self, e):
self.model._checker.typecheck_operand(e, caller="Model.pwl", accept_numbers=True)
return self.model._add_pwl_expr(self, e)
def __hash__(self):
return id(self)
def __str__(self):
return self.pwl_def.__str__()
def __repr__(self):
return 'docplex.mp.pwl.PwlFunction({0})'.format(self.pwl_def_as_breaks.repr_string())
[docs] def clone(self):
""" Creates a copy of the PWL function on the same model.
Returns:
The copy of the PWL function.
"""
return self.copy(self.model, None)
def __add__(self, e):
return self.plus(e)
def __radd__(self, e):
return self.plus(e)
def __iadd__(self, e):
self.model.fatal('Cannot modify a PWL function')
# self.add(e)
# return self
def plus(self, e):
cloned = self.clone()
return cloned.add(e)
[docs] def add(self, arg):
""" Adds an expression to self.
Note:
This method does not create a new PWL function but modifies the `self` instance.
Args:
arg: The expression to be added. Can be a PWL function or a number.
Returns:
The modified self.
"""
if isinstance(arg, PwlFunction):
if (isinstance(self.pwl_def, PwlFunction._PwlAsBreaks) and
isinstance(arg.pwl_def, PwlFunction._PwlAsBreaks)) or \
(isinstance(self.pwl_def, PwlFunction._PwlAsSlopes) and
isinstance(arg.pwl_def, PwlFunction._PwlAsSlopes)):
self._pwl_def = self.pwl_def + arg.pwl_def
self._set_pwl_definition(self._pwl_def)
else:
# Use Breaks representation
self._pwl_def = self.pwl_def_as_breaks + arg.pwl_def_as_breaks
self._set_pwl_definition(self._pwl_def)
elif is_number(arg):
self._pwl_def = self.pwl_def + arg
self._set_pwl_definition(self._pwl_def)
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
return self
def __sub__(self, e):
return self.minus(e)
def __rsub__(self, e):
return self * (-1) + e
def __isub__(self, e):
self.model.fatal('Cannot modify a PWL function')
def minus(self, e):
cloned = self.clone()
return cloned.subtract(e)
[docs] def subtract(self, arg):
""" Subtracts an expression from this PWL function.
Note:
This method does not create a new function but modifies the `self` instance.
Args:
arg: The expression to be subtracted. Can be either a PWL function, or a number.
Returns:
The modified self.
"""
if isinstance(arg, PwlFunction):
if (isinstance(self.pwl_def, PwlFunction._PwlAsBreaks) and
isinstance(arg.pwl_def, PwlFunction._PwlAsBreaks)) or \
(isinstance(self.pwl_def, PwlFunction._PwlAsSlopes) and
isinstance(arg.pwl_def, PwlFunction._PwlAsSlopes)):
self._pwl_def = self.pwl_def - arg.pwl_def
self._set_pwl_definition(self._pwl_def)
else:
# Use Breaks representation
self._pwl_def = self.pwl_def_as_breaks - arg.pwl_def_as_breaks
self._set_pwl_definition(self._pwl_def)
elif is_number(arg):
self._pwl_def = self.pwl_def - arg
self._set_pwl_definition(self._pwl_def)
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
return self
def __mul__(self, e):
return self.times(e)
def __rmul__(self, e):
return self.times(e)
def __imul__(self, e):
self.model.fatal('Cannot modify a PWL function')
# return self.multiply(e)
def __div__(self, e):
return self.quotient(e)
def __truediv__(self, e):
# for py3
# INTERNAL
return self.quotient(e) # pragma: no cover
def __itruediv__(self, e):
# for py3
# INTERNAL
self.model.fatal('Cannot modify a PWL function') # pragma: no cover
def __idiv__(self, other):
self.model.fatal('Cannot modify a PWL function')
def __rtruediv__(self, e):
# for py3
self.fatal("PWL function {0!s} cannot be used as denominator of {1!s}", self, e) # pragma: no cover
def __rdiv__(self, e):
self.fatal("PWL function {0!s} cannot be used as denominator of {1!s}", self, e)
def quotient(self, e):
cloned = self.clone()
cloned.divide(e)
return cloned
[docs] def divide(self, arg):
""" Divides this PWL function by a number.
Note:
This method does not create a new function but modifies the `self` instance.
Args:
arg: The number that is used to divide `self`.
Returns:
The modified `self`.
"""
self.model._typecheck_as_denominator(arg, numerator=self)
inverse = 1.0 / float(arg)
return self.multiply(inverse)
def times(self, e):
cloned = self.clone()
return cloned.multiply(e)
[docs] def multiply(self, arg):
""" Multiplies this PWL function by a number.
Note:
This method does not create a new function but modifies the `self` instance.
Args:
arg: The number that is used to multiply `self`.
Returns:
The modified `self`.
"""
if is_number(arg):
self._pwl_def = self.pwl_def * arg
self._set_pwl_definition(self._pwl_def)
else:
raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg))
return self
[docs] def translate(self, arg):
""" Translate this PWL function by a number.
This method creates a new PWL function instance for which all breakpoints have been moved
along the horizontal axis by the amount specified by `arg`.
Args:
arg: The number that is used to translate all breakpoints.
Returns:
The translated PWL function.
"""
if is_number(arg):
return PwlFunction(self.model, self.pwl_def.translate(arg))
else:
raise DOcplexException("Invalid type for argument: {0!s}.".format(arg))
[docs] def evaluate(self, x_val):
""" Evaluates the PWL function at the point whose x-coordinate is `x_val`.
Args:
x_val: The x value for which we want to compute the value of the function.
Returns:
The value of the PWL function at point `x_val`.
A DOcplexException exception is raised when evaluating at a discontinuity of the PWL function.
"""
return self._pwl_def_as_breaks.evaluate(x_val)
[docs] def plot(self, lx=None, rx=None, k=1, **kwargs): # pragma: no cover
"""
This method displays the piecewise linear function using the matplotlib package, if found.
:param lx: The value to show the `preslope` (must be before the first breakpoint x value).
:param rx: The value to show the `postslope` (must be after the last breakpoint x value).
:param k: Scaling factor to calculate default values for `rx` and/or `lx` if these arguments are not provided,
based on mean interval length between the `x` values of breakpoints.
:param kwargs: additional arguments to be passed to matplotlib plot() function
"""
try:
import matplotlib.pyplot as plt
except ImportError:
raise DOcplexException('matplotlib is required for plot()')
bks = self.pwl_def_as_breaks.breaksxy
xs = [bk[0] for bk in bks]
ys = [bk[1] for bk in bks]
# compute mean delta_x
first_x = xs[0]
last_x = xs[-1]
nb_intervals = self._pwl_def_as_breaks.get_nb_intervals()
# k times the mean interval length is used for left/right extra points
kdx_m = k * (last_x - first_x) / float(nb_intervals) if nb_intervals > 0 else 1
if lx is None:
lx = first_x - kdx_m
ly = ys[0] - self.pwl_def_as_breaks.preslope * (first_x - lx)
xs.insert(0, lx)
ys.insert(0, ly)
if rx is None or rx <= last_x:
rx = last_x + kdx_m
ry = ys[-1] + self.pwl_def_as_breaks.postslope * (rx - last_x)
xs.append(rx)
ys.append(ry)
if plt:
plt.plot(xs, ys, **kwargs)
if self.name:
plt.title('pwl: {0}'.format(self.name))
plt.show()