Source code for dypy.variables

import re
import logging

import six

import numpy

VARIABLE_ID_VALIDATION_PATTERN = re.compile('[^a-zA-Z0-9_]|_')
VARIABLE_NUMERAL_BEGINNING_PATTERN = re.compile('^[0-9]')

log = logging.getLogger("dypy.variables")


[docs]class AbstractVariable(object): def __init__(self, name, variable_id=None, minimum=None, maximum=None, step_size=None, values=None, *args, **kwargs): self.name = name self.variable_id = check_variable_id(name=name, variable_id=variable_id) self._min = minimum self._max = maximum self._step_size = step_size self._options = values if values is not None: self._user_set_options = True # keep track so we can zero it out later if they set min/max/stepsize params else: self._user_set_options = False # we have all of these simple things as @property methods instead of simple attributes so we can # make sure to have the correct behaviors if users set the options themselves @property def minimum(self): return self._min @property def maximum(self): return self._max @property def step_size(self): return self._step_size @minimum.setter def minimum(self, value): self._min = value self._reset_options() @maximum.setter def maximum(self, value): self._max = value self._reset_options() @step_size.setter def step_size(self, value): self._step_size = value self._reset_options() def _reset_options(self): if not self._user_set_options: self._options = None # if we change any of the params, clear the options @property def values(self): if self._options is not None: # if they gave us options or we already made them return self._options elif self._min is not None and self._max is not None and self._step_size is not None: if type(self._min) == "int" and type(self._max) == "int" and type(self.step_size) == "int": # if they're all integers we'll use range self._options = range(self._min, self._max, self.step_size) # cache it so next time we don't have to calculate else: # the `num` param here just transforms step_size to its equivalent number of steps for linspace. Add 1 to capture accurate spacing with both start and endpoints self._options = numpy.linspace(start=self._min, stop=self._max, num=int((self._max-self._min)/self.step_size)+1, endpoint=True) self._user_set_options = False return self._options raise ValueError("Can't get DecisionVariable options - need either explicit options (.options) or a minimum value, a maximum value, and a step size") @values.setter def values(self, value): self._options = value self._user_set_options = True
[docs]class StateVariable(AbstractVariable): """ :param name: :param values: :param initial_state: the initial value of this state variable at stage 0 - used when getting the ultimate solution - if not provided or None, then any state can be usd in the first stage, which is often not desired. :param availability_function: A numpy function indicating which states in the next stage are valid selections given the value in the current stage plus decisions. Should be: - numpy.equal (default) - only values that match the current state of this variable are available selections - numpy.not_equal - only values *not* matching the current state are valid - numpy.greater - only state values greater than the current state are available selections - numpy.greater_equal - same as above, but greater than or equal - numpy.less - only state values less than the current state are available selections - numpy.less_equal - same as above, but less than or equal Any function that takes a 2D numpy array as parameter one and the state value as parameter 2 and returns a new 2D array is valid. :param variable_id: will be used as the kwarg name when passing the value of the state into the objective function. If not provided, is generated from name by removing nonalphanumeric or underscore characters, lowercasing, and removing numbers from the beginning. If it is provided, it is still validated into a Python kwarg by removing leading numbers and removing non-alphanumeric/underscore characters, while leaving any capitalization intact """ def __init__(self, *args, **kwargs): self.column_index = None # this will be set by the calling DP - it indicates what column in the table has this information if 'initial_state' in kwargs: self.initial_state = kwargs['initial_state'] else: self.initial_state = None if 'availability_function' in kwargs: self.availability_function = kwargs['availability_function'] else: self.availability_function = numpy.equal self.current_state = self.initial_state if six.PY3: super().__init__(*args, **kwargs) elif six.PY2: super(StateVariable, self).__init__(*args, **kwargs)
[docs]class DecisionVariable(AbstractVariable): """ We'll use this to manage the decision variable - we'll need columns for each potential value here :param name: :param related_state: the StateVariable object that this DecisionVariable directly feeds back on :param variable_id: will be used as the kwarg name when passing the value of the state into the objective function. If not provided, is generated from name by removing nonalphanumeric or underscore characters, lowercasing, and removing numbers from the beginning. If it is provided, it is still validated into a Python kwarg by removing leading numbers and removing non-alphanumeric/underscore characters, while leaving any capitalization intact """ def __init__(self, *args, **kwargs): if 'related_state' in kwargs: self.related_state = kwargs['related_state'] else: self.related_state = None self.constraints = {} if six.PY3: super().__init__(*args, **kwargs) elif six.PY2: super(DecisionVariable, self).__init__(*args, **kwargs)
[docs] def add_constraint(self, stage, value): """ Will be used to add constraints on how much or little of the decision variable is chosen in each stage. Not yet implemented Want to figure out a way here to store also whether this constraint is a minimum or a maximum value constraint. Need to think how we'd handle that behavior :param stage: :param value: :return: """ raise NotImplementedError("Decision constraints aren't yet implemented. Sorry!")
[docs]def check_variable_id(name, variable_id): """ Given a full variable name and a current variable_id returns the keyword argument name for the variable. Designed for private within-package usage, but public for inspection and overriding. :param name: Full name of a variable :param variable_id: the current variable ID :return: sanitized new variable_id, suitable for usage in a Python keyword argument """ if variable_id is None: variable_id = name.lower() # replace non-alphanumeric values with _ variable_id = re.sub(VARIABLE_ID_VALIDATION_PATTERN, '_', variable_id) # strip numbers off the beginning variable_id = re.sub(VARIABLE_NUMERAL_BEGINNING_PATTERN, '', variable_id) return variable_id