def __init__(self, nseed=2147483647, heir=None): """ Initiates the random stream using the input seed 'nseed' and Python's __init__ constructor method. Unless... ...the input seed 'nseed' happens to be a list or tuple of numbers in [0.0, 1.0], in which case this external feed will be used as the basis of all random variate generation for the instance and will be used in place of consecutively sampled numbers from Python's built-in "random" method! """ if isinstance(nseed, int): assert is_posinteger(nseed), \ "The seed (if not a feed) must be a positive integer in ABCRand!" rstream = Random(nseed) self._feed = False self.runif01 = rstream.random if heir != "InverseRandomStream": self.randrange = rstream.randrange self.randint = rstream.randint self.vonmisesvariate = rstream.vonmisesvariate # Random.paretovariate and Random.weibullvariate # are used by methods in GeneralRandomStream self._paretovariate = rstream.paretovariate self._weibullvariate = rstream.weibullvariate else: # nseed is a list or tuple # Check to see beforehand that no numbers # from the feed is outside [0.0, 1.0] for x in nseed: assert 0.0 <= x <= 1.0, \ "number from feed is outside of [0.0, 1.0] in ABCRand!" self._feed = Stack(nseed) # Creates a Stack object self.runif01 = self.__rfeed01
def __init__(self, eventlist=[], timelist=[], sort=False): """ Creates two new stacks: one for the events and one for the corresponding time points. The events could for instance be described by strings. The times are (of course) floating-point numbers. The two stacks can be filled here but they have to be synchronized and in temporal order. """ assert len(timelist) == len(eventlist), \ "input lists are of unequal length in EventScheduleStack!" # If sort: # to be added later self.__eventstack = Stack(eventlist) self.__timestack = Stack(timelist)
def flattened(matrix, stack=False): """ OUTPUT WILL N O T BE ON Matrix OBJECT FORMAT!!! Places all elements in one single l i s t, row-by-row. NB. flattened returns a simple Python list (or a Stack if so desired), not a single-row 'Matrix' matrix! """ nrows, ncols = sized(matrix, 'flattened') sequence = [] for k in range(0, nrows): sequence.extend(list(matrix[k])) if stack: sequence = Stack(sequence) return sequence
def __init__(self, line, nserv): """ line is 'Line' or 'LineStack' nserv is the initial number of servers """ # Attributes made available from the outside (not assignable, though): # -------------------------------------------------------------------- self.__narriv = 0 # Accumulated number of arrivers until present self.__ninline = 0 # Present number waiting in line self.__nfreeserv = nserv # Present number of free servers self.__nbalked = 0 # Accumulated number of balkers self.__nreneged = 0 # Accumulated number of renegers self.__nescaped = 0 # = self.__nbalked + self.__nreneged # Attributes not available from the outside: # -------------------------------------------------------------------- self.__nserv = nserv # Initial number of free servers if line == 'Line': self.__line = Deque() elif line == 'LineStack': self.__line = Stack() else: raise Error("'line' must be 'Line' or 'LineStack' in ABCLine!") # dict containing the "reneging times" (time self.__renegers = {} # points of the future reneging events) with # the corresponding arrival times as keys self.__wtimes = array('d', []) # 'd' array of waiting times for those not escaped self.__length = {} # dict containing the line length history with # the corresponding time spans as keys self.__prevctl = 0.0 # The previous clock time when the line length # was last changed # dict containing the history of the number of self.__systemh = {} # customers in the system with the corresponding # time spans as keys self.__prevcts = 0.0 # The previous clock time when the number of # customers in system was last changed self.__length = {} # dict containing the line length history with # the corresponding time spans as keys self.__prevctl = 0.0 # The previous clock time when the line length # was last changed # dict containing the history of the number of self.__idleh = {} # of free/idle servers in the system with the # corresponding time spans as keys self.__prevcti = 0.0 # The previous clock time when the number of
class ABCLine(metaclass=ABCMeta): """ This class contains everything that is common to the Line and LineStack classes. Since this is also an abstract base class, it cannot be used in a standalone fashion. Its methods and attributes can only be reached through its subclasses Line and LineStack, which inherit from this class. """ # ------------------------------------------------------------------------------ @abstractmethod def __init__(self, line, nserv): """ line is 'Line' or 'LineStack' nserv is the initial number of servers """ # Attributes made available from the outside (not assignable, though): # -------------------------------------------------------------------- self.__narriv = 0 # Accumulated number of arrivers until present self.__ninline = 0 # Present number waiting in line self.__nfreeserv = nserv # Present number of free servers self.__nbalked = 0 # Accumulated number of balkers self.__nreneged = 0 # Accumulated number of renegers self.__nescaped = 0 # = self.__nbalked + self.__nreneged # Attributes not available from the outside: # -------------------------------------------------------------------- self.__nserv = nserv # Initial number of free servers if line == 'Line': self.__line = Deque() elif line == 'LineStack': self.__line = Stack() else: raise Error("'line' must be 'Line' or 'LineStack' in ABCLine!") # dict containing the "reneging times" (time self.__renegers = {} # points of the future reneging events) with # the corresponding arrival times as keys self.__wtimes = array('d', []) # 'd' array of waiting times for those not escaped self.__length = {} # dict containing the line length history with # the corresponding time spans as keys self.__prevctl = 0.0 # The previous clock time when the line length # was last changed # dict containing the history of the number of self.__systemh = {} # customers in the system with the corresponding # time spans as keys self.__prevcts = 0.0 # The previous clock time when the number of # customers in system was last changed self.__length = {} # dict containing the line length history with # the corresponding time spans as keys self.__prevctl = 0.0 # The previous clock time when the line length # was last changed # dict containing the history of the number of self.__idleh = {} # of free/idle servers in the system with the # corresponding time spans as keys self.__prevcti = 0.0 # The previous clock time when the number of # free/idle servers was last changed # end of __init__ # ------------------------------------------------------------------------------ def __getattr__(self, attr_name): """ This method overrides the built-in __getattr__ and makes the values of the internal attributes externally available. """ if attr_name == "ninline": return self.__ninline elif attr_name == "nfreeserv": return self.__nfreeserv elif attr_name == "narriv": return self.__narriv elif attr_name == "nbalked": return self.__nbalked elif attr_name == "renegers": return self.__renegers elif attr_name == "nreneged": return self.__nreneged elif attr_name == "nescaped": return self.__nescaped # end of __getattr__ # ------------------------------------------------------------------------------ def __setattr__(self, attr_name, value): """ This method overrides the built-in __setattr__ and makes the values of the internal attributes externally available more difficult to screw up from the outside. """ if attr_name == "ninline" \ or attr_name == "nfreeserv" \ or attr_name == "narriv" \ or attr_name == "nbalked" \ or attr_name == "renegers" \ or attr_name == "nreneged" \ or attr_name == "nescaped" : errtxt1 = ": Can't change value/length of attribute" errtxt2 = " from the outside!" raise Error(attr_name + errtxt1 + errtxt2) else: self.__dict__[attr_name] = value # end of __setattr__ # ------------------------------------------------------------------------------ def place_last_in_line(self, times): """ Add one or several arrival times at the back end of the line. The length of the expanded line is returned. NB The elements in an iterable will be placed so that the last will be the last in the line etc. Arguments: ---------- times single time or tuple/list of times Outputs: ---------- The number presently in the expanded line """ try: # First try the hunch that the argument is a sequence: self.__narriv += len(times) for tim in times: self.__length[tim-self.__prevctl] = len(self.__line) self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = tim self.__prevcts = tim except TypeError: # ..otherwise the arg is a single number self.__narriv += 1 self.__length[times-self.__prevctl] = len(self.__line) self.__systemh[times-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = times self.__prevcts = times self.__line.push(times) self.__ninline = len(self.__line) return self.__ninline # end of place_last_in_line # ------------------------------------------------------------------------------ def place_first_in_line(self, times): """ Add one or several arrival times at the front end of the line. The length of the expanded line is returned. NB The elements in an iterable will be placed so that the first will be the first in the line etc. Arguments: ---------- times single time or tuple/list of times Outputs: ---------- The length of the expanded line """ try: # First try the hunch that the argument is a sequence: self.__narriv += len(times) for tim in times: self.__length[tim-self.__prevctl] = len(self.__line) self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = tim self.__prevcts = tim except TypeError: # ..otherwise the arg is a single number self.__narriv += 1 self.__length[times-self.__prevctl] = len(self.__line) self.__systemh[times-self.__prevctl] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = times self.__prevcts = times self.__line.unshift(times) self.__ninline = len(self.__line) return self.__ninline # end of place_first_in_line # ------------------------------------------------------------------------------ def call_next_in_line(self, calltime): """ Fetch the first arrival time at the front end of the line, remove it from the line, and make one server busy. Outputs: -------- The arrival time at the front end of the line """ self.__length[calltime-self.__prevctl] = len(self.__line) self.__idleh[calltime-self.__prevcti] = self.nfreeserv self.__systemh[calltime-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = calltime self.__prevcti = calltime self.__prevcts = calltime arrivaltime = self.__line.shift() self.__nfreeserv -= 1 if self.__nfreeserv < 0: raise Error("Number of servers are negative in call_next_in_line!") self.__wtimes.append(calltime-arrivaltime) self.__ninline = len(self.__line) return arrivaltime # end of call_next_in_line # ------------------------------------------------------------------------------ def remove_last_in_line(self, tim): """ Fetch the last arrival time at the back end of the line and remove it from the line. To be used - for instance - when a customer that already has been placed in line balks (even balkers must first be placed in line - before balking!). Outputs: -------- The arrival time at the back end of the line """ self.__length[tim-self.__prevctl] = len(self.__line) self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = tim self.__prevcts = tim lasttime = self.__line.pop() self.__ninline = len(self.__line) return lasttime # end of remove_last_in_line # ------------------------------------------------------------------------------ def server_freed_up(self, tim): """ Adds '1' to the present number of free servers - to be used when a customer has been served. May also be used to change the total number of servers during the course of a simulation. """ self.__idleh[tim-self.__prevcti] = self.nfreeserv self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__nfreeserv += 1 self.__prevcti = tim self.__prevcts = tim # end of server_freed_up # ------------------------------------------------------------------------------ def balker(self, tim): """ Removes the arrival just placed last in line. Returns the arrival time of the balker. NB. Balkers must first be placed in line - before balking! Outputs: -------- The arrival time of the balker """ self.__nbalked += 1 self.__nescaped += 1 return self.remove_last_in_line(tim) # end of balker # ------------------------------------------------------------------------------ def reneger(self, arentime): """ To be used when a "renege" type event is picked up by get_next_event and removes the corresponding arrival time (arentime) in the line of arrival times GIVEN that it has not been removed already from calling call_next_in_line (the existence of the corresponding arrival time in the line is checked first). """ arrivaltime = self.__renegers[arentime] del self.__renegers[arentime] if arrivaltime in self.__line: self.__length[arentime-self.__prevctl] = len(self.__line) self.__systemh[arentime-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = arentime self.__prevcts = arentime self.__line.remove(arrivaltime) self.__ninline = len(self.__line) self.__nreneged += 1 self.__nescaped += 1 return True else: return False # end of reneger # ------------------------------------------------------------------------------ def waiting_times_all(self): """ Returns an unsorted 'd' array containing the waiting times for all served. """ return self.__wtimes # end of waiting_times_all # ------------------------------------------------------------------------------ def waiting_times_linedup(self): """ Returns an unsorted 'd' array containing the waiting times only for those who had to wait in line before being served. """ wtimesshort = array('d', []) for wt in self.__wtimes: if wt != 0.0: wtimesshort.append(wt) return wtimesshort # end of waiting_times_linedup # ------------------------------------------------------------------------------ def line_stats(self): """ Returns a dict containing line length statistics with the line lengths as keys and the corresponding times as values. """ # This method turns the self.__length dict around: the statistics are # collected with the line length as keys and the corresponding times # as values (there will be fewer elements in the returned dict than in # self.__length, of course...) statdict = {} for keytime in self.__length: try: statdict[self.__length[keytime]] += keytime except KeyError: statdict[self.__length[keytime]] = keytime return statdict # end of line_stats # ------------------------------------------------------------------------------ def idle_stats(self): """ Returns a dict containing server statistics with the number of idle servers as keys and the corresponding times as values. """ # This method turns the self.__idleh dict around: the statistics are # collected with the number of free/idle servers in the system as keys # and the corresponding times as values (there will be fewer elements # in the returned dict than in self.__idleh, of course...) statdict = {} for keytime in self.__idleh: try: statdict[self.__idleh[keytime]] += keytime except KeyError: statdict[self.__idleh[keytime]] = keytime return statdict # end of idle_stats # ------------------------------------------------------------------------------ def system_stats(self): """ Returns a dict containing statistics for the total number of customers in the system as keys and the corresponding times as values. """ # This method turns the self.__systemh dict around: the statstics are # collected with the number of customers in the system as keys and the # corresponding times as values (there will be fewer elements in the # returned dict than in self.__systemh, of course...) statdict = {} for keytime in self.__systemh: try: statdict[self.__systemh[keytime]] += keytime except KeyError: statdict[self.__systemh[keytime]] = keytime return statdict # end of system_stats # ------------------------------------------------------------------------------ # end of ABCLine # ------------------------------------------------------------------------------
class ABCRand(metaclass=ABCMeta): """ This class contains everything that is common to the GeneralRandomStream and InverseRandomStream classes. Since this is also an abstract base class, it cannot be used in a standalone fashion. Its methods and attributes can only be reached through its subclasses GeneralRandomStream and InverseRandomStream, which inherit from this class. ABCRand imports (and uses) some of the methods from Python's built-in Random class including the "Mersenne Twister". This makes the Mersenne Twister the basic rng of ABCRand and its heirs. All methods in ABCRand that are not taken from Random are inverse-based, but the methods from Random are generally not inverse-based. It may be noted that the Mersenne Twister is a very reputable random number generator having a period of 2**19937-1. The following methods from Python's own Random class are inheritable from ABCRand: randrange, randint, choice, shuffle, sample, vonmisesvariate, paretovariate and weibullvariate. All the methods in ABCRand are inherited by GeneralRandomStream including the ones imported from Random. The methods added by GeneralRandomStream do NOT return the inverse of the [0.0, 1.0] random numbers from the basic rng. InverseRandomStream inherits the methods in ABCRand with the EXCEPTION of the methods from Random (the Mersenne Twister is still there, though), making all the methods in InverseRandomStream inverse-based, including the methods added in the latter. The docstring documentation of Random, GeneralRandomStream and InverseRandomStream must always be consulted before using the methods inherited from ABCRand! NB Some methods may return float('inf') or float('-inf') ! """ # ------------------------------------------------------------------------------ @abstractmethod def __init__(self, nseed=2147483647, heir=None): """ Initiates the random stream using the input seed 'nseed' and Python's __init__ constructor method. Unless... ...the input seed 'nseed' happens to be a list or tuple of numbers in [0.0, 1.0], in which case this external feed will be used as the basis of all random variate generation for the instance and will be used in place of consecutively sampled numbers from Python's built-in "random" method! """ if isinstance(nseed, int): assert is_posinteger(nseed), \ "The seed (if not a feed) must be a positive integer in ABCRand!" rstream = Random(nseed) self._feed = False self.runif01 = rstream.random if heir != "InverseRandomStream": self.randrange = rstream.randrange self.randint = rstream.randint self.vonmisesvariate = rstream.vonmisesvariate # Random.paretovariate and Random.weibullvariate # are used by methods in GeneralRandomStream self._paretovariate = rstream.paretovariate self._weibullvariate = rstream.weibullvariate else: # nseed is a list or tuple # Check to see beforehand that no numbers # from the feed is outside [0.0, 1.0] for x in nseed: assert 0.0 <= x <= 1.0, \ "number from feed is outside of [0.0, 1.0] in ABCRand!" self._feed = Stack(nseed) # Creates a Stack object self.runif01 = self.__rfeed01 # end of __init__ # ------------------------------------------------------------------------------ def __rfeed01(self): """ Will be used as the "getter" of numbers in [0.0, 1.0] when the input to the class is a feed rather than a positive integer seed! """ # Removes one number at a time return self._feed.shift() # from the stack # end of __rfeed01 # ------------------------------------------------------------------------------ def runif_int0N(self, number): """ Generator of uniformly distributed integers in [0, number) (also the basis of some other procedures for generating random variates). Numbers returned are 0 through number-1. NB!!!!!!! """ assert is_posinteger(number) return int(number*self.runif01()) # end of runif_int0N # ------------------------------------------------------------------------------ def rsign(self): """ Returns -1.0 or 1.0 with probability 0.5 for each. """ x = self.runif01() if x <= 0.5: return -1.0 else: return 1.0 # end of rsign # ------------------------------------------------------------------------------ def runifab(self, left, right): """ Generator of uniformly distributed floats between 'left' and 'right'. """ assert right >= left, "support span must not be negative in runifab!" x = left + (right-left)*self.runif01() x = kept_within(left, x, right) return x # end of runifab # ------------------------------------------------------------------------------ def rchistogram(self, values, qumul): """ Generates random variates from an input CUMULATIVE histogram. 'values' is a list/tuple with FLOATS in ascending order - A MUST! These values represent bin end points and must be one more than the number of cumulative frequencies, and where... ...'qumul' are the corresponding CUMULATIVE FREQUENCIES such that qumul[k] = P(x<=values[k+1]). The cumulative frequencies must of course obey qumul[k+1] >= qumul[k], otherwise an exception will be raised! The values of the random variate are assumed to be uniformly distributed within each bin. """ p = self.runif01() x = ichistogram(p, values, qumul) return x # end of rchistogram # ------------------------------------------------------------------------------ def rchistogram_int(self, values, qumul): """ Generates random variates from an input CUMULATIVE histogram. 'values' is a list/tuple with INTEGERS in ascending order - A MUST! These values represent bin end points and must be one more than the number of cumulative frequencies, and where... ...'qumul' are the corresponding CUMULATIVE FREQUENCIES such that qumul[k] = P(x<=values[k+1]). NB The first element of the values list is will never be returned! The first integer to be returned is values[0] + 1 !!!! The cumulative frequencies must of course obey qumul[k+1] >= qumul[k], otherwise an exception will be raised! The integer values of the random variate are assumed to be uniformly distributed within each bin. """ p = self.runif01() x = ichistogram_int(p, values, qumul) return x # end of rchistogram_int # ------------------------------------------------------------------------------ def rtriang(self, left, mode, right): """ Generator of triangularly distributed random numbers on [left, right] with the peak of the pdf at mode. """ assert left <= mode and mode <= right, \ "mode out of support range in rtriang!" p = self.runif01() span = right - left spanlo = mode - left spanhi = right - mode #height = 2.0 / span #surf1 = 0.5 * spanlo * height #surf1 = spanlo/float(span) #if p <= surf1: if p <= spanlo/float(span): #x = sqrt(2.0*spanlo*p/height) x = sqrt(spanlo*span*p) else: #x = span - sqrt(2.0*spanhi*(1.0-p)/height) x = span - sqrt(spanhi*span*(1.0-p)) x += left x = kept_within(left, x, right) return x # end of rtriang # ------------------------------------------------------------------------------ def rtri_unif_tri(self, a, b, c, d): """ Triangular-uniform-triangular distribution with support on [a, d] and with breakpoints in b and c ------ pdf: / \ / \ ------ ------- """ # Input check ----------------------- assert a <= b and b <= c and c <= d, \ "break points scrambled in rtri_unif_tri!" # ----------------------------------- if d == a: return a dcba = d + c - b - a h = 2.0 / dcba first = 0.5 * h * (b-a) p = self.runif01() poh = 0.5 * p * dcba if p <= first: x = sqrt(2.0*(b-a)*poh) + a elif first < p <= first + h*(c-b): x = (c-b)*(poh-0.5*(b-a)) + b else: x = d - sqrt((d-c)*dcba*(1.0-p)) x = kept_within(a, x, d) return x # end of rtri_unif_tri # ------------------------------------------------------------------------------ def rkumaraswamy(self, a, b, x1, x2): """ The Kumaraswamy distribution f = a*b*x**(a-1) * (1-x**a)**(b-1) F = 1 - (1-x**a)**b a, b >= 0; 0 <= x <= 1 The Kumaraswamy is very similar to the beta distribution !!! x2 >= x1 !!!! """ assert a > 0.0, "shape parameters in rkumaraswamy must be positive!" assert b > 0.0, "shape parameters in rkumaraswamy must be positive!" assert x2 >= x1, "support range in rkumaraswamy must not be negative!" y = (1.0 - (1.0-self.runif01())**(1.0/b)) ** (1.0/a) x = y*(x2-x1) + x1 x = kept_within(x1, x, x2) return x # end of rkumaraswamy # ------------------------------------------------------------------------------ def rsinus(self, left, right): """ The "sinus distribution". """ assert right >= left, "support range must not be negative in rsinus!" #x = left + (right-left) * acos(1.0-2.0*self.runif01()) / PI x = left + (right-left) * PIINV*acos(1.0-2.0*self.runif01()) x = kept_within(left, x, right) return x # end of rsinus # ------------------------------------------------------------------------------ def rgeometric(self, phi): """ The geometric distribution with p(K=k) = phi * (1-phi)**(k-1) and P(K>=k) = sum phi * (1-phi)**k = 1 - q**k, where q = 1 - phi and 0 < phi <= 1 is the success frequency or "Bernoulli probability" and K >= 1 is the number of trials to the first success in a series of Bernoulli trials. It is easy to prove that P(k) = 1 - (1-phi)**k: let q = 1 - phi. p(k) = (1-q) * q**(k-1) = q**(k-1) - q**k. Then P(1) = p(1) = 1 - q. P(2) = p(1) + p(2) = 1 - q + q - q**2 = 1 - q**2. Induction can be used to show that P(k) = 1 - q**k = 1 - (1-phi)**k """ assert 0.0 <= phi and phi <= 1.0, \ "success frequency must be in [0.0, 1.0] in rgeometric!" if phi == 1.0: return 1 # Obvious... p = self.runif01() q = 1.0 - phi if phi < 0.25: # Use the direct inversion formula lnq = - safelog(q) ln1mp = - safelog(1.0 - p) kg = 1 + int(ln1mp/lnq) else: # Looking for the passing point is more efficient for kg = 1 # phi >= 0.25 (it's still inversion) u = p a = phi while True: u = u - a if u > 0.0: kg += 1 a *= q else: break return kg # end of rgeometric # ------------------------------------------------------------------------------ def remp_exp(self, values, npexp=0, ordered=False, \ xmax=float('inf'), pmax=1.0): """ The mixed expirical/exponential distribution from Bratley, Fox and Schrage. A polygon (piecewise linearly interpolated cdf) is used together with a (shifted) exponential for the tail. The procedure is designed so as to preserve the mean of the input sample. The input is a set of observed points (vector) and an integer representing the npexp largest points that will be used to formulate the exponential tail. NB it is assumed that x is in [0, inf) (with the usual cutoff provisions) !!!!! The function may also be used for a piecewise linear cdf without the exponential tail - corrections are made to preserve the mean in this case as well !!! """ assert xmax >= 0.0, "xmax must be a non-negative float in remp_exp!" self._checkpmax(pmax, 'remp_exp') pmx = pmax #if xmax < float('inf'): #pmx = min(pmax, cemp_exp(values, npexp, ordered, xmax)) p = pmx * self.runif01() x = iemp_exp(p, values, npexp, ordered) return x # end of remp_exp # ------------------------------------------------------------------------------ def rexpo_gen(self, a, b, c, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ The generalized continuous double-sided exponential distribution (x in R): x <= c: f = [a*b/(a+b)] * exp(+a*[x-c]) F = [b/(a+b)] * exp(+a*[x-c]) x >= c: f = [a*b/(a+b)] * exp(-b*[x-c]) F = 1 - [a/(a+b)]*exp(-b*[x-c]) a > 0, b > 0 NB The symmetrical double-sided exponential sits in rlaplace! """ self._checkminmax(xmin, xmax, pmin, pmax, 'rexpo_gen') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, cexpo_gen(a, b, c, xmin)) if xmax < float('inf'): pmx = min(pmax, cexpo_gen(a, b, c, xmax)) p = pmn + (pmx-pmn)*self.runif01() x = iexpo_gen(p, a, b, c) return x # end of rexpo_gen # ------------------------------------------------------------------------------ def rlaplace(self, loc, scale, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ The Laplace aka the symmetrical double-sided exponential distribution f = ((1/2)/s)) * exp(-abs([x-l]/s)) F = (1/2)*exp([x-l]/s) {x <= 0}, F = 1 - (1/2)*exp(-[x-l]/s) {x >= 0} s >= 0 """ self._checkminmax(xmin, xmax, pmin, pmax, 'rlaplace') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, claplace(shift, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, claplace(shift, scale, xmax)) p = pmn + (pmx-pmn)*self.runif01() x = ilaplace(p, loc, scale) return x # end of rlaplace # ------------------------------------------------------------------------------ def rtukeylambda_gen(self, lam1, lam2, lam3, lam4, pmin=0.0, pmax=1.0): """ The Friemer-Mudholkar-Kollia-Lin generalized Tukey lambda distribution. lam1 is a location parameter and lam2 a scale parameter. lam3 and lam4 are associated with the shape of the distribution. """ assert lam2 > 0.0, \ "shape parameter lam2 must be a positive float in rtukeylambda_gen!" assert 0.0 <= pmin < pmax, \ "pmin must be in [0.0, pmax) in rtukeylambda_gen!" assert pmin < pmax <= 1.0, \ "pmax must be in (pmin, 1.0] in rtukeylambda_gen!" p = pmin + (pmax-pmin)*self.runif01() if lam3 == 0.0: q3 = safelog(p) else: q3 = (p**lam3-1.0) / lam3 if lam4 == 0.0: q4 = safelog(1.0-p) else: q4 = ((1.0-p)**lam4 - 1.0) / lam4 x = lam1 + (q3-q4)/lam2 return x # end of rtukeylambda_gen # ------------------------------------------------------------------------------ def rcauchy(self, location, scale, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ Generator of random variates from the Cauchy distribution: f = 1 / [s*pi*(1 + [(x-l)/s]**2)] F = (1/pi)*arctan((x-l)/s) + 1/2 (also known as the Lorentzian or Lorentz distribution) scale must be >= 0 """ self._checkminmax(xmin, xmax, pmin, pmax, 'rcauchy') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, ccauchy(location, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, ccauchy(location, scale, xmax)) p = pmn + (pmx-pmn)*self.runif01() x = icauchy(p, location, scale) return x # end of rcauchy # ------------------------------------------------------------------------------ def rextreme_I(self, type, mu, scale, \ xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ Extreme value distribution type I (aka the Gumbel distribution or Gumbel distribution type I): F = exp{-exp[-(x-mu)/scale]} (max variant) f = exp[-(x-mu)/scale] * exp{-exp[-(x-mu)/scale]} / scale F = 1 - exp{-exp[+(x-mu)/scale]} (min variant) f = exp[+(x-mu)/scale] * exp{-exp[+(x-mu)/scale]} / scale type must be 'max' or 'min' scale must be > 0.0 """ self._checkminmax(xmin, xmax, pmin, pmax, 'rextreme_I') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, cextreme_I(type, mu, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, cextreme_I(type, mu, scale, xmax)) p = pmn + (pmx-pmn)*self.runif01() x = iextreme_I(p, type, mu, scale) return x # end of rextreme_I # ------------------------------------------------------------------------------ def rextreme_gen(self, type, shape, mu, scale, \ xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ Generalized extreme value distribution: F = exp{-[1-shape*(x-mu)/scale]**(1/shape)} (max version) f = [1-shape*(x-mu)/scale]**(1/shape-1) * exp{-[1-shape*(x-mu)/scale]**(1/shape)} / scale F = 1 - exp{-[1+shape*(x-mu)/scale]**(1/shape)} (min version) f = [1+shape*(x-mu)/scale]**(1/shape-1) * exp{-[1+shape*(x-mu)/scale]**(1/shape)} / scale shape < 0 => Type II shape > 0 => Type III shape -> 0 => Type I - Gumbel type must be 'max' or 'min' scale must be > 0.0 A REASONABLE SCHEME SEEMS TO BE mu = scale WHICH SEEMS TO LIMIT THE DISTRIBUTION TO EITHER SIDE OF THE Y-AXIS! """ self._checkminmax(xmin, xmax, pmin, pmax, 'rextreme_gen') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, cextreme_gen(type, shape, mu, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, cextreme_gen(type, shape, mu, scale, xmax)) p = pmn + (pmx-pmn)*self.runif01() x = iextreme_gen(p, type, shape, mu, scale) return x # end of rextreme_gen # ------------------------------------------------------------------------------ def rlogistic(self, mu, scale, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ The logistic distribution: F = 1 / {1 + exp[-(x-m)/s]}; x on R m is the mean and mode, and s is a scale parameter (s >= 0) """ self._checkminmax(xmin, xmax, pmin, pmax, 'rlogistic') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, clogistic(mu, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, clogistic(mu, scale, xmax)) p = pmn + (pmx-pmn)*self.runif01() x = ilogistic(p, mu, scale) return x # end of rlogistic # ------------------------------------------------------------------------------ def rrayleigh(self, sigma, xmax=float('inf'), pmax=1.0): """ The Rayleigh distribution: f = (x/s**2) * exp[-x**2/(2*s**2)] F = 1 - exp[-x**2/(2*s**2)] x, s >= 0 """ assert xmax >= 0.0, "xmax must be a non-negative float in rrayleigh!" self._checkpmax(pmax, 'rrayleigh') pmx = pmax if xmax < float('inf'): pmx = min(pmax, crayleigh(sigma, xmax)) p = pmx * self.runif01() x = irayleigh(p, sigma) return x # end of rrayleigh # ------------------------------------------------------------------------------ def rpareto_zero(self, lam, xm, xmax=float('inf'), pmax=1.0): """ The Pareto distribution with the support shifted to [0, inf): f = lam * xm**lam / (x+xm)**(lam+1) F = 1 - [xm/(x+xm)]**lam x in [0, inf) lam > 0 For lam < 1 all moments are infinite For lam < 2 all moments are infinite except for the mean """ assert xmax >= 0.0, "xmax must be a non-negative float in rpareto_zero!" self._checkpmax(pmax, 'rpareto_zeroero') pmx = pmax if xmax < float('inf'): pmx = min(pmax, cpareto_zero(lam, xm, xmax)) p = pmx * self.runif01() x = ipareto_zero(p, lam, xm) return x # end of rpareto_zero # ------------------------------------------------------------------------------ def rkodlin(self, gam, eta, xmax=float('inf'), pmax=1.0): """ The Kodlin distribution, aka the linear hazard rate distribution: f = (gam + eta*x) * exp{-[gam*x + (1/2)*eta*x**2]}, F = 1 - exp{-[gam*x + (1/2)*eta*x**2]}; x, gam, eta >= 0 """ assert xmax >= 0.0, "xmax must be a non-negative float in rkodlin!" self._checkpmax(pmax, 'rkodlin') pmx = pmax if xmax < float('inf'): pmx = min(pmax, ckodlin(scale, xmax)) p = pmx * self.runif01() x = ikodlin(p, gam, eta) return x # end of rkodlin # ------------------------------------------------------------------------------ # Auxiliary functions # ------------------------------------------------------------------------------ def _checkpmax(self, pmax, caller='caller'): assert 0.0 <= pmax and pmax <= 1.0, \ "pmax must be in [0.0, 1.0] in" + caller + "!" # end of _checkpmax # ------------------------------------------------------------------------------ def _checkminmax(self, xmin, xmax, pmin, pmax, caller='caller'): assert xmax >= xmin, \ "xmax must be >= xmin in " + caller + "!" assert 0.0 <= pmin <= pmax, \ "pmin must be in [0.0, pmax] in " + caller + "!" assert pmin <= pmax <= 1.0,\ "pmax must be in [pmin, 1.0] in " + caller + "!" # end of _checkminmax # ------------------------------------------------------------------------------ # end of ABCRand # ------------------------------------------------------------------------------
class EventScheduleStack: """ The class defines a schedule of events in a general discrete-event simulation. It relies on the misclib.Stack class (but it does not inherit from the Stack class). EventScheduleStack is less efficient (=slower) than the heap-based EventSchedule class but the two classes are otherwise equivalent in principle. Since it uses the Stack class also for the event types, and since all the methods of the list class are available via the Stack class, EventScheduleStack may be used for creating subclasses to this class which can handle more complex types of schedules than those that can be handled by the dict-based EventSchedule. An additional feature of EventScheduleStack is that it can handle TIES perfectly (the dict-based EventSchedule only handles ties approximately). """ # ------------------------------------------------------------------------------ def __init__(self, eventlist=[], timelist=[], sort=False): """ Creates two new stacks: one for the events and one for the corresponding time points. The events could for instance be described by strings. The times are (of course) floating-point numbers. The two stacks can be filled here but they have to be synchronized and in temporal order. """ assert len(timelist) == len(eventlist), \ "input lists are of unequal length in EventScheduleStack!" # If sort: # to be added later self.__eventstack = Stack(eventlist) self.__timestack = Stack(timelist) # end of __init__ # ------------------------------------------------------------------------------ def put_event(self, eventtype, eventtime): """ Method used to place an event in the event schedule. The event is placed in temporal order in the synchronized stacks eventstack and timestack. """ # Place in correct time order (what push and unshift returns - the # number of elements in the Stack After the "putting" - is not needed # for anything and it is OK to do as below) if not self.__timestack or eventtime >= self.__timestack[ -1]: # Put last self.__timestack.push(eventtime) self.__eventstack.push(eventtype) #self.__timestack = [eventtime] # Does not work - turns the #self.__eventstack = [eventtype] # Stack into a list again elif eventtime < self.__timestack[0]: # Put first self.__timestack.unshift(eventtime) self.__eventstack.unshift(eventtype) else: index = bisect(self.__timestack, eventtime) self.__timestack.splice(index, 0, eventtime) self.__eventstack.splice(index, 0, eventtype) return eventtime # For symmetry with put_event of EventSchedule # end of put_event # ------------------------------------------------------------------------------ def show_next_event(self): """ Just look at the next event without touching the stack. """ #if not equal_length(self.__eventstack, self.__timestack): # is not needed due to put_event try: nextevent = self.__eventstack[0] except IndexError: nextevent = None try: nexttime = self.__timestack[0] except IndexError: nexttime = None return nextevent, nexttime # end of show_next_event # ------------------------------------------------------------------------------ def get_next_event(self): """ Method used to get the next stored event (the first in time) from the event schedule. The synchronized stacks eventstack and timestack are shifted in Perl fashion - i. e. the element that is returned is also removed from the stack. Returns None if the stacks are empty. """ #if not equal_length(self.__eventstack, self.__timestack): # is not needed due to put_event nextevent = self.__eventstack.shift() # None if empty nexttime = self.__timestack.shift() # None if empty return nextevent, nexttime # end of get_next_event # ------------------------------------------------------------------------------ def zap_events(self): """ Empty the schedule to allow for a restart. Return the length of the stack as it was before zapping. """ #if not equal_length(self.__eventstack, self.__timestack): # is not needed due to put_event length = self.__eventstack.zap() length = self.__timestack.zap() return length # end of zap_events # ------------------------------------------------------------------------------ # end of EventScheduleStack # ------------------------------------------------------------------------------
class EventScheduleStack: """ The class defines a schedule of events in a general discrete-event simulation. It relies on the misclib.Stack class (but it does not inherit from the Stack class). EventScheduleStack is less efficient (=slower) than the heap-based EventSchedule class but the two classes are otherwise equivalent in principle. Since it uses the Stack class also for the event types, and since all the methods of the list class are available via the Stack class, EventScheduleStack may be used for creating subclasses to this class which can handle more complex types of schedules than those that can be handled by the dict-based EventSchedule. An additional feature of EventScheduleStack is that it can handle TIES perfectly (the dict-based EventSchedule only handles ties approximately). """ # ------------------------------------------------------------------------------ def __init__(self, eventlist=[], timelist=[], sort=False): """ Creates two new stacks: one for the events and one for the corresponding time points. The events could for instance be described by strings. The times are (of course) floating-point numbers. The two stacks can be filled here but they have to be synchronized and in temporal order. """ assert len(timelist) == len(eventlist), \ "input lists are of unequal length in EventScheduleStack!" # If sort: # to be added later self.__eventstack = Stack(eventlist) self.__timestack = Stack(timelist) # end of __init__ # ------------------------------------------------------------------------------ def put_event(self, eventtype, eventtime): """ Method used to place an event in the event schedule. The event is placed in temporal order in the synchronized stacks eventstack and timestack. """ # Place in correct time order (what push and unshift returns - the # number of elements in the Stack After the "putting" - is not needed # for anything and it is OK to do as below) if not self.__timestack or eventtime >= self.__timestack[-1]: # Put last self.__timestack.push(eventtime) self.__eventstack.push(eventtype) #self.__timestack = [eventtime] # Does not work - turns the #self.__eventstack = [eventtype] # Stack into a list again elif eventtime < self.__timestack[0]: # Put first self.__timestack.unshift(eventtime) self.__eventstack.unshift(eventtype) else: index = bisect(self.__timestack, eventtime) self.__timestack.splice(index, 0, eventtime) self.__eventstack.splice(index, 0, eventtype) return eventtime # For symmetry with put_event of EventSchedule # end of put_event # ------------------------------------------------------------------------------ def show_next_event(self): """ Just look at the next event without touching the stack. """ #if not equal_length(self.__eventstack, self.__timestack): # is not needed due to put_event try: nextevent = self.__eventstack[0] except IndexError: nextevent = None try: nexttime = self.__timestack[0] except IndexError: nexttime = None return nextevent, nexttime # end of show_next_event # ------------------------------------------------------------------------------ def get_next_event(self): """ Method used to get the next stored event (the first in time) from the event schedule. The synchronized stacks eventstack and timestack are shifted in Perl fashion - i. e. the element that is returned is also removed from the stack. Returns None if the stacks are empty. """ #if not equal_length(self.__eventstack, self.__timestack): # is not needed due to put_event nextevent = self.__eventstack.shift() # None if empty nexttime = self.__timestack.shift() # None if empty return nextevent, nexttime # end of get_next_event # ------------------------------------------------------------------------------ def zap_events(self): """ Empty the schedule to allow for a restart. Return the length of the stack as it was before zapping. """ #if not equal_length(self.__eventstack, self.__timestack): # is not needed due to put_event length = self.__eventstack.zap() length = self.__timestack.zap() return length # end of zap_events # ------------------------------------------------------------------------------ # end of EventScheduleStack # ------------------------------------------------------------------------------
class ABCLine(metaclass=ABCMeta): """ This class contains everything that is common to the Line and LineStack classes. Since this is also an abstract base class, it cannot be used in a standalone fashion. Its methods and attributes can only be reached through its subclasses Line and LineStack, which inherit from this class. """ # ------------------------------------------------------------------------------ @abstractmethod def __init__(self, line, nserv): """ line is 'Line' or 'LineStack' nserv is the initial number of servers """ # Attributes made available from the outside (not assignable, though): # -------------------------------------------------------------------- self.__narriv = 0 # Accumulated number of arrivers until present self.__ninline = 0 # Present number waiting in line self.__nfreeserv = nserv # Present number of free servers self.__nbalked = 0 # Accumulated number of balkers self.__nreneged = 0 # Accumulated number of renegers self.__nescaped = 0 # = self.__nbalked + self.__nreneged # Attributes not available from the outside: # -------------------------------------------------------------------- self.__nserv = nserv # Initial number of free servers if line == 'Line': self.__line = Deque() elif line == 'LineStack': self.__line = Stack() else: raise Error("'line' must be 'Line' or 'LineStack' in ABCLine!") # dict containing the "reneging times" (time self.__renegers = {} # points of the future reneging events) with # the corresponding arrival times as keys self.__wtimes = array('d', []) # 'd' array of waiting times for those not escaped self.__length = {} # dict containing the line length history with # the corresponding time spans as keys self.__prevctl = 0.0 # The previous clock time when the line length # was last changed # dict containing the history of the number of self.__systemh = {} # customers in the system with the corresponding # time spans as keys self.__prevcts = 0.0 # The previous clock time when the number of # customers in system was last changed self.__length = {} # dict containing the line length history with # the corresponding time spans as keys self.__prevctl = 0.0 # The previous clock time when the line length # was last changed # dict containing the history of the number of self.__idleh = {} # of free/idle servers in the system with the # corresponding time spans as keys self.__prevcti = 0.0 # The previous clock time when the number of # free/idle servers was last changed # end of __init__ # ------------------------------------------------------------------------------ def __getattr__(self, attr_name): """ This method overrides the built-in __getattr__ and makes the values of the internal attributes externally available. """ if attr_name == "ninline": return self.__ninline elif attr_name == "nfreeserv": return self.__nfreeserv elif attr_name == "narriv": return self.__narriv elif attr_name == "nbalked": return self.__nbalked elif attr_name == "renegers": return self.__renegers elif attr_name == "nreneged": return self.__nreneged elif attr_name == "nescaped": return self.__nescaped # end of __getattr__ # ------------------------------------------------------------------------------ def __setattr__(self, attr_name, value): """ This method overrides the built-in __setattr__ and makes the values of the internal attributes externally available more difficult to screw up from the outside. """ if attr_name == "ninline" \ or attr_name == "nfreeserv" \ or attr_name == "narriv" \ or attr_name == "nbalked" \ or attr_name == "renegers" \ or attr_name == "nreneged" \ or attr_name == "nescaped" : errtxt1 = ": Can't change value/length of attribute" errtxt2 = " from the outside!" raise Error(attr_name + errtxt1 + errtxt2) else: self.__dict__[attr_name] = value # end of __setattr__ # ------------------------------------------------------------------------------ def place_last_in_line(self, times): """ Add one or several arrival times at the back end of the line. The length of the expanded line is returned. NB The elements in an iterable will be placed so that the last will be the last in the line etc. Arguments: ---------- times single time or tuple/list of times Outputs: ---------- The number presently in the expanded line """ try: # First try the hunch that the argument is a sequence: self.__narriv += len(times) for tim in times: self.__length[tim - self.__prevctl] = len(self.__line) self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = tim self.__prevcts = tim except TypeError: # ..otherwise the arg is a single number self.__narriv += 1 self.__length[times - self.__prevctl] = len(self.__line) self.__systemh[times-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = times self.__prevcts = times self.__line.push(times) self.__ninline = len(self.__line) return self.__ninline # end of place_last_in_line # ------------------------------------------------------------------------------ def place_first_in_line(self, times): """ Add one or several arrival times at the front end of the line. The length of the expanded line is returned. NB The elements in an iterable will be placed so that the first will be the first in the line etc. Arguments: ---------- times single time or tuple/list of times Outputs: ---------- The length of the expanded line """ try: # First try the hunch that the argument is a sequence: self.__narriv += len(times) for tim in times: self.__length[tim - self.__prevctl] = len(self.__line) self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = tim self.__prevcts = tim except TypeError: # ..otherwise the arg is a single number self.__narriv += 1 self.__length[times - self.__prevctl] = len(self.__line) self.__systemh[times-self.__prevctl] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = times self.__prevcts = times self.__line.unshift(times) self.__ninline = len(self.__line) return self.__ninline # end of place_first_in_line # ------------------------------------------------------------------------------ def call_next_in_line(self, calltime): """ Fetch the first arrival time at the front end of the line, remove it from the line, and make one server busy. Outputs: -------- The arrival time at the front end of the line """ self.__length[calltime - self.__prevctl] = len(self.__line) self.__idleh[calltime - self.__prevcti] = self.nfreeserv self.__systemh[calltime-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = calltime self.__prevcti = calltime self.__prevcts = calltime arrivaltime = self.__line.shift() self.__nfreeserv -= 1 if self.__nfreeserv < 0: raise Error("Number of servers are negative in call_next_in_line!") self.__wtimes.append(calltime - arrivaltime) self.__ninline = len(self.__line) return arrivaltime # end of call_next_in_line # ------------------------------------------------------------------------------ def remove_last_in_line(self, tim): """ Fetch the last arrival time at the back end of the line and remove it from the line. To be used - for instance - when a customer that already has been placed in line balks (even balkers must first be placed in line - before balking!). Outputs: -------- The arrival time at the back end of the line """ self.__length[tim - self.__prevctl] = len(self.__line) self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = tim self.__prevcts = tim lasttime = self.__line.pop() self.__ninline = len(self.__line) return lasttime # end of remove_last_in_line # ------------------------------------------------------------------------------ def server_freed_up(self, tim): """ Adds '1' to the present number of free servers - to be used when a customer has been served. May also be used to change the total number of servers during the course of a simulation. """ self.__idleh[tim - self.__prevcti] = self.nfreeserv self.__systemh[tim-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__nfreeserv += 1 self.__prevcti = tim self.__prevcts = tim # end of server_freed_up # ------------------------------------------------------------------------------ def balker(self, tim): """ Removes the arrival just placed last in line. Returns the arrival time of the balker. NB. Balkers must first be placed in line - before balking! Outputs: -------- The arrival time of the balker """ self.__nbalked += 1 self.__nescaped += 1 return self.remove_last_in_line(tim) # end of balker # ------------------------------------------------------------------------------ def reneger(self, arentime): """ To be used when a "renege" type event is picked up by get_next_event and removes the corresponding arrival time (arentime) in the line of arrival times GIVEN that it has not been removed already from calling call_next_in_line (the existence of the corresponding arrival time in the line is checked first). """ arrivaltime = self.__renegers[arentime] del self.__renegers[arentime] if arrivaltime in self.__line: self.__length[arentime - self.__prevctl] = len(self.__line) self.__systemh[arentime-self.__prevcts] = len(self.__line) + \ self.__nserv - self.__nfreeserv self.__prevctl = arentime self.__prevcts = arentime self.__line.remove(arrivaltime) self.__ninline = len(self.__line) self.__nreneged += 1 self.__nescaped += 1 return True else: return False # end of reneger # ------------------------------------------------------------------------------ def waiting_times_all(self): """ Returns an unsorted 'd' array containing the waiting times for all served. """ return self.__wtimes # end of waiting_times_all # ------------------------------------------------------------------------------ def waiting_times_linedup(self): """ Returns an unsorted 'd' array containing the waiting times only for those who had to wait in line before being served. """ wtimesshort = array('d', []) for wt in self.__wtimes: if wt != 0.0: wtimesshort.append(wt) return wtimesshort # end of waiting_times_linedup # ------------------------------------------------------------------------------ def line_stats(self): """ Returns a dict containing line length statistics with the line lengths as keys and the corresponding times as values. """ # This method turns the self.__length dict around: the statistics are # collected with the line length as keys and the corresponding times # as values (there will be fewer elements in the returned dict than in # self.__length, of course...) statdict = {} for keytime in self.__length: try: statdict[self.__length[keytime]] += keytime except KeyError: statdict[self.__length[keytime]] = keytime return statdict # end of line_stats # ------------------------------------------------------------------------------ def idle_stats(self): """ Returns a dict containing server statistics with the number of idle servers as keys and the corresponding times as values. """ # This method turns the self.__idleh dict around: the statistics are # collected with the number of free/idle servers in the system as keys # and the corresponding times as values (there will be fewer elements # in the returned dict than in self.__idleh, of course...) statdict = {} for keytime in self.__idleh: try: statdict[self.__idleh[keytime]] += keytime except KeyError: statdict[self.__idleh[keytime]] = keytime return statdict # end of idle_stats # ------------------------------------------------------------------------------ def system_stats(self): """ Returns a dict containing statistics for the total number of customers in the system as keys and the corresponding times as values. """ # This method turns the self.__systemh dict around: the statstics are # collected with the number of customers in the system as keys and the # corresponding times as values (there will be fewer elements in the # returned dict than in self.__systemh, of course...) statdict = {} for keytime in self.__systemh: try: statdict[self.__systemh[keytime]] += keytime except KeyError: statdict[self.__systemh[keytime]] = keytime return statdict # end of system_stats # ------------------------------------------------------------------------------ # end of ABCLine # ------------------------------------------------------------------------------
class ABCRand(metaclass=ABCMeta): """ This class contains everything that is common to the GeneralRandomStream and InverseRandomStream classes. Since this is also an abstract base class, it cannot be used in a standalone fashion. Its methods and attributes can only be reached through its subclasses GeneralRandomStream and InverseRandomStream, which inherit from this class. ABCRand imports (and uses) some of the methods from Python's built-in Random class including the "Mersenne Twister". This makes the Mersenne Twister the basic rng of ABCRand and its heirs. All methods in ABCRand that are not taken from Random are inverse-based, but the methods from Random are generally not inverse-based. It may be noted that the Mersenne Twister is a very reputable random number generator having a period of 2**19937-1. The following methods from Python's own Random class are inheritable from ABCRand: randrange, randint, choice, shuffle, sample, vonmisesvariate, paretovariate and weibullvariate. All the methods in ABCRand are inherited by GeneralRandomStream including the ones imported from Random. The methods added by GeneralRandomStream do NOT return the inverse of the [0.0, 1.0] random numbers from the basic rng. InverseRandomStream inherits the methods in ABCRand with the EXCEPTION of the methods from Random (the Mersenne Twister is still there, though), making all the methods in InverseRandomStream inverse-based, including the methods added in the latter. The docstring documentation of Random, GeneralRandomStream and InverseRandomStream must always be consulted before using the methods inherited from ABCRand! NB Some methods may return float('inf') or float('-inf') ! """ # ------------------------------------------------------------------------------ @abstractmethod def __init__(self, nseed=2147483647, heir=None): """ Initiates the random stream using the input seed 'nseed' and Python's __init__ constructor method. Unless... ...the input seed 'nseed' happens to be a list or tuple of numbers in [0.0, 1.0], in which case this external feed will be used as the basis of all random variate generation for the instance and will be used in place of consecutively sampled numbers from Python's built-in "random" method! """ if isinstance(nseed, int): assert is_posinteger(nseed), \ "The seed (if not a feed) must be a positive integer in ABCRand!" rstream = Random(nseed) self._feed = False self.runif01 = rstream.random if heir != "InverseRandomStream": self.randrange = rstream.randrange self.randint = rstream.randint self.vonmisesvariate = rstream.vonmisesvariate # Random.paretovariate and Random.weibullvariate # are used by methods in GeneralRandomStream self._paretovariate = rstream.paretovariate self._weibullvariate = rstream.weibullvariate else: # nseed is a list or tuple # Check to see beforehand that no numbers # from the feed is outside [0.0, 1.0] for x in nseed: assert 0.0 <= x <= 1.0, \ "number from feed is outside of [0.0, 1.0] in ABCRand!" self._feed = Stack(nseed) # Creates a Stack object self.runif01 = self.__rfeed01 # end of __init__ # ------------------------------------------------------------------------------ def __rfeed01(self): """ Will be used as the "getter" of numbers in [0.0, 1.0] when the input to the class is a feed rather than a positive integer seed! """ # Removes one number at a time return self._feed.shift() # from the stack # end of __rfeed01 # ------------------------------------------------------------------------------ def runif_int0N(self, number): """ Generator of uniformly distributed integers in [0, number) (also the basis of some other procedures for generating random variates). Numbers returned are 0 through number-1. NB!!!!!!! """ assert is_posinteger(number) return int(number * self.runif01()) # end of runif_int0N # ------------------------------------------------------------------------------ def rsign(self): """ Returns -1.0 or 1.0 with probability 0.5 for each. """ x = self.runif01() if x <= 0.5: return -1.0 else: return 1.0 # end of rsign # ------------------------------------------------------------------------------ def runifab(self, left, right): """ Generator of uniformly distributed floats between 'left' and 'right'. """ assert right >= left, "support span must not be negative in runifab!" x = left + (right - left) * self.runif01() x = kept_within(left, x, right) return x # end of runifab # ------------------------------------------------------------------------------ def rchistogram(self, values, qumul): """ Generates random variates from an input CUMULATIVE histogram. 'values' is a list/tuple with FLOATS in ascending order - A MUST! These values represent bin end points and must be one more than the number of cumulative frequencies, and where... ...'qumul' are the corresponding CUMULATIVE FREQUENCIES such that qumul[k] = P(x<=values[k+1]). The cumulative frequencies must of course obey qumul[k+1] >= qumul[k], otherwise an exception will be raised! The values of the random variate are assumed to be uniformly distributed within each bin. """ p = self.runif01() x = ichistogram(p, values, qumul) return x # end of rchistogram # ------------------------------------------------------------------------------ def rchistogram_int(self, values, qumul): """ Generates random variates from an input CUMULATIVE histogram. 'values' is a list/tuple with INTEGERS in ascending order - A MUST! These values represent bin end points and must be one more than the number of cumulative frequencies, and where... ...'qumul' are the corresponding CUMULATIVE FREQUENCIES such that qumul[k] = P(x<=values[k+1]). NB The first element of the values list is will never be returned! The first integer to be returned is values[0] + 1 !!!! The cumulative frequencies must of course obey qumul[k+1] >= qumul[k], otherwise an exception will be raised! The integer values of the random variate are assumed to be uniformly distributed within each bin. """ p = self.runif01() x = ichistogram_int(p, values, qumul) return x # end of rchistogram_int # ------------------------------------------------------------------------------ def rtriang(self, left, mode, right): """ Generator of triangularly distributed random numbers on [left, right] with the peak of the pdf at mode. """ assert left <= mode and mode <= right, \ "mode out of support range in rtriang!" p = self.runif01() span = right - left spanlo = mode - left spanhi = right - mode #height = 2.0 / span #surf1 = 0.5 * spanlo * height #surf1 = spanlo/float(span) #if p <= surf1: if p <= spanlo / float(span): #x = sqrt(2.0*spanlo*p/height) x = sqrt(spanlo * span * p) else: #x = span - sqrt(2.0*spanhi*(1.0-p)/height) x = span - sqrt(spanhi * span * (1.0 - p)) x += left x = kept_within(left, x, right) return x # end of rtriang # ------------------------------------------------------------------------------ def rtri_unif_tri(self, a, b, c, d): """ Triangular-uniform-triangular distribution with support on [a, d] and with breakpoints in b and c ------ pdf: / \ / \ ------ ------- """ # Input check ----------------------- assert a <= b and b <= c and c <= d, \ "break points scrambled in rtri_unif_tri!" # ----------------------------------- if d == a: return a dcba = d + c - b - a h = 2.0 / dcba first = 0.5 * h * (b - a) p = self.runif01() poh = 0.5 * p * dcba if p <= first: x = sqrt(2.0 * (b - a) * poh) + a elif first < p <= first + h * (c - b): x = (c - b) * (poh - 0.5 * (b - a)) + b else: x = d - sqrt((d - c) * dcba * (1.0 - p)) x = kept_within(a, x, d) return x # end of rtri_unif_tri # ------------------------------------------------------------------------------ def rkumaraswamy(self, a, b, x1, x2): """ The Kumaraswamy distribution f = a*b*x**(a-1) * (1-x**a)**(b-1) F = 1 - (1-x**a)**b a, b >= 0; 0 <= x <= 1 The Kumaraswamy is very similar to the beta distribution !!! x2 >= x1 !!!! """ assert a > 0.0, "shape parameters in rkumaraswamy must be positive!" assert b > 0.0, "shape parameters in rkumaraswamy must be positive!" assert x2 >= x1, "support range in rkumaraswamy must not be negative!" y = (1.0 - (1.0 - self.runif01())**(1.0 / b))**(1.0 / a) x = y * (x2 - x1) + x1 x = kept_within(x1, x, x2) return x # end of rkumaraswamy # ------------------------------------------------------------------------------ def rsinus(self, left, right): """ The "sinus distribution". """ assert right >= left, "support range must not be negative in rsinus!" #x = left + (right-left) * acos(1.0-2.0*self.runif01()) / PI x = left + (right - left) * PIINV * acos(1.0 - 2.0 * self.runif01()) x = kept_within(left, x, right) return x # end of rsinus # ------------------------------------------------------------------------------ def rgeometric(self, phi): """ The geometric distribution with p(K=k) = phi * (1-phi)**(k-1) and P(K>=k) = sum phi * (1-phi)**k = 1 - q**k, where q = 1 - phi and 0 < phi <= 1 is the success frequency or "Bernoulli probability" and K >= 1 is the number of trials to the first success in a series of Bernoulli trials. It is easy to prove that P(k) = 1 - (1-phi)**k: let q = 1 - phi. p(k) = (1-q) * q**(k-1) = q**(k-1) - q**k. Then P(1) = p(1) = 1 - q. P(2) = p(1) + p(2) = 1 - q + q - q**2 = 1 - q**2. Induction can be used to show that P(k) = 1 - q**k = 1 - (1-phi)**k """ assert 0.0 <= phi and phi <= 1.0, \ "success frequency must be in [0.0, 1.0] in rgeometric!" if phi == 1.0: return 1 # Obvious... p = self.runif01() q = 1.0 - phi if phi < 0.25: # Use the direct inversion formula lnq = -safelog(q) ln1mp = -safelog(1.0 - p) kg = 1 + int(ln1mp / lnq) else: # Looking for the passing point is more efficient for kg = 1 # phi >= 0.25 (it's still inversion) u = p a = phi while True: u = u - a if u > 0.0: kg += 1 a *= q else: break return kg # end of rgeometric # ------------------------------------------------------------------------------ def remp_exp(self, values, npexp=0, ordered=False, \ xmax=float('inf'), pmax=1.0): """ The mixed expirical/exponential distribution from Bratley, Fox and Schrage. A polygon (piecewise linearly interpolated cdf) is used together with a (shifted) exponential for the tail. The procedure is designed so as to preserve the mean of the input sample. The input is a set of observed points (vector) and an integer representing the npexp largest points that will be used to formulate the exponential tail. NB it is assumed that x is in [0, inf) (with the usual cutoff provisions) !!!!! The function may also be used for a piecewise linear cdf without the exponential tail - corrections are made to preserve the mean in this case as well !!! """ assert xmax >= 0.0, "xmax must be a non-negative float in remp_exp!" self._checkpmax(pmax, 'remp_exp') pmx = pmax #if xmax < float('inf'): #pmx = min(pmax, cemp_exp(values, npexp, ordered, xmax)) p = pmx * self.runif01() x = iemp_exp(p, values, npexp, ordered) return x # end of remp_exp # ------------------------------------------------------------------------------ def rexpo_gen(self, a, b, c, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ The generalized continuous double-sided exponential distribution (x in R): x <= c: f = [a*b/(a+b)] * exp(+a*[x-c]) F = [b/(a+b)] * exp(+a*[x-c]) x >= c: f = [a*b/(a+b)] * exp(-b*[x-c]) F = 1 - [a/(a+b)]*exp(-b*[x-c]) a > 0, b > 0 NB The symmetrical double-sided exponential sits in rlaplace! """ self._checkminmax(xmin, xmax, pmin, pmax, 'rexpo_gen') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, cexpo_gen(a, b, c, xmin)) if xmax < float('inf'): pmx = min(pmax, cexpo_gen(a, b, c, xmax)) p = pmn + (pmx - pmn) * self.runif01() x = iexpo_gen(p, a, b, c) return x # end of rexpo_gen # ------------------------------------------------------------------------------ def rlaplace(self, loc, scale, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ The Laplace aka the symmetrical double-sided exponential distribution f = ((1/2)/s)) * exp(-abs([x-l]/s)) F = (1/2)*exp([x-l]/s) {x <= 0}, F = 1 - (1/2)*exp(-[x-l]/s) {x >= 0} s >= 0 """ self._checkminmax(xmin, xmax, pmin, pmax, 'rlaplace') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, claplace(shift, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, claplace(shift, scale, xmax)) p = pmn + (pmx - pmn) * self.runif01() x = ilaplace(p, loc, scale) return x # end of rlaplace # ------------------------------------------------------------------------------ def rtukeylambda_gen(self, lam1, lam2, lam3, lam4, pmin=0.0, pmax=1.0): """ The Friemer-Mudholkar-Kollia-Lin generalized Tukey lambda distribution. lam1 is a location parameter and lam2 a scale parameter. lam3 and lam4 are associated with the shape of the distribution. """ assert lam2 > 0.0, \ "shape parameter lam2 must be a positive float in rtukeylambda_gen!" assert 0.0 <= pmin < pmax, \ "pmin must be in [0.0, pmax) in rtukeylambda_gen!" assert pmin < pmax <= 1.0, \ "pmax must be in (pmin, 1.0] in rtukeylambda_gen!" p = pmin + (pmax - pmin) * self.runif01() if lam3 == 0.0: q3 = safelog(p) else: q3 = (p**lam3 - 1.0) / lam3 if lam4 == 0.0: q4 = safelog(1.0 - p) else: q4 = ((1.0 - p)**lam4 - 1.0) / lam4 x = lam1 + (q3 - q4) / lam2 return x # end of rtukeylambda_gen # ------------------------------------------------------------------------------ def rcauchy(self, location, scale, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ Generator of random variates from the Cauchy distribution: f = 1 / [s*pi*(1 + [(x-l)/s]**2)] F = (1/pi)*arctan((x-l)/s) + 1/2 (also known as the Lorentzian or Lorentz distribution) scale must be >= 0 """ self._checkminmax(xmin, xmax, pmin, pmax, 'rcauchy') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, ccauchy(location, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, ccauchy(location, scale, xmax)) p = pmn + (pmx - pmn) * self.runif01() x = icauchy(p, location, scale) return x # end of rcauchy # ------------------------------------------------------------------------------ def rextreme_I(self, type, mu, scale, \ xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ Extreme value distribution type I (aka the Gumbel distribution or Gumbel distribution type I): F = exp{-exp[-(x-mu)/scale]} (max variant) f = exp[-(x-mu)/scale] * exp{-exp[-(x-mu)/scale]} / scale F = 1 - exp{-exp[+(x-mu)/scale]} (min variant) f = exp[+(x-mu)/scale] * exp{-exp[+(x-mu)/scale]} / scale type must be 'max' or 'min' scale must be > 0.0 """ self._checkminmax(xmin, xmax, pmin, pmax, 'rextreme_I') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, cextreme_I(type, mu, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, cextreme_I(type, mu, scale, xmax)) p = pmn + (pmx - pmn) * self.runif01() x = iextreme_I(p, type, mu, scale) return x # end of rextreme_I # ------------------------------------------------------------------------------ def rextreme_gen(self, type, shape, mu, scale, \ xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ Generalized extreme value distribution: F = exp{-[1-shape*(x-mu)/scale]**(1/shape)} (max version) f = [1-shape*(x-mu)/scale]**(1/shape-1) * exp{-[1-shape*(x-mu)/scale]**(1/shape)} / scale F = 1 - exp{-[1+shape*(x-mu)/scale]**(1/shape)} (min version) f = [1+shape*(x-mu)/scale]**(1/shape-1) * exp{-[1+shape*(x-mu)/scale]**(1/shape)} / scale shape < 0 => Type II shape > 0 => Type III shape -> 0 => Type I - Gumbel type must be 'max' or 'min' scale must be > 0.0 A REASONABLE SCHEME SEEMS TO BE mu = scale WHICH SEEMS TO LIMIT THE DISTRIBUTION TO EITHER SIDE OF THE Y-AXIS! """ self._checkminmax(xmin, xmax, pmin, pmax, 'rextreme_gen') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, cextreme_gen(type, shape, mu, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, cextreme_gen(type, shape, mu, scale, xmax)) p = pmn + (pmx - pmn) * self.runif01() x = iextreme_gen(p, type, shape, mu, scale) return x # end of rextreme_gen # ------------------------------------------------------------------------------ def rlogistic(self, mu, scale, xmin=float('-inf'), xmax=float('inf'), \ pmin=0.0, pmax=1.0): """ The logistic distribution: F = 1 / {1 + exp[-(x-m)/s]}; x on R m is the mean and mode, and s is a scale parameter (s >= 0) """ self._checkminmax(xmin, xmax, pmin, pmax, 'rlogistic') pmn = pmin pmx = pmax if xmin > float('-inf'): pmn = max(pmin, clogistic(mu, scale, xmin)) if xmax < float('inf'): pmx = min(pmax, clogistic(mu, scale, xmax)) p = pmn + (pmx - pmn) * self.runif01() x = ilogistic(p, mu, scale) return x # end of rlogistic # ------------------------------------------------------------------------------ def rrayleigh(self, sigma, xmax=float('inf'), pmax=1.0): """ The Rayleigh distribution: f = (x/s**2) * exp[-x**2/(2*s**2)] F = 1 - exp[-x**2/(2*s**2)] x, s >= 0 """ assert xmax >= 0.0, "xmax must be a non-negative float in rrayleigh!" self._checkpmax(pmax, 'rrayleigh') pmx = pmax if xmax < float('inf'): pmx = min(pmax, crayleigh(sigma, xmax)) p = pmx * self.runif01() x = irayleigh(p, sigma) return x # end of rrayleigh # ------------------------------------------------------------------------------ def rpareto_zero(self, lam, xm, xmax=float('inf'), pmax=1.0): """ The Pareto distribution with the support shifted to [0, inf): f = lam * xm**lam / (x+xm)**(lam+1) F = 1 - [xm/(x+xm)]**lam x in [0, inf) lam > 0 For lam < 1 all moments are infinite For lam < 2 all moments are infinite except for the mean """ assert xmax >= 0.0, "xmax must be a non-negative float in rpareto_zero!" self._checkpmax(pmax, 'rpareto_zeroero') pmx = pmax if xmax < float('inf'): pmx = min(pmax, cpareto_zero(lam, xm, xmax)) p = pmx * self.runif01() x = ipareto_zero(p, lam, xm) return x # end of rpareto_zero # ------------------------------------------------------------------------------ def rkodlin(self, gam, eta, xmax=float('inf'), pmax=1.0): """ The Kodlin distribution, aka the linear hazard rate distribution: f = (gam + eta*x) * exp{-[gam*x + (1/2)*eta*x**2]}, F = 1 - exp{-[gam*x + (1/2)*eta*x**2]}; x, gam, eta >= 0 """ assert xmax >= 0.0, "xmax must be a non-negative float in rkodlin!" self._checkpmax(pmax, 'rkodlin') pmx = pmax if xmax < float('inf'): pmx = min(pmax, ckodlin(scale, xmax)) p = pmx * self.runif01() x = ikodlin(p, gam, eta) return x # end of rkodlin # ------------------------------------------------------------------------------ # Auxiliary functions # ------------------------------------------------------------------------------ def _checkpmax(self, pmax, caller='caller'): assert 0.0 <= pmax and pmax <= 1.0, \ "pmax must be in [0.0, 1.0] in" + caller + "!" # end of _checkpmax # ------------------------------------------------------------------------------ def _checkminmax(self, xmin, xmax, pmin, pmax, caller='caller'): assert xmax >= xmin, \ "xmax must be >= xmin in " + caller + "!" assert 0.0 <= pmin <= pmax, \ "pmin must be in [0.0, pmax] in " + caller + "!" assert pmin <= pmax <= 1.0,\ "pmax must be in [pmin, 1.0] in " + caller + "!" # end of _checkminmax # ------------------------------------------------------------------------------ # end of ABCRand # ------------------------------------------------------------------------------