Source code for hynet.scenario.cost

"""
Representation of (injector) cost functions in *hynet*.
"""

import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import interp1d

from hynet.types_ import hynet_float_


[docs]class PWLFunction: """ Representation of a piecewise linear function ``f: R -> R``. Parameters ---------- samples : (numpy.ndarray[.hynet_float_], numpy.ndarray[.hynet_float_]), optional Tuple ``(x, y)`` of ``x``- and ``y``-coordinates of sample points of the piecewise linear function, i.e., ``(x[0], y[0]), ..., (x[N], y[N])``. """ def __init__(self, samples=None, marginal_price=None): """ Create the PWL function. Either the sample points (``samples``) or the marginal price (``marginal_price``) may be specified to initialize the function. Please refer to the respective properties for more information. By default, the object is set to a linear function with slope zero. """ self._x = np.array([0, 1], dtype=hynet_float_) self._y = np.array([0, 0], dtype=hynet_float_) if samples is not None and marginal_price is not None: raise ValueError("Only samples or the marginal price may be specified.") elif samples is not None: self.samples = samples elif marginal_price is not None: self.marginal_price = marginal_price else: self.marginal_price = 0 @property def samples(self): """ Return the tuple ``(x, y)`` of ``x``- and ``y``-coordinate arrays. """ return self._x, self._y @samples.setter def samples(self, value): """ Set the tuple ``(x, y)`` of ``x``- and ``y``-coordinate arrays. """ if len(value) != 2: raise ValueError("Expecting a tuple (x, y) of coordinates.") x = np.array(value[0], dtype=hynet_float_) y = np.array(value[1], dtype=hynet_float_) if len(x) < 2 or len(x) != len(y) or x.ndim != 1 or y.ndim != 1: raise ValueError("Expecting a tuple (x, y) of coordinates " "comprising at least two points.") if any(np.diff(x) <= 0): raise ValueError("The x-coordinate of samples must be " "strictly increasing.") (self._x, self._y) = (x, y) @property def marginal_price(self): """ Return the slope if the function is linear or ``numpy.nan`` otherwise. **Remark:** This function requires that the linear function is specified using two sample points, i.e., the function will return ``numpy.nan`` even if three or more samples lie on a line. The latter case should be avoided anyway, as it introduces unnecessary constraints in the epigraph representation of the function. """ if len(self._x) != 2: return np.nan return (np.diff(self._y) / np.diff(self._x)).item() @marginal_price.setter def marginal_price(self, value): """ Set to this object to a linear function with the slope ``value``. """ self.samples = ((0, 1), (0, value)) def __repr__(self): marginal_price = self.marginal_price if not np.isnan(marginal_price): return str(marginal_price) if len(self._x) == 2: return str((np.diff(self._y) / np.diff(self._x)).item()) return '-'.join(['({0:.1f},{1:.1f})'.format(x, y) for x, y in zip(self._x, self._y)]) def __eq__(self, other): """Return True if the cost functions feature the same sample points.""" if other is None: return False return (np.array_equal(self._x, other.samples[0]) and np.array_equal(self._y, other.samples[1]))
[docs] def evaluate(self, x): """ Evaluate the function at ``x``, i.e., return ``y = f(x)``. """ result = interp1d(self._x, self._y, kind='linear', fill_value='extrapolate')(x) if np.isscalar(x): return result.item() return result
[docs] def show(self): """ Show the piecewise linear function. """ fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.plot(self._x, self._y, color='xkcd:sea blue', linestyle='-', linewidth=1) ax.set_xlabel('x') ax.set_ylabel('y') fig.tight_layout() fig.show() return fig
[docs] def scale(self, scaling): """ Scale the function by ``scaling``, i.e., ``f(x) -> scaling * f(x)``. """ if scaling < 0: raise ValueError("Expecting a nonnegative scaling factor.") self._y *= scaling return self
[docs] def is_convex(self): """ Return ``True`` if the function is convex. """ if len(self._x) == 2: # Affine function return True # Convex if curvature is nonnegative return all(np.diff(np.diff(self._y) / np.diff(self._x)) >= 0)
[docs] def get_epigraph_polyhedron(self): """ Return ``(A, b)`` such that ``f(x) = min{z in R: z*1 >= A*x + b}``. Note that ``f`` must be **convex**. """ A = np.diff(self._y) / np.diff(self._x) # Slope b = self._y[1:] - np.multiply(A, self._x[1:]) # Offset return A, b