From 4587f217ef7a1187685131c87854cfe69df83f36 Mon Sep 17 00:00:00 2001 From: cargar_github Date: Mon, 30 Sep 2024 23:02:01 -0500 Subject: [PATCH 01/11] adding process simulation --- phitter/simulation/__init__.py | 0 .../process_simulation/process_simulation.py | 218 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 phitter/simulation/__init__.py create mode 100644 phitter/simulation/process_simulation/process_simulation.py diff --git a/phitter/simulation/__init__.py b/phitter/simulation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/phitter/simulation/process_simulation/process_simulation.py b/phitter/simulation/process_simulation/process_simulation.py new file mode 100644 index 0000000..73ecb8d --- /dev/null +++ b/phitter/simulation/process_simulation/process_simulation.py @@ -0,0 +1,218 @@ +import phitter +import random +import numpy as np +import math +from graphviz import Digraph +from IPython.display import display + + +class Simulation: + def __init__(self) -> None: + + self.process_prob_distr = dict() + self.branches = dict() + self.order = dict() + self.number_of_products = dict() + self.process_positions = dict() + self.next_process = dict() + self.probability_distribution = ( + phitter.continuous.CONTINUOUS_DISTRIBUTIONS + | phitter.discrete.DISCRETE_DISTRIBUTIONS + ) + + def add_process( + self, + prob_distribution: str, + parameters: dict, + process_id: str, + number_of_products: int = 1, + new_branch: bool = False, + previous_ids: list[str] = None, + ) -> None: + """Add element to the simulation + + Args: + prob_distribution (str): Probability distribution to be used. You can use one of the following: 'alpha', 'arcsine', 'argus', 'beta', 'beta_prime', 'beta_prime_4p', 'bradford', 'burr', 'burr_4p', 'cauchy', 'chi_square', 'chi_square_3p', 'dagum', 'dagum_4p', 'erlang', 'erlang_3p', 'error_function', 'exponential', 'exponential_2p', 'f', 'fatigue_life', 'folded_normal', 'frechet', 'f_4p', 'gamma', 'gamma_3p', 'generalized_extreme_value', 'generalized_gamma', 'generalized_gamma_4p', 'generalized_logistic', 'generalized_normal', 'generalized_pareto', 'gibrat', 'gumbel_left', 'gumbel_right', 'half_normal', 'hyperbolic_secant', 'inverse_gamma', 'inverse_gamma_3p', 'inverse_gaussian', 'inverse_gaussian_3p', 'johnson_sb', 'johnson_su', 'kumaraswamy', 'laplace', 'levy', 'loggamma', 'logistic', 'loglogistic', 'loglogistic_3p', 'lognormal', 'maxwell', 'moyal', 'nakagami', 'non_central_chi_square', 'non_central_f', 'non_central_t_student', 'normal', 'pareto_first_kind', 'pareto_second_kind', 'pert', 'power_function', 'rayleigh', 'reciprocal', 'rice', 'semicircular', 'trapezoidal', 'triangular', 't_student', 't_student_3p', 'uniform', 'weibull', 'weibull_3p', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. + parameters (dict): Parameters of the probability distribution. + process_id (str): Unique name of the process to be simulated + number_of_products (int ,optional): Number of elements that are need to simulate in that stage. Value has to be greater than 0. Defaults equals to 1. + new_branch (bool ,optional): Required if you want to start a new process that does not have previous processes. You cannot use this parameter at the same time with "previous_id". Defaults to False. + previous_id (list[str], optional): Required if you have previous processes that are before this process. You cannot use this parameter at the same time with "new_branch". Defaults to None. + """ + + if prob_distribution not in self.probability_distribution.keys(): + raise ValueError( + f"""Please select one of the following probability distributions: '{"', '".join(self.probability_distribution.keys())}'.""" + ) + else: + if process_id not in self.order.keys(): + if number_of_products >= 1: + if new_branch == True and previous_ids != None: + raise ValueError( + f"""You cannot select 'new_branch' is equals to True if 'previous_id' is not empty. OR you cannot add 'previous_ids' if 'new_branch' is equals to True.""" + ) + else: + if new_branch == True: + branch_id = len(self.branches) + self.branches[branch_id] = process_id + self.order[process_id] = branch_id + self.number_of_products[process_id] = number_of_products + self.process_prob_distr[process_id] = ( + self.probability_distribution[prob_distribution]( + parameters + ) + ) + self.next_process[process_id] = 0 + elif previous_ids != None and all( + id in self.order.keys() for id in previous_ids + ): + self.order[process_id] = previous_ids + self.number_of_products[process_id] = number_of_products + self.process_prob_distr[process_id] = ( + self.probability_distribution[prob_distribution]( + parameters + ) + ) + self.next_process[process_id] = 0 + for prev_id in previous_ids: + self.next_process[prev_id] += 1 + else: + raise ValueError( + f"""Please create a new_brach == True if you need a new process or specify the previous process/processes (previous_ids) that are before this one. Processes that have been added: '{"', '".join(self.order.keys())}'.""" + ) + else: + raise ValueError( + f"""You must add number_of_products grater or equals than 1.""" + ) + else: + raise ValueError( + f"""You need to create diferent process_id for each process, '{process_id}' already exists.""" + ) + + def run(self, number_of_simulations: int = 1) -> list[float]: + """Simulation of the described process + + Args: + number_of_simulations (int, optional): Number of simulations of the process that you want to do. Defaults to 1. + + Returns: + list[float]: Results of every simulation requested + """ + simulation_result = list() + for simulation in range(number_of_simulations): + simulation_partial_result = dict() + simulation_accumulative_result = dict() + for key in self.branches.keys(): + partial_result = 0 + for _ in range(self.number_of_products[self.branches[key]]): + partial_result += self.process_prob_distr[self.branches[key]].ppf( + random.random() + ) + simulation_partial_result[self.branches[key]] = partial_result + simulation_accumulative_result[self.branches[key]] = ( + simulation_partial_result[self.branches[key]] + ) + for key in self.process_prob_distr.keys(): + if isinstance(self.order[key], list): + partial_result = 0 + for _ in range(self.number_of_products[key]): + partial_result += self.process_prob_distr[key].ppf( + random.random() + ) + simulation_partial_result[key] = partial_result + simulation_accumulative_result[key] = ( + simulation_partial_result[key] + + simulation_accumulative_result[ + max(self.order[key], key=simulation_accumulative_result.get) + ] + ) + simulation_result.append( + simulation_accumulative_result[ + max( + simulation_accumulative_result.keys(), + key=simulation_accumulative_result.get, + ) + ] + ) + return simulation_result + + def run_confidence_interval( + self, + confidence_level: float = 0.95, + number_of_simulations: int = 1, + replications: int = 30, + ) -> tuple[float]: + """Generates a confidence interval for the replications of the requested number of simulations. + + Args: + confidence_level (float, optional): Confidence required of the interval. Defaults to 0.95. + number_of_simulations (int, optional): Number of simulations that are going to be run in each replication. Defaults to 1. + replications (int, optional): Number of samples needed. Defaults to 30. + + Returns: + tuple[float]: Returns the lower bound, average, upper bound and standard deviation of the confidence interval + """ + # Simulate with replications + average_results_simulations = [ + np.mean(self.run(number_of_simulations)) for _ in range(replications) + ] + # Confidence Interval + ## Sample standard deviation + standar_deviation = np.std(average_results_simulations, ddof=1) + standard_error = standar_deviation / math.sqrt(replications) + average = np.mean(average_results_simulations) + normal_standard = phitter.continuous.NORMAL({"mu": 0, "sigma": 1}) + z = normal_standard.ppf((1 + confidence_level) / 2) + ## Confidence Interval + upper_bound = average + (z * standard_error) + lower_bound = average - (z * standard_error) + # Return confidence interval + return lower_bound, average, upper_bound, standar_deviation + + def process_graph( + self, graph_direction: str = "LR", save_graph_pdf: bool = False + ) -> None: + """Generates the graph of the process + + Args: + graph_direction (str, optional): You can show the graph in two ways: 'LR' left to right OR 'TB' top to bottom. Defaults to 'LR'. + save_graph_pdf (bool, optional): You can save the process graph in a PDF file. Defaults to False. + """ + graph = Digraph(comment="Simulation Process Steb-by-Step") + + for node in set(self.order.keys()): + print(node) + if isinstance(self.order[node], int): + graph.node( + node, node, shape="circle", style="filled", fillcolor="lightgreen" + ) + elif self.next_process[node] == 0: + graph.node( + node, + node, + shape="doublecircle", + style="filled", + fillcolor="lightblue", + ) + else: + graph.node(node, node, shape="box") + + for node in set(self.order.keys()): + if isinstance(self.order[node], list): + for previous_node in self.order[node]: + graph.edge( + previous_node, + node, + label=str(self.number_of_products[previous_node]), + fontsize="10", + ) + + if graph_direction == "TB": + graph.attr(rankdir="TB") + else: + graph.attr(rankdir="LR") + + if save_graph_pdf: + graph.render("Simulation Process Steb-by-Step", view=True) + + display(graph) From 6a1303ceda4b5c8eaa82a0d148962077d4275cb7 Mon Sep 17 00:00:00 2001 From: cargar_github Date: Wed, 9 Oct 2024 18:38:25 -0500 Subject: [PATCH 02/11] Adding Own Distribution, Queueing Simulation and Changing Simulation to ProcessSimulation --- .../own_distribution/own_distribution.py | 63 ++ .../process_simulation/process_simulation.py | 2 +- .../queueing_simulation.py | 939 ++++++++++++++++++ 3 files changed, 1003 insertions(+), 1 deletion(-) create mode 100644 phitter/simulation/own_distribution/own_distribution.py create mode 100644 phitter/simulation/queueing_simulation/queueing_simulation.py diff --git a/phitter/simulation/own_distribution/own_distribution.py b/phitter/simulation/own_distribution/own_distribution.py new file mode 100644 index 0000000..85efe0d --- /dev/null +++ b/phitter/simulation/own_distribution/own_distribution.py @@ -0,0 +1,63 @@ +import random +import numpy as np + + +class OwnDistributions: + def __init__(self, parameters): + self.__parameters = parameters + + self.__first_verification() + + self.__acummulative_parameters = dict() + acum = 0 + for key in self.__parameters.keys(): + acum += self.__parameters[key] + self.__acummulative_parameters[key] = acum + + self.__error_detections() + + def __first_verification(self): + # Quick verification + if isinstance(self.__parameters, dict) == False: + raise ValueError("You must pass a dictionary") + + for key in self.__parameters.keys(): + if isinstance(key, int) == False: + raise ValueError( + f"""All keys must be integers greater or equal than 0.""" + ) + elif key < 0: + raise ValueError( + f"""All keys must be integers greater or equal than 0.""" + ) + + if isinstance(self.__parameters[key], float) == False: + raise ValueError(f"""All keys must be floats.""") + + def __error_detections(self): + + for key in self.__parameters.keys(): + if self.__parameters[key] <= 0 or self.__parameters[key] >= 1: + raise ValueError( + f"""All probabilities must be greater than 0 and less than 1. You have a value of {self.__parameters[key]} for key {key}""" + ) + + if ( + self.__acummulative_parameters[key] > 1 + or self.__acummulative_parameters[key] <= 0 + ): + raise ValueError( + f"""All probabilities must be add up to 1 and must be greater than 0. You have a acummulative value of {self.__acummulative_parameters[key]}""" + ) + else: + last = self.__acummulative_parameters[key] + + if last != 1: + raise ValueError( + f"""All probabilities must be add up to 1, your probabilities sum a total of {last}""" + ) + + def ppf(self, probability: int): + for label in self.__acummulative_parameters.keys(): + if probability <= self.__acummulative_parameters[label]: + return label diff --git a/phitter/simulation/process_simulation/process_simulation.py b/phitter/simulation/process_simulation/process_simulation.py index 73ecb8d..159c955 100644 --- a/phitter/simulation/process_simulation/process_simulation.py +++ b/phitter/simulation/process_simulation/process_simulation.py @@ -6,7 +6,7 @@ from IPython.display import display -class Simulation: +class ProcessSimulation: def __init__(self) -> None: self.process_prob_distr = dict() diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py new file mode 100644 index 0000000..b8de8b2 --- /dev/null +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -0,0 +1,939 @@ +import phitter +import pandas as pd +import random +import numpy as np + + +class QueueingSimulation: + def __init__( + self, + a: str, + a_parameters: dict, + s: str, + s_parameters: dict, + c: int, + k: float = float("inf"), + n: float = float("inf"), + d: str = "FIFO", + pbs_distribution: str | None = None, + pbs_parameters: dict | None = None, + ) -> None: + """Simulation of any queueing model. + + Args: + a (str): Arrival time distribution (Arrival). Distributions that can be used: 'alpha', 'arcsine', 'argus', 'beta', 'beta_prime', 'beta_prime_4p', 'bradford', 'burr', 'burr_4p', 'cauchy', 'chi_square', 'chi_square_3p', 'dagum', 'dagum_4p', 'erlang', 'erlang_3p', 'error_function', 'exponential', 'exponential_2p', 'f', 'fatigue_life', 'folded_normal', 'frechet', 'f_4p', 'gamma', 'gamma_3p', 'generalized_extreme_value', 'generalized_gamma', 'generalized_gamma_4p', 'generalized_logistic', 'generalized_normal', 'generalized_pareto', 'gibrat', 'gumbel_left', 'gumbel_right', 'half_normal', 'hyperbolic_secant', 'inverse_gamma', 'inverse_gamma_3p', 'inverse_gaussian', 'inverse_gaussian_3p', 'johnson_sb', 'johnson_su', 'kumaraswamy', 'laplace', 'levy', 'loggamma', 'logistic', 'loglogistic', 'loglogistic_3p', 'lognormal', 'maxwell', 'moyal', 'nakagami', 'non_central_chi_square', 'non_central_f', 'non_central_t_student', 'normal', 'pareto_first_kind', 'pareto_second_kind', 'pert', 'power_function', 'rayleigh', 'reciprocal', 'rice', 'semicircular', 'trapezoidal', 'triangular', 't_student', 't_student_3p', 'uniform', 'weibull', 'weibull_3p', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. + a_parameters (dict): All the needed parameters to use the probability distribution of the arrival time. + s (str): Service time distribution (Service). Distributions that can be used: 'alpha', 'arcsine', 'argus', 'beta', 'beta_prime', 'beta_prime_4p', 'bradford', 'burr', 'burr_4p', 'cauchy', 'chi_square', 'chi_square_3p', 'dagum', 'dagum_4p', 'erlang', 'erlang_3p', 'error_function', 'exponential', 'exponential_2p', 'f', 'fatigue_life', 'folded_normal', 'frechet', 'f_4p', 'gamma', 'gamma_3p', 'generalized_extreme_value', 'generalized_gamma', 'generalized_gamma_4p', 'generalized_logistic', 'generalized_normal', 'generalized_pareto', 'gibrat', 'gumbel_left', 'gumbel_right', 'half_normal', 'hyperbolic_secant', 'inverse_gamma', 'inverse_gamma_3p', 'inverse_gaussian', 'inverse_gaussian_3p', 'johnson_sb', 'johnson_su', 'kumaraswamy', 'laplace', 'levy', 'loggamma', 'logistic', 'loglogistic', 'loglogistic_3p', 'lognormal', 'maxwell', 'moyal', 'nakagami', 'non_central_chi_square', 'non_central_f', 'non_central_t_student', 'normal', 'pareto_first_kind', 'pareto_second_kind', 'pert', 'power_function', 'rayleigh', 'reciprocal', 'rice', 'semicircular', 'trapezoidal', 'triangular', 't_student', 't_student_3p', 'uniform', 'weibull', 'weibull_3p', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. + s_parameters (dict): All the needed parameters to use the probability distribution of the service time. + c (int): Number of servers. This represents the total number of service channels available in the system. It indicates how many customers can be served simultaneously, affecting the system's capacity to handle incoming clients and impacting metrics like waiting times and queue lengths. + k (float, optional): Maximum system capacity. This is the maximum number of customers that the system can accommodate at any given time, including both those in service and those waiting in the queue. It defines the limit beyond which arriving customers are either turned away or blocked from entering the system. Defaults to float("inf"). + n (float, optional): Total population of potential customers. This denotes the overall number of potential customers who might require service from the system. It can be finite or infinite and affects the arrival rates and the modeling of the system, especially in closed queueing networks. Defaults to float("inf"). + d (str, optional): Queue discipline. This describes the rule or policy that determines the order in which customers are served. Common disciplines include First-In-First-Out ("FIFO"), Last-In-First-Out ("LIFO"), priority-based service ("PBS"). The queue discipline impacts waiting times and the overall fairness of the system.. Defaults to "FIFO". + simulation_time (float, optional): This variable defines the total duration of the simulation. It sets the length of time over which the simulation will model the system's behavior. Defaults to float("inf") + number_of_simulations (int, optional): Number of simulations of the process. Can also be considered as the number of days or number of times you want to simulate your scenario. Defaults to 1. + pbs_distribution (str | None, optional): Discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". Distributions that can be used: 'own_distribution', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. Defaults to None. + pbs_parameters (dict | None, optional): Parameters of the discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". If it is 'own-distribution' add labels in the following way (example): {0: 0.5, 1: 0.3, 2: 0.2}. Where the "key" corresponds to the label and the "value" the probability whose total sum must add up to 1; "keys" with greater importances are the smallers and always have to be numeric keys. You can add as labels as you need. + """ + + self.__probability_distribution = ( + phitter.continuous.CONTINUOUS_DISTRIBUTIONS + | phitter.discrete.DISCRETE_DISTRIBUTIONS + ) + + self.__pbs_distribution = { + "own_distribution": OwnDistributions + } | phitter.discrete.DISCRETE_DISTRIBUTIONS + + self.__queue_discipline = ["FIFO", "LIFO", "PBS"] + + self.__verify_variables(a, s, c, k, n, d, pbs_distribution, pbs_parameters) + + self.__a = self.__probability_distribution[a](a_parameters) + self.__s = self.__probability_distribution[s](s_parameters) + self.__c = c + self.__k = k + self.__n = n + self.__d = d + + if d == "PBS": + self.__label = self.__pbs_distribution[pbs_distribution](pbs_parameters) + + def __verify_variables( + self, + a: str, + s: str, + c: int, + k: float, + n: float, + d: str, + pbs_distribution: str, + pbs_parameters: dict, + ) -> None: + """Verify if the variable value is correct + + Args: + a (str): Arrival time distribution (Arrival). + s (str): Service time distribution (Service). + c (int): Number of servers. + k (float): Maximum system capacity. + n (float): Total population of potential customers. + d (str): Queue discipline. + pbs_distribution (str): Label Distribution. + pbs_parameters (dict): Parameters of the PBS Distribution + """ + + # Verify if a and s belong to a actual probability distribution + if ( + a not in self.__probability_distribution.keys() + or s not in self.__probability_distribution.keys() + ): + raise ValueError( + f"""Please select one of the following probability distributions: '{"', '".join(self.__probability_distribution.keys())}'.""" + ) + # Verify Number of Servers + if c <= 0: + raise ValueError( + f"""'c' has to be a number and cannot be less or equals than zero.""" + ) + # Verify Maximum System Capacity + if k <= 0: + raise ValueError( + f"""'k' has to be a number and cannot be less or equals than zero.""" + ) + # Verify Total population of potential customers. + if n <= 0: + raise ValueError( + f"""'n' has to be a number and cannot be less or equals than zero.""" + ) + + if d not in self.__queue_discipline: + raise ValueError( + f"""'d' has to be one of the following queue discipline: '{"', '".join(self.__queue_discipline)}'.""" + ) + + if k < c: + raise ValueError(f"""'k' cannot be less than the number of servers (c)""") + + if d == "PBS": + if pbs_distribution != None and pbs_parameters != None: + if pbs_distribution not in self.__pbs_distribution: + raise ValueError( + f"""You should select one of the following distributions: {self.__pbs_distribution}""" + ) + elif pbs_distribution == None and pbs_parameters == None: + raise ValueError( + f"""You must include 'pbs_distribution' and 'pbs_parameters' if you want to use 'PBS'.""" + ) + elif d != "PBS" and (pbs_distribution != None or pbs_parameters != None): + raise ValueError( + f"""You can only use 'pbs_distribution' and 'pbs_parameters' with 'd="PBS"'""" + ) + + def run( + self, simulation_time: float = float("inf"), number_of_simulations: int = 1 + ) -> tuple: + """Simulation of any queueing model. + + Args: + simulation_time (float, optional): This variable defines the total duration of the simulation. It sets the length of time over which the simulation will model the system's behavior. Defaults to float("inf") + number_of_simulations (int, optional): Number of simulations of the process. Can also be considered as the number of days or number of times you want to simulate your scenario. Defaults to 1. + + Returns: + tuple: [description] + """ + if simulation_time <= 0: + raise ValueError( + f"""'simulation_time' has to be a number and cannot be less or equals than zero.""" + ) + + if number_of_simulations <= 0: + raise ValueError( + f"""'number_of_simulations' has to be a number and cannot be less or equals than zero.""" + ) + + if simulation_time == float("inf"): + # Big number for the actual probabilities + simulation_time = round(self.__a.ppf(0.9999)) * 1000000 + + # Results Dictionary - initialized with everything equals to 0 + simulation_results = dict() + simulation_results["Attention Order"] = [0] + simulation_results["Arrival Time"] = [0] + simulation_results["Total Number of people"] = [0] + simulation_results["Number of people in Line"] = [0] + simulation_results["Time in Line"] = [0] + simulation_results["Time in service"] = [0] + simulation_results["Leave Time"] = [0] + simulation_results["Join the system?"] = [0] + # Servers Information + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"] = [0] + + if self.__d == "FIFO": + simulation_results = self.__fifo(simulation_time, simulation_results) + elif self.__d == "LIFO": + simulation_results = self.__lifo(simulation_time, simulation_results) + elif self.__d == "PBS": + simulation_results = self.__pbs(simulation_time, simulation_results) + + return simulation_results + + def __last_not_null(self, array): + for element in reversed(array): + if not np.isnan(element): + return element + + def __fifo(self, simulation_time, simulation_results): + arrivals = list() + arriving_time = 0 + population = 0 + # Determine all the arrival hours + while arriving_time < simulation_time and population < self.__n: + arrivals.append(self.__a.ppf(random.random())) + arriving_time += arrivals[-1] + population += 1 + + for arrival in arrivals: + simulation_results["Arrival Time"].append( + simulation_results["Arrival Time"][-1] + arrival + ) + + # Number of people at that time + number_of_people = 0 + start = simulation_results["Arrival Time"][-1] + for other_person in range(len(simulation_results["Arrival Time"]) - 1): + if ( + simulation_results["Arrival Time"][other_person] <= start + and simulation_results["Leave Time"][other_person] >= start + ): + number_of_people += 1 + # Plus one means that person in the system + simulation_results["Total Number of people"].append(number_of_people + 1) + + if simulation_results["Total Number of people"][-1] <= self.__c: + simulation_results["Number of people in Line"].append(0) + else: + simulation_results["Number of people in Line"].append( + simulation_results["Total Number of people"][-1] - self.__c + ) + + if simulation_results["Total Number of people"][-1] <= self.__k: + + # JOin the system + simulation_results["Join the system?"].append(1) + + # Attention order + simulation_results["Attention Order"].append( + max(simulation_results["Attention Order"]) + 1 + ) + + # Review shortest time among all servers and choosing the first server that is available + first_server_available = 0 + first_server_available_time = float("Inf") + for server in range(1, self.__c + 1): + last_time_server_not_null = self.__last_not_null( + simulation_results[f"Time busy server {server}"] + ) + if ( + last_time_server_not_null + <= simulation_results["Arrival Time"][-1] + ): + first_server_available = server + first_server_available_time = last_time_server_not_null + break + elif last_time_server_not_null < first_server_available_time: + first_server_available = server + first_server_available_time = last_time_server_not_null + + simulation_results["Time in Line"].append( + max( + first_server_available_time, + simulation_results["Arrival Time"][-1], + ) + - simulation_results["Arrival Time"][-1] + ) + simulation_results["Time in service"].append( + self.__s.ppf(random.random()) + ) + simulation_results["Leave Time"].append( + simulation_results["Arrival Time"][-1] + + simulation_results["Time in Line"][-1] + + simulation_results["Time in service"][-1] + ) + simulation_results[f"Time busy server {first_server_available}"].append( + simulation_results["Leave Time"][-1] + ) + + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + if server != first_server_available: + simulation_results[f"Time busy server {server}"].append( + simulation_results[f"Time busy server {server}"][-1] + ) + else: + simulation_results["Join the system?"].append(0) + simulation_results["Attention Order"].append(np.nan) + simulation_results["Time in Line"].append(np.nan) + simulation_results["Time in service"].append(np.nan) + simulation_results["Leave Time"].append(np.nan) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(np.nan) + + return simulation_results + + def __lifo(self, simulation_time, simulation_results): + arrivals = list() + arriving_time = 0 + population = 0 + + # Dictionary to identify the order, we initialize it with the first row that also is the first one attended (everything it's zero) + order_idx = {0: 0} + + # Determine all the arrival hours + while arriving_time < simulation_time and population < self.__n: + arrivals.append(self.__a.ppf(random.random())) + arriving_time += arrivals[-1] + population += 1 + + for arrival in arrivals: + # Review time of arrival + simulation_results["Arrival Time"].append( + simulation_results["Arrival Time"][-1] + arrival + ) + + # Last person that was served + last_attended = max(simulation_results["Attention Order"]) + + # If person that arrives time is greater than end of services of at least one machine, we can review people in line or this person to take the service, if not, go to the line + go_to_queue = True + for server in range(1, self.__c + 1): + if ( + simulation_results["Arrival Time"][-1] + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + ): + first_server_available = server + first_server_available_time = simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + go_to_queue = False + break + + if go_to_queue == True: + + ## We should verify the number of people including him!! + + # Number of people at that time + number_of_people = len( + list( + filter( + lambda x: True if x == -1 else False, + simulation_results["Attention Order"], + ) + ) + ) + # Plus one means that person that has just arrived into the system + simulation_results["Number of people in Line"].append( + number_of_people + 1 + ) + # Total people + simulation_results["Total Number of people"].append( + simulation_results["Number of people in Line"][-1] + self.__c + ) + + # Can that person enter? + if simulation_results["Total Number of people"][-1] <= self.__k: + # Add that person into the queue + + simulation_results["Join the system?"].append(1) + simulation_results["Attention Order"].append(-1) + simulation_results["Time in Line"].append(-1) + simulation_results["Time in service"].append(-1) + simulation_results["Leave Time"].append(-1) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(-1) + # if not + else: + simulation_results["Join the system?"].append(0) + simulation_results["Attention Order"].append(np.nan) + simulation_results["Time in Line"].append(np.nan) + simulation_results["Time in service"].append(np.nan) + simulation_results["Leave Time"].append(np.nan) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(np.nan) + + else: + + ## We need to send the last element that arrived before him to the service, if there was nobody, we send the current person + + number_of_people = len( + list( + filter( + lambda x: True if x == -1 else False, + simulation_results["Attention Order"], + ) + ) + ) + + if number_of_people == 0: + # Plus one means that person that has just arrived into the system + simulation_results["Number of people in Line"].append( + number_of_people + ) + simulation_results["Join the system?"].append(1) + + # Attention position + simulation_results["Attention Order"].append( + max(simulation_results["Attention Order"]) + 1 + ) + + # Add the service time and additional information + simulation_results["Time in Line"].append( + max( + first_server_available_time, + simulation_results["Arrival Time"][-1], + ) + - simulation_results["Arrival Time"][-1] + ) + simulation_results["Time in service"].append( + self.__s.ppf(random.random()) + ) + simulation_results["Leave Time"].append( + simulation_results["Arrival Time"][-1] + + simulation_results["Time in Line"][-1] + + simulation_results["Time in service"][-1] + ) + simulation_results[ + f"Time busy server {first_server_available}" + ].append(simulation_results["Leave Time"][-1]) + + # Keep same finish time to other servers + people_being_served = 0 + for server in range(1, self.__c + 1): + if server != first_server_available: + simulation_results[f"Time busy server {server}"].append( + max(simulation_results[f"Time busy server {server}"]) + ) + if ( + simulation_results[f"Time busy server {server}"][-1] + >= simulation_results["Arrival Time"][-1] + ): + people_being_served += 1 + else: + people_being_served += 1 + + simulation_results["Total Number of people"].append( + people_being_served + ) + + order_idx[max(simulation_results["Attention Order"])] = ( + len(simulation_results["Attention Order"]) - 1 + ) + + else: + + # pendiente logica de varias personas y varias maquinas para asignar, la personas de los ultimos a primeros y cada que pase una persona se debe ver si hay maquinas disponibles antes de que llegue la persona de este punto, ahi muere y se manda a esta persona a la fila y se continua el proceso + + for idx in range( + len(simulation_results["Attention Order"]) - 1, -1, -1 + ): + if simulation_results["Attention Order"][idx] == -1: + + min_time = float("Inf") + no_servers_available = True + # Search in all Servers which is available + for server in range(1, self.__c + 1): + # Review if servers are available (before last arrival (-1)) + if ( + simulation_results["Arrival Time"][-1] + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + and min_time + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + ): + # Find Server number and time + first_server_available = server + first_server_available_time = simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + min_time = first_server_available_time + # Let's review again + no_servers_available = False + + if no_servers_available == True: + break + + else: + + ## Assigning + # Attention position + simulation_results["Attention Order"][idx] = ( + max(simulation_results["Attention Order"]) + 1 + ) + + # Add the service time and additional information + simulation_results["Time in Line"][idx] = ( + max( + first_server_available_time, + simulation_results["Arrival Time"][idx], + ) + - simulation_results["Arrival Time"][idx] + ) + simulation_results["Time in service"][idx] = ( + self.__s.ppf(random.random()) + ) + simulation_results["Leave Time"][idx] = ( + simulation_results["Arrival Time"][idx] + + simulation_results["Time in Line"][idx] + + simulation_results["Time in service"][idx] + ) + simulation_results[ + f"Time busy server {first_server_available}" + ][idx] = simulation_results["Leave Time"][idx] + + # Keep same finish time to other servers + for others_servers in range(1, self.__c + 1): + if others_servers != first_server_available: + simulation_results[ + f"Time busy server {others_servers}" + ][idx] = max( + simulation_results[ + f"Time busy server {others_servers}" + ] + ) + + # Assign last attended as this one + last_attended = max( + simulation_results["Attention Order"] + ) + order_idx[last_attended] = idx + + # New number of people after assignig to machines + number_of_people = len( + list( + filter( + lambda x: True if x == -1 else False, + simulation_results["Attention Order"], + ) + ) + ) + + ## Add to the queue + # Add last person into the queue + # Plus one means that person that has just arrived into the system + value_to_add = 1 + people_being_served = 0 + for server in range(1, self.__c + 1): + # Review if servers are available (before last arrival (-1)) we do not add a "1" in Number of people in line + if ( + simulation_results["Arrival Time"][-1] + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + ): + value_to_add = 0 + elif ( + simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + >= simulation_results["Arrival Time"][-1] + ): + people_being_served += 1 + + simulation_results["Number of people in Line"].append( + number_of_people + value_to_add + ) + simulation_results["Total Number of people"].append( + simulation_results["Number of people in Line"][-1] + + people_being_served + + 1 + if value_to_add == 0 + else simulation_results["Number of people in Line"][-1] + + people_being_served + ) + simulation_results["Join the system?"].append(1) + simulation_results["Attention Order"].append(-1) + simulation_results["Time in Line"].append(-1) + simulation_results["Time in service"].append(-1) + simulation_results["Leave Time"].append(-1) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(-1) + + ## Last people to assign + + last_attended = max(simulation_results["Attention Order"]) + for idx in range(len(simulation_results["Attention Order"]) - 1, -1, -1): + if simulation_results["Attention Order"][idx] == -1: + + min_time = float("Inf") + # Search in all Servers which is available + for server in range(1, self.__c + 1): + # Review if servers are available (before last arrival (-1)) + if ( + min_time + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + ): + # Find Server number and time + min_time = simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + first_server_available = server + first_server_available_time = simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + + ## Assigning + # Attention position + simulation_results["Attention Order"][idx] = ( + max(simulation_results["Attention Order"]) + 1 + ) + + # Add the service time and additional information + simulation_results["Time in Line"][idx] = ( + max( + first_server_available_time, + simulation_results["Arrival Time"][idx], + ) + - simulation_results["Arrival Time"][idx] + ) + simulation_results["Time in service"][idx] = self.__s.ppf( + random.random() + ) + simulation_results["Leave Time"][idx] = ( + simulation_results["Arrival Time"][idx] + + simulation_results["Time in Line"][idx] + + simulation_results["Time in service"][idx] + ) + simulation_results[f"Time busy server {first_server_available}"][ + idx + ] = simulation_results["Leave Time"][idx] + + # Keep same finish time to other servers + for others_servers in range(1, self.__c + 1): + if others_servers != first_server_available: + simulation_results[f"Time busy server {others_servers}"][ + idx + ] = max( + simulation_results[f"Time busy server {others_servers}"] + ) + + # Assign last attended as this one + last_attended = max(simulation_results["Attention Order"]) + order_idx[last_attended] = idx + + return simulation_results + + def __pbs(self, simulation_time, simulation_results): + + arrivals = list() + all_priorities = list() + arriving_time = 0 + population = 0 + simulation_results["Priority"] = [0] + + # Dictionary to identify the order, we initialize it with the first row that also is the first one attended (everything it's zero) + order_idx = {0: 0} + + # Determine all the arrival hours + while arriving_time < simulation_time and population < self.__n: + arrivals.append(self.__a.ppf(random.random())) + all_priorities.append(self.__label.ppf(random.random())) + arriving_time += arrivals[-1] + population += 1 + + for index_arrival, arrival in enumerate(arrivals): + simulation_results["Arrival Time"].append( + simulation_results["Arrival Time"][-1] + arrival + ) + simulation_results["Priority"].append(all_priorities[index_arrival]) + + # Number of people at that time + number_of_people = 0 + start = simulation_results["Arrival Time"][-1] + + for other_person in range(len(simulation_results["Arrival Time"]) - 1): + if ( + simulation_results["Arrival Time"][other_person] <= start + and simulation_results["Leave Time"][other_person] >= start + ): + number_of_people += 1 + elif simulation_results["Attention Order"][other_person] == -1: + number_of_people += 1 + + # Plus one means that person in the system + simulation_results["Total Number of people"].append(number_of_people + 1) + + if simulation_results["Total Number of people"][-1] <= self.__c: + simulation_results["Number of people in Line"].append(0) + else: + simulation_results["Number of people in Line"].append( + simulation_results["Total Number of people"][-1] - self.__c + ) + + if simulation_results["Total Number of people"][-1] <= self.__k: + + # JOin the system + simulation_results["Join the system?"].append(1) + + # Last person that was served + last_attended = max(simulation_results["Attention Order"]) + + # If person that arrives time is greater than end of services of at least one machine, we can review people in line or this person to take the service, if not, go to the line + go_to_queue = True + for server in range(1, self.__c + 1): + if ( + simulation_results["Arrival Time"][-1] + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + ): + first_server_available = server + first_server_available_time = simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + go_to_queue = False + break + + if go_to_queue == True: + + # Add that person into the queue + + simulation_results["Attention Order"].append(-1) + simulation_results["Time in Line"].append(-1) + simulation_results["Time in service"].append(-1) + simulation_results["Leave Time"].append(-1) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(-1) + + else: + + if simulation_results["Number of people in Line"][-1] == 0: + + # Attention position + simulation_results["Attention Order"].append( + max(simulation_results["Attention Order"]) + 1 + ) + + # Add the service time and additional information + simulation_results["Time in Line"].append( + max( + first_server_available_time, + simulation_results["Arrival Time"][-1], + ) + - simulation_results["Arrival Time"][-1] + ) + simulation_results["Time in service"].append( + self.__s.ppf(random.random()) + ) + simulation_results["Leave Time"].append( + simulation_results["Arrival Time"][-1] + + simulation_results["Time in Line"][-1] + + simulation_results["Time in service"][-1] + ) + simulation_results[ + f"Time busy server {first_server_available}" + ].append(simulation_results["Leave Time"][-1]) + + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + if server != first_server_available: + simulation_results[f"Time busy server {server}"].append( + max( + simulation_results[f"Time busy server {server}"] + ) + ) + + order_idx[max(simulation_results["Attention Order"])] = ( + len(simulation_results["Attention Order"]) - 1 + ) + + else: + + # Add last person into the queue - This part helps to identify if the person needs to be attended, they coulb be attended first if priority is the highest + + simulation_results["Attention Order"].append(-1) + simulation_results["Time in Line"].append(-1) + simulation_results["Time in service"].append(-1) + simulation_results["Leave Time"].append(-1) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(-1) + + # Bigger numbers are first priority, smaller numbers are less priority + priority_list = sorted( + list(set(simulation_results["Priority"])), reverse=True + ) + + for priority in priority_list: + for idx in range( + len(simulation_results["Attention Order"]) + ): + if ( + simulation_results["Attention Order"][idx] == -1 + and simulation_results["Priority"][idx] == priority + ): + + min_time = float("Inf") + no_servers_available = True + # Search in all Servers which is available + for server in range(1, self.__c + 1): + # Review if servers are available (before last arrival (-1)) + if ( + simulation_results["Arrival Time"][-1] + > simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + and min_time + > simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + ): + # Find Server number and time + first_server_available = server + first_server_available_time = ( + simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + ) + min_time = first_server_available_time + # Let's review again + no_servers_available = False + + if no_servers_available == True: + break + + else: + + ## Assigning + # Attention position + simulation_results["Attention Order"][idx] = ( + max(simulation_results["Attention Order"]) + + 1 + ) + + # Add the service time and additional information + simulation_results["Time in Line"][idx] = ( + max( + first_server_available_time, + simulation_results["Arrival Time"][idx], + ) + - simulation_results["Arrival Time"][idx] + ) + simulation_results["Time in service"][idx] = ( + self.__s.ppf(random.random()) + ) + simulation_results["Leave Time"][idx] = ( + simulation_results["Arrival Time"][idx] + + simulation_results["Time in Line"][idx] + + simulation_results["Time in service"][idx] + ) + simulation_results[ + f"Time busy server {first_server_available}" + ][idx] = simulation_results["Leave Time"][idx] + + # Keep same finish time to other servers + for others_servers in range(1, self.__c + 1): + if others_servers != first_server_available: + simulation_results[ + f"Time busy server {others_servers}" + ][idx] = max( + simulation_results[ + f"Time busy server {others_servers}" + ] + ) + + # Assign last attended as this one + last_attended = max( + simulation_results["Attention Order"] + ) + order_idx[last_attended] = idx + + else: + simulation_results["Join the system?"].append(0) + simulation_results["Attention Order"].append(np.nan) + simulation_results["Time in Line"].append(np.nan) + simulation_results["Time in service"].append(np.nan) + simulation_results["Leave Time"].append(np.nan) + # Keep same finish time to other servers + for server in range(1, self.__c + 1): + simulation_results[f"Time busy server {server}"].append(np.nan) + + ## Assign Missing elements + # Bigger numbers are first priority, smaller numbers are less priority + priority_list = sorted(list(set(simulation_results["Priority"])), reverse=True) + + for priority in priority_list: + for idx in range(len(simulation_results["Attention Order"])): + if ( + simulation_results["Attention Order"][idx] == -1 + and simulation_results["Priority"][idx] == priority + ): + + min_time = float("Inf") + # Search in all Servers which is available + for server in range(1, self.__c + 1): + # Review if servers are available (before last arrival (-1)) + if ( + min_time + > simulation_results[f"Time busy server {server}"][ + order_idx[last_attended] + ] + ): + # Find Server number and time + first_server_available = server + first_server_available_time = simulation_results[ + f"Time busy server {server}" + ][order_idx[last_attended]] + min_time = first_server_available_time + + ## Assigning + # Attention position + simulation_results["Attention Order"][idx] = ( + max(simulation_results["Attention Order"]) + 1 + ) + + # Add the service time and additional information + simulation_results["Time in Line"][idx] = ( + max( + first_server_available_time, + simulation_results["Arrival Time"][idx], + ) + - simulation_results["Arrival Time"][idx] + ) + simulation_results["Time in service"][idx] = self.__s.ppf( + random.random() + ) + simulation_results["Leave Time"][idx] = ( + simulation_results["Arrival Time"][idx] + + simulation_results["Time in Line"][idx] + + simulation_results["Time in service"][idx] + ) + simulation_results[f"Time busy server {first_server_available}"][ + idx + ] = simulation_results["Leave Time"][idx] + + # Keep same finish time to other servers + for others_servers in range(1, self.__c + 1): + if others_servers != first_server_available: + simulation_results[f"Time busy server {others_servers}"][ + idx + ] = max( + simulation_results[f"Time busy server {others_servers}"] + ) + + # Assign last attended as this one + last_attended = max(simulation_results["Attention Order"]) + order_idx[last_attended] = idx + + return simulation_results From a26ea7ea0651087f22c704e708cfc6febe048ebd Mon Sep 17 00:00:00 2001 From: cargar_github Date: Wed, 9 Oct 2024 23:30:26 -0500 Subject: [PATCH 03/11] Adding Additional Functions to QueueSim --- .../queueing_simulation.py | 132 +++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py index b8de8b2..28b4d64 100644 --- a/phitter/simulation/queueing_simulation/queueing_simulation.py +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -2,6 +2,7 @@ import pandas as pd import random import numpy as np +import collections class QueueingSimulation: @@ -58,6 +59,19 @@ def __init__( if d == "PBS": self.__label = self.__pbs_distribution[pbs_distribution](pbs_parameters) + self.result_simulation = pd.DataFrame() + self.__simulation_time = 0 + self.number_probabilities = dict() + + def __str__(self): + return self.__result_simulation.to_string() + + def _repr_html_(self): + return self.__result_simulation._repr_html_() + + def __getitem__(self, key): + return self.__result_simulation[key] + def __verify_variables( self, a: str, @@ -141,6 +155,9 @@ def run( Returns: tuple: [description] """ + + self.__simulation_time = simulation_time + if simulation_time <= 0: raise ValueError( f"""'simulation_time' has to be a number and cannot be less or equals than zero.""" @@ -176,7 +193,15 @@ def run( elif self.__d == "PBS": simulation_results = self.__pbs(simulation_time, simulation_results) - return simulation_results + self.__result_simulation = pd.DataFrame(simulation_results) + self.__result_simulation = self.__result_simulation.drop(index=0).reset_index( + drop=True + ) + + # Probabilities + self.elements_prob() + + return self.__result_simulation def __last_not_null(self, array): for element in reversed(array): @@ -937,3 +962,108 @@ def __pbs(self, simulation_time, simulation_results): order_idx[last_attended] = idx return simulation_results + + def to_csv(self, file_name: str, index: bool = True): + if len(self.__result_simulation) == 0: + raise ValueError(f"""You need to run the simulation to use this""") + else: + self.__result_simulation.to_csv(file_name, index=index) + + def to_excel(self, file_name: str, sheet_name: str = "Sheet1", index: bool = True): + if len(self.__result_simulation) == 0: + raise ValueError(f"""You need to run the simulation to use this""") + else: + self.__result_simulation.to_excel( + file_name, index=index, sheet_name=sheet_name + ) + + def system_utilization(self): + return ( + self.__result_simulation["Time in service"].sum() / self.__simulation_time + ) + + def no_clients_prob(self): + return ( + 1 + - self.__result_simulation["Time in service"].sum() / self.__simulation_time + ) + + def elements_prob(self, bins: int = 50000): + + multiplier = 1 + step = 0 + while step < 1: + # Range to determine probabilities + max_val = int(self.__result_simulation["Leave Time"].max() * multiplier) + min_val = int(self.__result_simulation["Arrival Time"].min() * multiplier) + step = int((max_val - min_val) / bins) + if step < 1: + multiplier = multiplier * 10 + + # Definimos un rango de tiempos para analizar la cantidad de clientes en el sistema + time_points = [round(t, 2) for t in range(min_val, max_val, step)] + + # Number of clients in system in each instant + customers_at_time = [ + ( + (self.__result_simulation["Arrival Time"] <= t / 100) + & (self.__result_simulation["Leave Time"] >= t / 100) + ).sum() + for t in time_points + ] + + # Count a number of times each number appears + count_customers = collections.Counter(customers_at_time) + + # Calculate probability per number of customer + total_points = len(time_points) + self.number_probabilities = { + k: v / total_points for k, v in count_customers.items() + } + + return self.number_probabilities + + def number_elements_prob(self, number: int, prob_type: str): + if isinstance(number, int) == False: + raise ValueError(f"""number can only be integer""") + + if prob_type == "exact_value": + return self.number_probabilities[number] + elif prob_type == "greater_equals": + return sum( + [ + self.number_probabilities[key] + for key in self.number_probabilities.keys() + if key >= number + ] + ) + elif prob_type == "less_equals": + return sum( + [ + self.number_probabilities[key] + for key in self.number_probabilities.keys() + if key <= number + ] + ) + else: + raise ValueError( + f"""You can only select one of the following prob_type: 'exact_value', 'greater_equals', 'less_equals'""" + ) + + def average_time_system(self): + return ( + self.__result_simulation["Time in service"] + + self.__result_simulation["Time in Line"] + ).mean() + + def average_time_queue(self): + return self.__result_simulation["Time in Line"].mean() + + def average_elements_system(self): + return ( + self.__result_simulation["Time in service"] + + self.__result_simulation["Time in Line"] + ).sum() / self.__simulation_time + + def average_elements_queue(self): + return (self.__result_simulation["Time in Line"]).sum() / self.__simulation_time From 9c678e4b7c8095ae1a3c16b3214404981d2ae35a Mon Sep 17 00:00:00 2001 From: cargar_github Date: Thu, 10 Oct 2024 22:21:05 -0500 Subject: [PATCH 04/11] =?UTF-8?q?Simulation=20Finished=20without=20comment?= =?UTF-8?q?s=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../queueing_simulation.py | 226 ++++++++++++++++-- 1 file changed, 211 insertions(+), 15 deletions(-) diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py index 28b4d64..3555e86 100644 --- a/phitter/simulation/queueing_simulation/queueing_simulation.py +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -4,6 +4,8 @@ import numpy as np import collections +import math + class QueueingSimulation: def __init__( @@ -41,7 +43,7 @@ def __init__( | phitter.discrete.DISCRETE_DISTRIBUTIONS ) - self.__pbs_distribution = { + self.__pbs_distributions = { "own_distribution": OwnDistributions } | phitter.discrete.DISCRETE_DISTRIBUTIONS @@ -49,15 +51,24 @@ def __init__( self.__verify_variables(a, s, c, k, n, d, pbs_distribution, pbs_parameters) - self.__a = self.__probability_distribution[a](a_parameters) - self.__s = self.__probability_distribution[s](s_parameters) + self.__save_a = a + self.__save_a_params = a_parameters + self.__save_s = s + self.__save_s_params = s_parameters + + self.__a = self.__probability_distribution[self.__save_a](self.__save_a_params) + self.__s = self.__probability_distribution[self.__save_s](self.__save_s_params) self.__c = c self.__k = k self.__n = n self.__d = d + self.__pbs_parameters = pbs_parameters + self.__pbs_distribution = pbs_distribution if d == "PBS": - self.__label = self.__pbs_distribution[pbs_distribution](pbs_parameters) + self.__label = self.__pbs_distributions[self.__pbs_distribution]( + self.__pbs_parameters + ) self.result_simulation = pd.DataFrame() self.__simulation_time = 0 @@ -130,9 +141,9 @@ def __verify_variables( if d == "PBS": if pbs_distribution != None and pbs_parameters != None: - if pbs_distribution not in self.__pbs_distribution: + if pbs_distribution not in self.__pbs_distributions: raise ValueError( - f"""You should select one of the following distributions: {self.__pbs_distribution}""" + f"""You should select one of the following distributions: {self.__pbs_distributions}""" ) elif pbs_distribution == None and pbs_parameters == None: raise ValueError( @@ -143,9 +154,7 @@ def __verify_variables( f"""You can only use 'pbs_distribution' and 'pbs_parameters' with 'd="PBS"'""" ) - def run( - self, simulation_time: float = float("inf"), number_of_simulations: int = 1 - ) -> tuple: + def run(self, simulation_time: float = float("inf")) -> tuple: """Simulation of any queueing model. Args: @@ -163,11 +172,6 @@ def run( f"""'simulation_time' has to be a number and cannot be less or equals than zero.""" ) - if number_of_simulations <= 0: - raise ValueError( - f"""'number_of_simulations' has to be a number and cannot be less or equals than zero.""" - ) - if simulation_time == float("inf"): # Big number for the actual probabilities simulation_time = round(self.__a.ppf(0.9999)) * 1000000 @@ -193,12 +197,17 @@ def run( elif self.__d == "PBS": simulation_results = self.__pbs(simulation_time, simulation_results) + # Create a new column that is the same for all d + simulation_results["Finish after closed"] = [ + 1 if leave_time > self.__simulation_time else 0 + for leave_time in simulation_results["Leave Time"] + ] + self.__result_simulation = pd.DataFrame(simulation_results) self.__result_simulation = self.__result_simulation.drop(index=0).reset_index( drop=True ) - # Probabilities self.elements_prob() return self.__result_simulation @@ -1059,6 +1068,21 @@ def average_time_system(self): def average_time_queue(self): return self.__result_simulation["Time in Line"].mean() + def average_time_service(self): + return self.__result_simulation["Time in service"].mean() + + def standard_deviation_time_system(self): + return ( + self.__result_simulation["Time in service"] + + self.__result_simulation["Time in Line"] + ).std() + + def standard_deviation_time_queue(self): + return self.__result_simulation["Time in Line"].std() + + def standard_deviation_time_service(self): + return self.__result_simulation["Time in service"].std() + def average_elements_system(self): return ( self.__result_simulation["Time in service"] @@ -1067,3 +1091,175 @@ def average_elements_system(self): def average_elements_queue(self): return (self.__result_simulation["Time in Line"]).sum() / self.__simulation_time + + def probability_to_join_system(self): + return (self.__result_simulation["Join the system?"]).sum() / len( + self.__result_simulation + ) + + def probability_to_finish_after_time(self): + return (self.__result_simulation["Finish after closed"]).sum() / len( + self.__result_simulation + ) + + def probability_to_wait_in_line(self): + result = np.where(self.__result_simulation["Time in Line"] > 0, 1, 0) + + return result.sum() / len(self.__result_simulation) + + def number_probability_summary(self): + + options = ["less_equals", "exact_value", "greater_equals"] + + dictionaty_number = { + key: [self.number_elements_prob(int(key), option) for option in options] + for key in self.number_probabilities.keys() + } + + df = pd.DataFrame.from_dict(dictionaty_number, orient="index").rename( + columns={ + 0: "Prob. Less or Equals", + 1: "Exact Probability", + 2: "Prob. Greter or equals", + } + ) + + df.index.name = "Number of elements" + + return df.reset_index() + + def metrics_summary(self): + metrics = dict() + metrics["Average Time in System"] = float(self.average_time_system()) + metrics["Average Time in Queue"] = float(self.average_time_queue()) + metrics["Average Time in Service"] = float(self.average_time_service()) + metrics["Std. Dev. Time in System"] = float( + self.standard_deviation_time_system() + ) + metrics["Std. Dev. Time in Queue"] = float(self.standard_deviation_time_queue()) + metrics["Std. Dev. Time in Service"] = float( + self.standard_deviation_time_service() + ) + metrics["Average Elements in System"] = float(self.average_elements_system()) + metrics["Average Elements in Queue"] = float(self.average_elements_queue()) + metrics["Probability to join the System"] = float( + self.probability_to_join_system() + ) + metrics["Probability to finish after Time"] = float( + self.probability_to_finish_after_time() + ) + metrics["Probability to Wait in Line"] = float( + self.probability_to_wait_in_line() + ) + + df = pd.DataFrame.from_dict(metrics, orient="index").rename( + columns={0: "Value"} + ) + + df.index.name = "Metrics" + + return df.reset_index() + + def confidence_interval_metrics( + self, simulation_time: int, confidence_level: int = 0.95, replications: int = 30 + ) -> tuple[pd.DataFrame, pd.DataFrame]: + tot_prob = pd.DataFrame() + tot_metrics = pd.DataFrame() + for _ in range(replications): + self.__init__( + self.__save_a, + self.__save_a_params, + self.__save_s, + self.__save_s_params, + self.__c, + self.__k, + self.__n, + self.__d, + self.__pbs_distribution, + self.__pbs_parameters, + ) + self.run(simulation_time) + number_probability_summary = self.number_probability_summary() + metrics_summary = self.metrics_summary() + tot_prob = pd.concat([tot_prob, number_probability_summary]) + tot_metrics = pd.concat([tot_metrics, metrics_summary]) + + # First Interval + std__ = tot_prob.groupby(["Number of elements"]).std() + mean__ = tot_prob.groupby(["Number of elements"]).mean() + + standard_error = std__ / math.sqrt(replications) + normal_standard = phitter.continuous.NORMAL({"mu": 0, "sigma": 1}) + z = normal_standard.ppf((1 + confidence_level) / 2) + ## Confidence Interval + avg = mean__.copy() + lower_bound = ( + (mean__ - (z * standard_error)) + .copy() + .rename( + columns={ + "Prob. Less or Equals": "LB - Prob. Less or Equals", + "Exact Probability": "LB - Exact Probability", + "Prob. Greter or equals": "LB - Prob. Greater or equals", + } + ) + ) + upper_bound = ( + (mean__ + (z * standard_error)) + .copy() + .rename( + columns={ + "Prob. Less or Equals": "UB - Prob. Less or Equals", + "Exact Probability": "UB - Exact Probability", + "Prob. Greter or equals": "UB - Prob. Greater or equals", + } + ) + ) + avg = avg.rename( + columns={ + "Prob. Less or Equals": "AVG - Prob. Less or Equals", + "Exact Probability": "AVG - Exact Probability", + "Prob. Greter or equals": "AVG - Prob. Greater or equals", + } + ) + tot_prob_interval = pd.concat([lower_bound, avg, upper_bound], axis=1) + tot_prob_interval = tot_prob_interval[ + [ + "LB - Prob. Less or Equals", + "AVG - Prob. Less or Equals", + "UB - Prob. Less or Equals", + "LB - Exact Probability", + "AVG - Exact Probability", + "UB - Exact Probability", + "LB - Prob. Greater or equals", + "AVG - Prob. Greater or equals", + "UB - Prob. Greater or equals", + ] + ] + + # Second Interval + std__2 = tot_metrics.groupby(["Metrics"]).std() + mean__2 = tot_metrics.groupby(["Metrics"]).mean() + + standard_error = std__2 / math.sqrt(replications) + normal_standard = phitter.continuous.NORMAL({"mu": 0, "sigma": 1}) + z = normal_standard.ppf((1 + confidence_level) / 2) + ## Confidence Interval + avg__2 = mean__2.copy() + lower_bound = ( + (mean__2 - (z * standard_error)) + .copy() + .rename(columns={"Value": "LB - Value"}) + ) + upper_bound = ( + (mean__2 + (z * standard_error)) + .copy() + .rename(columns={"Value": "UB - Value"}) + ) + avg__2 = avg__2.rename(columns={"Value": "AVG - Value"}) + tot_metrics_interval = pd.concat([lower_bound, avg__2, upper_bound], axis=1) + tot_metrics_interval = tot_metrics_interval[ + ["LB - Value", "AVG - Value", "UB - Value"] + ] + + return tot_prob_interval.reset_index(), tot_metrics_interval.reset_index() From 5e3eb8f12730d18d10216c97db243ce4d4364383 Mon Sep 17 00:00:00 2001 From: cargar_github Date: Fri, 11 Oct 2024 23:28:28 -0500 Subject: [PATCH 05/11] =?UTF-8?q?simulation=20with=20comments=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../own_distribution/own_distribution.py | 23 +- .../queueing_simulation.py | 336 +++++++++++++++--- 2 files changed, 301 insertions(+), 58 deletions(-) diff --git a/phitter/simulation/own_distribution/own_distribution.py b/phitter/simulation/own_distribution/own_distribution.py index 85efe0d..b1cd129 100644 --- a/phitter/simulation/own_distribution/own_distribution.py +++ b/phitter/simulation/own_distribution/own_distribution.py @@ -3,7 +3,12 @@ class OwnDistributions: - def __init__(self, parameters): + def __init__(self, parameters: dict): + """Creates the "OwnDistributions" Class + + Args: + parameters (dict): Parameters of that distribution. all keys should be numbers greater or equals than zero. All values must sum up to 1. + """ self.__parameters = parameters self.__first_verification() @@ -16,7 +21,8 @@ def __init__(self, parameters): self.__error_detections() - def __first_verification(self): + def __first_verification(self) -> None: + """Verify if the keys are integers greater than zero, and verify if all values are floats.""" # Quick verification if isinstance(self.__parameters, dict) == False: raise ValueError("You must pass a dictionary") @@ -34,7 +40,8 @@ def __first_verification(self): if isinstance(self.__parameters[key], float) == False: raise ValueError(f"""All keys must be floats.""") - def __error_detections(self): + def __error_detections(self) -> None: + """Identify the values that are greater than 1 or less than 0. Verify if accumulative probabilities are less or greater than 1. Must sum 1""" for key in self.__parameters.keys(): if self.__parameters[key] <= 0 or self.__parameters[key] >= 1: @@ -57,7 +64,15 @@ def __error_detections(self): f"""All probabilities must be add up to 1, your probabilities sum a total of {last}""" ) - def ppf(self, probability: int): + def ppf(self, probability: int) -> int: + """Assign a label according to a probability given by the created distribution + + Args: + probability (int): Number between 0 and 1 + + Returns: + int: Returns label according to probability + """ for label in self.__acummulative_parameters.keys(): if probability <= self.__acummulative_parameters[label]: return label diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py index 3555e86..d487776 100644 --- a/phitter/simulation/queueing_simulation/queueing_simulation.py +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -38,24 +38,28 @@ def __init__( pbs_parameters (dict | None, optional): Parameters of the discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". If it is 'own-distribution' add labels in the following way (example): {0: 0.5, 1: 0.3, 2: 0.2}. Where the "key" corresponds to the label and the "value" the probability whose total sum must add up to 1; "keys" with greater importances are the smallers and always have to be numeric keys. You can add as labels as you need. """ + # All phitter probability distributions self.__probability_distribution = ( phitter.continuous.CONTINUOUS_DISTRIBUTIONS | phitter.discrete.DISCRETE_DISTRIBUTIONS ) + # Distributions for labels self.__pbs_distributions = { "own_distribution": OwnDistributions } | phitter.discrete.DISCRETE_DISTRIBUTIONS - + # Queue discipline self.__queue_discipline = ["FIFO", "LIFO", "PBS"] - + # Verify if all variables are correct self.__verify_variables(a, s, c, k, n, d, pbs_distribution, pbs_parameters) + # Saving input variables self.__save_a = a self.__save_a_params = a_parameters self.__save_s = s self.__save_s_params = s_parameters + # Variables input for simualtion self.__a = self.__probability_distribution[self.__save_a](self.__save_a_params) self.__s = self.__probability_distribution[self.__save_s](self.__save_s_params) self.__c = c @@ -63,6 +67,10 @@ def __init__( self.__n = n self.__d = d + # This variables are assigned later + self.__simulation_time = 0 + + # PBS parameters self.__pbs_parameters = pbs_parameters self.__pbs_distribution = pbs_distribution if d == "PBS": @@ -70,14 +78,25 @@ def __init__( self.__pbs_parameters ) + # Simulation results self.result_simulation = pd.DataFrame() self.__simulation_time = 0 self.number_probabilities = dict() - def __str__(self): + def __str__(self) -> str: + """Print dataset + + Returns: + str: Dataframe in str mode + """ return self.__result_simulation.to_string() - def _repr_html_(self): + def _repr_html_(self) -> pd.DataFrame: + """Print DataFrames in jupyter notebooks + + Returns: + pd.DataFrame: Simulation result + """ return self.__result_simulation._repr_html_() def __getitem__(self, key): @@ -103,8 +122,8 @@ def __verify_variables( k (float): Maximum system capacity. n (float): Total population of potential customers. d (str): Queue discipline. - pbs_distribution (str): Label Distribution. - pbs_parameters (dict): Parameters of the PBS Distribution + pbs_distribution (str): Label Distribution. It can only be used if d='PBS' is selected + pbs_parameters (dict): Parameters of the PBS Distribution. It can only be used if d='PBS' is selected """ # Verify if a and s belong to a actual probability distribution @@ -131,30 +150,36 @@ def __verify_variables( f"""'n' has to be a number and cannot be less or equals than zero.""" ) + # Review if the discipline is in the list if d not in self.__queue_discipline: raise ValueError( f"""'d' has to be one of the following queue discipline: '{"', '".join(self.__queue_discipline)}'.""" ) + # Maximum number of people should be greater or equals to the number of servers if k < c: raise ValueError(f"""'k' cannot be less than the number of servers (c)""") + # PBS logic if d == "PBS": if pbs_distribution != None and pbs_parameters != None: + # Review if the selected distribution was created if pbs_distribution not in self.__pbs_distributions: raise ValueError( f"""You should select one of the following distributions: {self.__pbs_distributions}""" ) + # Review if PBS is selected if the distribution and parameters exits elif pbs_distribution == None and pbs_parameters == None: raise ValueError( f"""You must include 'pbs_distribution' and 'pbs_parameters' if you want to use 'PBS'.""" ) + # You can only use this two parameters if PBS is selected elif d != "PBS" and (pbs_distribution != None or pbs_parameters != None): raise ValueError( f"""You can only use 'pbs_distribution' and 'pbs_parameters' with 'd="PBS"'""" ) - def run(self, simulation_time: float = float("inf")) -> tuple: + def run(self, simulation_time: float = float("inf")) -> pd.DataFrame: """Simulation of any queueing model. Args: @@ -165,13 +190,16 @@ def run(self, simulation_time: float = float("inf")) -> tuple: tuple: [description] """ + # Create a new variable with the simulation time self.__simulation_time = simulation_time + # Simulation time has to be greater than 0 if simulation_time <= 0: raise ValueError( f"""'simulation_time' has to be a number and cannot be less or equals than zero.""" ) + # If it is infinity, create a massive number if simulation_time == float("inf"): # Big number for the actual probabilities simulation_time = round(self.__a.ppf(0.9999)) * 1000000 @@ -186,10 +214,11 @@ def run(self, simulation_time: float = float("inf")) -> tuple: simulation_results["Time in service"] = [0] simulation_results["Leave Time"] = [0] simulation_results["Join the system?"] = [0] - # Servers Information + # Servers Information - Depends on the number in self.__c for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"] = [0] + # Selections according to the queue discipline if self.__d == "FIFO": simulation_results = self.__fifo(simulation_time, simulation_results) elif self.__d == "LIFO": @@ -197,37 +226,61 @@ def run(self, simulation_time: float = float("inf")) -> tuple: elif self.__d == "PBS": simulation_results = self.__pbs(simulation_time, simulation_results) - # Create a new column that is the same for all d + # Create a new column that is the same for all queue disciplines, to determine if that person was attended after the "close time" simulation_results["Finish after closed"] = [ 1 if leave_time > self.__simulation_time else 0 for leave_time in simulation_results["Leave Time"] ] + # Convert result to a DataFrame self.__result_simulation = pd.DataFrame(simulation_results) self.__result_simulation = self.__result_simulation.drop(index=0).reset_index( drop=True ) + # Calculte probabilities for each number of elements self.elements_prob() return self.__result_simulation - def __last_not_null(self, array): + def __last_not_null(self, array: list) -> int: + """Reviews all elements and returns the last one that is not null + + Args: + array (list): Array that we need to identify the last element no null + + Returns: + int: last element no null + """ for element in reversed(array): if not np.isnan(element): return element - def __fifo(self, simulation_time, simulation_results): + def __fifo(self, simulation_time: int, simulation_results: dict) -> dict: + """Simulation of the FIFO queue discipline according to all input parameters + + Args: + simulation_time (int): Max. Time the simulation would take + simulation_results (dict): Dictionary where all answers will be stored + + Returns: + dict: Returns simulation_results input variable with all the calculations + """ + + # Initialize some variable for this simulation arrivals = list() arriving_time = 0 population = 0 - # Determine all the arrival hours + + # Determine all the arrival hours if the number of people do not exceed the maximum or the arriving time is less than the Max. Simulation Time while arriving_time < simulation_time and population < self.__n: arrivals.append(self.__a.ppf(random.random())) arriving_time += arrivals[-1] population += 1 + # Start simulation for each arrival for arrival in arrivals: + # Include that person time in the result simulation_results["Arrival Time"].append( simulation_results["Arrival Time"][-1] + arrival ) @@ -244,6 +297,7 @@ def __fifo(self, simulation_time, simulation_results): # Plus one means that person in the system simulation_results["Total Number of people"].append(number_of_people + 1) + # Calculte the number of people in line at that time if simulation_results["Total Number of people"][-1] <= self.__c: simulation_results["Number of people in Line"].append(0) else: @@ -251,9 +305,10 @@ def __fifo(self, simulation_time, simulation_results): simulation_results["Total Number of people"][-1] - self.__c ) + # Verify if the number of people is less or equals the max value if simulation_results["Total Number of people"][-1] <= self.__k: - # JOin the system + # Join the system simulation_results["Join the system?"].append(1) # Attention order @@ -279,6 +334,7 @@ def __fifo(self, simulation_time, simulation_results): first_server_available = server first_server_available_time = last_time_server_not_null + # Assign the time in line if the arrival time is less the available server then he needed to do the line simulation_results["Time in Line"].append( max( first_server_available_time, @@ -286,14 +342,17 @@ def __fifo(self, simulation_time, simulation_results): ) - simulation_results["Arrival Time"][-1] ) + # Simulate time in service simulation_results["Time in service"].append( self.__s.ppf(random.random()) ) + # Leave time of that person simulation_results["Leave Time"].append( simulation_results["Arrival Time"][-1] + simulation_results["Time in Line"][-1] + simulation_results["Time in service"][-1] ) + # Same as leave time is the max time busy server simulation_results[f"Time busy server {first_server_available}"].append( simulation_results["Leave Time"][-1] ) @@ -305,6 +364,7 @@ def __fifo(self, simulation_time, simulation_results): simulation_results[f"Time busy server {server}"][-1] ) else: + # If the number of people is greater than the maximum allowed, then do not include any stat to that person simulation_results["Join the system?"].append(0) simulation_results["Attention Order"].append(np.nan) simulation_results["Time in Line"].append(np.nan) @@ -316,20 +376,32 @@ def __fifo(self, simulation_time, simulation_results): return simulation_results - def __lifo(self, simulation_time, simulation_results): + def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: + """Simulation of the LIFO queue discipline according to all input parameters + + Args: + simulation_time (int): Max. Time the simulation would take + simulation_results (dict): Dictionary where all answers will be stored + + Returns: + dict: Returns simulation_results input variable with all the calculations + """ + + # Initialize some variable for this simulation arrivals = list() arriving_time = 0 population = 0 - # Dictionary to identify the order, we initialize it with the first row that also is the first one attended (everything it's zero) + # Dictionary to identify the order, we initialize it with the first row (value) that also is the first one attended (key) (everything it's zero) order_idx = {0: 0} - # Determine all the arrival hours + # Determine all the arrival hours if the number of people do not exceed the maximum or the arriving time is less than the Max. Simulation Time while arriving_time < simulation_time and population < self.__n: arrivals.append(self.__a.ppf(random.random())) arriving_time += arrivals[-1] population += 1 + # Start simulation for each arrival for arrival in arrivals: # Review time of arrival simulation_results["Arrival Time"].append( @@ -355,6 +427,7 @@ def __lifo(self, simulation_time, simulation_results): go_to_queue = False break + # If he needs to go to the line if go_to_queue == True: ## We should verify the number of people including him!! @@ -379,8 +452,7 @@ def __lifo(self, simulation_time, simulation_results): # Can that person enter? if simulation_results["Total Number of people"][-1] <= self.__k: - # Add that person into the queue - + # Add that person into the queue (send to queue = -1) simulation_results["Join the system?"].append(1) simulation_results["Attention Order"].append(-1) simulation_results["Time in Line"].append(-1) @@ -391,6 +463,7 @@ def __lifo(self, simulation_time, simulation_results): simulation_results[f"Time busy server {server}"].append(-1) # if not else: + # If the number of people is greater than the maximum allowed, then do not include any stat to that person simulation_results["Join the system?"].append(0) simulation_results["Attention Order"].append(np.nan) simulation_results["Time in Line"].append(np.nan) @@ -402,8 +475,8 @@ def __lifo(self, simulation_time, simulation_results): else: - ## We need to send the last element that arrived before him to the service, if there was nobody, we send the current person - + ## We need to send the last element that arrived before him to the service, if there was nobody, we send the current person. + # Number of people at that time number_of_people = len( list( filter( @@ -412,7 +485,7 @@ def __lifo(self, simulation_time, simulation_results): ) ) ) - + # If there is nobody, we assign that element. if number_of_people == 0: # Plus one means that person that has just arrived into the system simulation_results["Number of people in Line"].append( @@ -460,21 +533,24 @@ def __lifo(self, simulation_time, simulation_results): else: people_being_served += 1 + # Number of people at that time simulation_results["Total Number of people"].append( people_being_served ) - + # Update order order_idx[max(simulation_results["Attention Order"])] = ( len(simulation_results["Attention Order"]) - 1 ) else: - # pendiente logica de varias personas y varias maquinas para asignar, la personas de los ultimos a primeros y cada que pase una persona se debe ver si hay maquinas disponibles antes de que llegue la persona de este punto, ahi muere y se manda a esta persona a la fila y se continua el proceso + # The people go from last to first, and each time a person passes, it must be checked if there are machines available before the person reaches this point. If not, the process stops, and this person is sent to the queue, and the process continues. + # Review from the end to the beggining without the element that has just arrived for idx in range( len(simulation_results["Attention Order"]) - 1, -1, -1 ): + # If that element has not been served if simulation_results["Attention Order"][idx] == -1: min_time = float("Inf") @@ -549,6 +625,7 @@ def __lifo(self, simulation_time, simulation_results): ) order_idx[last_attended] = idx + ### If there is no more servers available, we review if the person that has just arrived can join the system, if so we send it to the line # New number of people after assignig to machines number_of_people = len( list( @@ -581,6 +658,8 @@ def __lifo(self, simulation_time, simulation_results): ): people_being_served += 1 + ## After all send the last person to the queue (person that has just arrived) + # Assign all to queue simulation_results["Number of people in Line"].append( number_of_people + value_to_add ) @@ -602,9 +681,13 @@ def __lifo(self, simulation_time, simulation_results): simulation_results[f"Time busy server {server}"].append(-1) ## Last people to assign + # After "closing time" there are people that are not assign yet. This logic assign that people + # Identify last person attended last_attended = max(simulation_results["Attention Order"]) + # Iterate from the last one to the first one if there is any element not assigned yet for idx in range(len(simulation_results["Attention Order"]) - 1, -1, -1): + # if that element hasn't been attended yet if simulation_results["Attention Order"][idx] == -1: min_time = float("Inf") @@ -667,24 +750,35 @@ def __lifo(self, simulation_time, simulation_results): return simulation_results - def __pbs(self, simulation_time, simulation_results): + def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: + """Simulation of the PBS queue discipline according to all input parameters + Args: + simulation_time (int): Max. Time the simulation would take + simulation_results (dict): Dictionary where all answers will be stored + + Returns: + dict: Returns simulation_results input variable with all the calculations + """ + # Initialize some variable for this simulation arrivals = list() all_priorities = list() arriving_time = 0 population = 0 + # A new field for the result dictionary is created because of the PBS logic simulation_results["Priority"] = [0] # Dictionary to identify the order, we initialize it with the first row that also is the first one attended (everything it's zero) order_idx = {0: 0} - # Determine all the arrival hours + # Determine all the arrival hours if the number of people do not exceed the maximum or the arriving time is less than the Max. Simulation Time. Also this line determines the priority of each element while arriving_time < simulation_time and population < self.__n: arrivals.append(self.__a.ppf(random.random())) all_priorities.append(self.__label.ppf(random.random())) arriving_time += arrivals[-1] population += 1 + # Start simulation for each arrival for index_arrival, arrival in enumerate(arrivals): simulation_results["Arrival Time"].append( simulation_results["Arrival Time"][-1] + arrival @@ -695,6 +789,7 @@ def __pbs(self, simulation_time, simulation_results): number_of_people = 0 start = simulation_results["Arrival Time"][-1] + # Number of people at that time for other_person in range(len(simulation_results["Arrival Time"]) - 1): if ( simulation_results["Arrival Time"][other_person] <= start @@ -704,9 +799,10 @@ def __pbs(self, simulation_time, simulation_results): elif simulation_results["Attention Order"][other_person] == -1: number_of_people += 1 - # Plus one means that person in the system + # Plus one means that person is in the system simulation_results["Total Number of people"].append(number_of_people + 1) + # Determine the number of people in line at that time if simulation_results["Total Number of people"][-1] <= self.__c: simulation_results["Number of people in Line"].append(0) else: @@ -714,6 +810,7 @@ def __pbs(self, simulation_time, simulation_results): simulation_results["Total Number of people"][-1] - self.__c ) + # Can that person enter? if simulation_results["Total Number of people"][-1] <= self.__k: # JOin the system @@ -738,9 +835,10 @@ def __pbs(self, simulation_time, simulation_results): go_to_queue = False break + # If need to go to the queue if go_to_queue == True: - # Add that person into the queue + # Add that person into the queue (-1 means queue) simulation_results["Attention Order"].append(-1) simulation_results["Time in Line"].append(-1) @@ -752,6 +850,7 @@ def __pbs(self, simulation_time, simulation_results): else: + # IF there is nobody that person can be served if simulation_results["Number of people in Line"][-1] == 0: # Attention position @@ -787,15 +886,15 @@ def __pbs(self, simulation_time, simulation_results): simulation_results[f"Time busy server {server}"] ) ) - + # Update order list order_idx[max(simulation_results["Attention Order"])] = ( len(simulation_results["Attention Order"]) - 1 ) else: - # Add last person into the queue - This part helps to identify if the person needs to be attended, they coulb be attended first if priority is the highest - + ## Add last person into the queue - This part helps to identify if the person needs to be attended, they could be attended first if priority is the highest + # Adding that person into the queue (just to verify priority) simulation_results["Attention Order"].append(-1) simulation_results["Time in Line"].append(-1) simulation_results["Time in service"].append(-1) @@ -809,10 +908,12 @@ def __pbs(self, simulation_time, simulation_results): list(set(simulation_results["Priority"])), reverse=True ) + # Verify priority (starts with bigger numbers) for priority in priority_list: for idx in range( len(simulation_results["Attention Order"]) ): + # Verify if person is in line and its priority if ( simulation_results["Attention Order"][idx] == -1 and simulation_results["Priority"][idx] == priority @@ -893,7 +994,9 @@ def __pbs(self, simulation_time, simulation_results): ) order_idx[last_attended] = idx + # If not else: + # If the number of people is greater than the maximum allowed, then do not include any stat to that person simulation_results["Join the system?"].append(0) simulation_results["Attention Order"].append(np.nan) simulation_results["Time in Line"].append(np.nan) @@ -906,8 +1009,9 @@ def __pbs(self, simulation_time, simulation_results): ## Assign Missing elements # Bigger numbers are first priority, smaller numbers are less priority priority_list = sorted(list(set(simulation_results["Priority"])), reverse=True) - + # Verify priority (starts with bigger numbers) for priority in priority_list: + # Verify if that element has not been attended and if belongs to the actual priority for idx in range(len(simulation_results["Attention Order"])): if ( simulation_results["Attention Order"][idx] == -1 @@ -972,13 +1076,27 @@ def __pbs(self, simulation_time, simulation_results): return simulation_results - def to_csv(self, file_name: str, index: bool = True): + def to_csv(self, file_name: str, index: bool = True) -> None: + """Simulation results to CVS + + Args: + file_name (str): File Name to add to the CSV file. You should include ".csv" at the end of your file + index (bool, optional): Defaults to True. Add index in CSV file. + """ if len(self.__result_simulation) == 0: raise ValueError(f"""You need to run the simulation to use this""") else: self.__result_simulation.to_csv(file_name, index=index) - def to_excel(self, file_name: str, sheet_name: str = "Sheet1", index: bool = True): + def to_excel( + self, file_name: str, sheet_name: str = "Sheet1", index: bool = True + ) -> None: + """Simulation results to Excel File + + Args: + file_name (str): File Name to add to the Excel file. You should include ".xlsx" at the end of your file + index (bool, optional): Defaults to True. Add index in Excel file. + """ if len(self.__result_simulation) == 0: raise ValueError(f"""You need to run the simulation to use this""") else: @@ -986,19 +1104,36 @@ def to_excel(self, file_name: str, sheet_name: str = "Sheet1", index: bool = Tru file_name, index=index, sheet_name=sheet_name ) - def system_utilization(self): + def system_utilization(self) -> float: + """Returns system utilization according to simulation + + Returns: + float: System Utilization + """ return ( self.__result_simulation["Time in service"].sum() / self.__simulation_time ) - def no_clients_prob(self): + def no_clients_prob(self) -> float: + """Probability of no having clients + + Returns: + float: No clients probability + """ return ( 1 - self.__result_simulation["Time in service"].sum() / self.__simulation_time ) - def elements_prob(self, bins: int = 50000): + def elements_prob(self, bins: int = 50000) -> dict: + """Creates the probability for each number of elements. Example: Probability to be 0, prob. to be 1, prob. to be 2... depending on simulation values + + Args: + bins (int, optional): Number of intervals to determine the probability to be in each stage. Defaults to 50000. + Returns: + dict: Element and probability result + """ multiplier = 1 step = 0 while step < 1: @@ -1032,9 +1167,18 @@ def elements_prob(self, bins: int = 50000): return self.number_probabilities - def number_elements_prob(self, number: int, prob_type: str): + def number_elements_prob(self, number: int, prob_type: str) -> float: + """Calculates the probability Exact, less or equals or greater or equals. + + Args: + number (int): Number that we want to identify the different probabilities + prob_type (str): Could be one of the following options: 'exact_value', 'greater_equals', 'less_equals' + + Returns: + float: Probability of the number of elements + """ if isinstance(number, int) == False: - raise ValueError(f"""number can only be integer""") + raise ValueError(f"""Number can only be integer""") if prob_type == "exact_value": return self.number_probabilities[number] @@ -1059,55 +1203,115 @@ def number_elements_prob(self, number: int, prob_type: str): f"""You can only select one of the following prob_type: 'exact_value', 'greater_equals', 'less_equals'""" ) - def average_time_system(self): + def average_time_system(self) -> float: + """Average time in system + + Returns: + float: Average time in system + """ return ( self.__result_simulation["Time in service"] + self.__result_simulation["Time in Line"] ).mean() - def average_time_queue(self): + def average_time_queue(self) -> float: + """Average time in queue + + Returns: + float: Average time in queue + """ return self.__result_simulation["Time in Line"].mean() - def average_time_service(self): + def average_time_service(self) -> float: + """Average time in service + + Returns: + float: Average time in service + """ return self.__result_simulation["Time in service"].mean() - def standard_deviation_time_system(self): + def standard_deviation_time_system(self) -> float: + """Standard Deviation time in system + + Returns: + float: Standard Deviation time in system + """ return ( self.__result_simulation["Time in service"] + self.__result_simulation["Time in Line"] ).std() - def standard_deviation_time_queue(self): + def standard_deviation_time_queue(self) -> float: + """Standard Deviation time in queue + + Returns: + float: Standard Deviation time in queue + """ return self.__result_simulation["Time in Line"].std() - def standard_deviation_time_service(self): + def standard_deviation_time_service(self) -> float: + """Standard Deviation time in service + + Returns: + float: Standard Deviation time in service + """ return self.__result_simulation["Time in service"].std() - def average_elements_system(self): + def average_elements_system(self) -> float: + """Average elements in system + + Returns: + float: Average elements in system + """ return ( self.__result_simulation["Time in service"] + self.__result_simulation["Time in Line"] ).sum() / self.__simulation_time - def average_elements_queue(self): + def average_elements_queue(self) -> float: + """Average elements in queue + + Returns: + float: Average elements in queue + """ return (self.__result_simulation["Time in Line"]).sum() / self.__simulation_time - def probability_to_join_system(self): + def probability_to_join_system(self) -> float: + """Probability to join the system + + Returns: + float: Probability to join the system + """ return (self.__result_simulation["Join the system?"]).sum() / len( self.__result_simulation ) - def probability_to_finish_after_time(self): + def probability_to_finish_after_time(self) -> float: + """Probability to finish after time + + Returns: + float: Probability to finish after time + """ return (self.__result_simulation["Finish after closed"]).sum() / len( self.__result_simulation ) - def probability_to_wait_in_line(self): + def probability_to_wait_in_line(self) -> float: + """Probability to wait in the queue + + Returns: + float: Probability to wait in the queue + """ result = np.where(self.__result_simulation["Time in Line"] > 0, 1, 0) return result.sum() / len(self.__result_simulation) - def number_probability_summary(self): + def number_probability_summary(self) -> pd.DataFrame: + """Returns the probability for each element. The probability is Exact, less or equals or greater or equals; represented in each column. + + Returns: + pd.DataFrame: Dataframe with all the needed probabilities for each element. + """ options = ["less_equals", "exact_value", "greater_equals"] @@ -1128,7 +1332,12 @@ def number_probability_summary(self): return df.reset_index() - def metrics_summary(self): + def metrics_summary(self) -> pd.DataFrame: + """Returns the summary of the following metrics: Average Time in System, Average Time in Queue, Average Time in Service, Std. Dev. Time in System, Std. Dev. Time in Queue, Std. Dev. Time in Service, Average Elements in System, Average Elements in Queue, Probability to join the System, Probability to finish after Time, Probability to Wait in Line + + Returns: + pd.DataFrame: Returns dataframe with all the information + """ metrics = dict() metrics["Average Time in System"] = float(self.average_time_system()) metrics["Average Time in Queue"] = float(self.average_time_queue()) @@ -1161,11 +1370,27 @@ def metrics_summary(self): return df.reset_index() def confidence_interval_metrics( - self, simulation_time: int, confidence_level: int = 0.95, replications: int = 30 + self, + simulation_time: int = float("Inf"), + confidence_level: int = 0.95, + replications: int = 30, ) -> tuple[pd.DataFrame, pd.DataFrame]: + """Generate a confidence interval for probabilities and metrics. + + Args: + simulation_time (int, optional): Simulation time. Defaults to float("Inf) + confidence_level (int, optional): Confidence level for the confidence interval for all the metrics and probabilities. Defaults to 0.95. + replications (int, optional): Number of samples of simulations to create. Defaults to 30. + + Returns: + tuple[pd.DataFrame, pd.DataFrame]: Returns probabilities and metrics dataframe with confidene interval for all metrics. + """ + # Initializa variables tot_prob = pd.DataFrame() tot_metrics = pd.DataFrame() + # Run replications for _ in range(replications): + # Initialize simulation to avoid issues self.__init__( self.__save_a, self.__save_a_params, @@ -1178,13 +1403,16 @@ def confidence_interval_metrics( self.__pbs_distribution, self.__pbs_parameters, ) + # Run simulation self.run(simulation_time) + # Save metrics and probabilities number_probability_summary = self.number_probability_summary() metrics_summary = self.metrics_summary() + # Concat previous results with current results tot_prob = pd.concat([tot_prob, number_probability_summary]) tot_metrics = pd.concat([tot_metrics, metrics_summary]) - # First Interval + # First Confidence Interval std__ = tot_prob.groupby(["Number of elements"]).std() mean__ = tot_prob.groupby(["Number of elements"]).mean() @@ -1237,7 +1465,7 @@ def confidence_interval_metrics( ] ] - # Second Interval + # Second Confidence Interval std__2 = tot_metrics.groupby(["Metrics"]).std() mean__2 = tot_metrics.groupby(["Metrics"]).mean() From 2445c033dede53892bfe079f55aa37be7fa3cdc2 Mon Sep 17 00:00:00 2001 From: cargar_github Date: Mon, 14 Oct 2024 18:11:10 -0500 Subject: [PATCH 06/11] Simulation fixing some issues and adding server utilization --- .../queueing_simulation.py | 145 ++++++++++++++++-- 1 file changed, 128 insertions(+), 17 deletions(-) diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py index d487776..4d81846 100644 --- a/phitter/simulation/queueing_simulation/queueing_simulation.py +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -32,10 +32,8 @@ def __init__( k (float, optional): Maximum system capacity. This is the maximum number of customers that the system can accommodate at any given time, including both those in service and those waiting in the queue. It defines the limit beyond which arriving customers are either turned away or blocked from entering the system. Defaults to float("inf"). n (float, optional): Total population of potential customers. This denotes the overall number of potential customers who might require service from the system. It can be finite or infinite and affects the arrival rates and the modeling of the system, especially in closed queueing networks. Defaults to float("inf"). d (str, optional): Queue discipline. This describes the rule or policy that determines the order in which customers are served. Common disciplines include First-In-First-Out ("FIFO"), Last-In-First-Out ("LIFO"), priority-based service ("PBS"). The queue discipline impacts waiting times and the overall fairness of the system.. Defaults to "FIFO". - simulation_time (float, optional): This variable defines the total duration of the simulation. It sets the length of time over which the simulation will model the system's behavior. Defaults to float("inf") - number_of_simulations (int, optional): Number of simulations of the process. Can also be considered as the number of days or number of times you want to simulate your scenario. Defaults to 1. pbs_distribution (str | None, optional): Discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". Distributions that can be used: 'own_distribution', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. Defaults to None. - pbs_parameters (dict | None, optional): Parameters of the discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". If it is 'own-distribution' add labels in the following way (example): {0: 0.5, 1: 0.3, 2: 0.2}. Where the "key" corresponds to the label and the "value" the probability whose total sum must add up to 1; "keys" with greater importances are the smallers and always have to be numeric keys. You can add as labels as you need. + pbs_parameters (dict | None, optional): Parameters of the discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". If it is 'own-distribution' add labels in the following way (example): {0: 0.5, 1: 0.3, 2: 0.2}. Where the "key" corresponds to the label and the "value" the probability whose total sum must add up to 1; "keys" with greater importances are the greaters and always have to be numeric keys. You can add as labels as you need. """ # All phitter probability distributions @@ -217,6 +215,8 @@ def run(self, simulation_time: float = float("inf")) -> pd.DataFrame: # Servers Information - Depends on the number in self.__c for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"] = [0] + for server in range(1, self.__c + 1): + simulation_results[f"Server {server} attended this element?"] = [0] # Selections according to the queue discipline if self.__d == "FIFO": @@ -274,9 +274,13 @@ def __fifo(self, simulation_time: int, simulation_results: dict) -> dict: # Determine all the arrival hours if the number of people do not exceed the maximum or the arriving time is less than the Max. Simulation Time while arriving_time < simulation_time and population < self.__n: - arrivals.append(self.__a.ppf(random.random())) - arriving_time += arrivals[-1] - population += 1 + arr = self.__a.ppf(random.random()) + if arriving_time + arr < simulation_time: + arrivals.append(arr) + arriving_time += arrivals[-1] + population += 1 + else: + break # Start simulation for each arrival for arrival in arrivals: @@ -356,13 +360,20 @@ def __fifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[f"Time busy server {first_server_available}"].append( simulation_results["Leave Time"][-1] ) + # This server was the enchanged of help the element + simulation_results[ + f"Server {first_server_available} attended this element?" + ].append(1) - # Keep same finish time to other servers + # Keep same finish time to other servers and did not attend those servers for server in range(1, self.__c + 1): if server != first_server_available: simulation_results[f"Time busy server {server}"].append( simulation_results[f"Time busy server {server}"][-1] ) + simulation_results[ + f"Server {server} attended this element?" + ].append(0) else: # If the number of people is greater than the maximum allowed, then do not include any stat to that person simulation_results["Join the system?"].append(0) @@ -370,9 +381,12 @@ def __fifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results["Time in Line"].append(np.nan) simulation_results["Time in service"].append(np.nan) simulation_results["Leave Time"].append(np.nan) - # Keep same finish time to other servers + # Keep same finish time to other servers and np.nan for all servers for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(np.nan) + simulation_results[ + f"Server {server} attended this element?" + ].append(np.nan) return simulation_results @@ -397,9 +411,13 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: # Determine all the arrival hours if the number of people do not exceed the maximum or the arriving time is less than the Max. Simulation Time while arriving_time < simulation_time and population < self.__n: - arrivals.append(self.__a.ppf(random.random())) - arriving_time += arrivals[-1] - population += 1 + arr = self.__a.ppf(random.random()) + if arriving_time + arr < simulation_time: + arrivals.append(arr) + arriving_time += arrivals[-1] + population += 1 + else: + break # Start simulation for each arrival for arrival in arrivals: @@ -461,6 +479,10 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: # Keep same finish time to other servers for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(-1) + # This server was the enchanged of help the element + simulation_results[ + f"Server {server} attended this element?" + ].append(-1) # if not else: # If the number of people is greater than the maximum allowed, then do not include any stat to that person @@ -472,6 +494,10 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: # Keep same finish time to other servers for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(np.nan) + # This server was the enchanged of help the element + simulation_results[ + f"Server {server} attended this element?" + ].append(np.nan) else: @@ -517,6 +543,10 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[ f"Time busy server {first_server_available}" ].append(simulation_results["Leave Time"][-1]) + # This server was the enchanged of help the element + simulation_results[ + f"Server {first_server_available} attended this element?" + ].append(1) # Keep same finish time to other servers people_being_served = 0 @@ -525,6 +555,10 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[f"Time busy server {server}"].append( max(simulation_results[f"Time busy server {server}"]) ) + # This server was the enchanged of help the element + simulation_results[ + f"Server {server} attended this element?" + ].append(0) if ( simulation_results[f"Time busy server {server}"][-1] >= simulation_results["Arrival Time"][-1] @@ -607,6 +641,10 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[ f"Time busy server {first_server_available}" ][idx] = simulation_results["Leave Time"][idx] + # This server was the enchanged of help the element + simulation_results[ + f"Server {first_server_available} attended this element?" + ][idx] = 1 # Keep same finish time to other servers for others_servers in range(1, self.__c + 1): @@ -618,6 +656,10 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: f"Time busy server {others_servers}" ] ) + # This server was the enchanged of help the element + simulation_results[ + f"Server {others_servers} attended this element?" + ][idx] = 0 # Assign last attended as this one last_attended = max( @@ -676,9 +718,12 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results["Time in Line"].append(-1) simulation_results["Time in service"].append(-1) simulation_results["Leave Time"].append(-1) - # Keep same finish time to other servers + # Keep same finish time to other servers and servers have not attended this user for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(-1) + simulation_results[ + f"Server {server} attended this element?" + ].append(-1) ## Last people to assign # After "closing time" there are people that are not assign yet. This logic assign that people @@ -734,6 +779,9 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[f"Time busy server {first_server_available}"][ idx ] = simulation_results["Leave Time"][idx] + simulation_results[ + f"Server {first_server_available} attended this element?" + ][idx] = 1 # Keep same finish time to other servers for others_servers in range(1, self.__c + 1): @@ -743,6 +791,9 @@ def __lifo(self, simulation_time: int, simulation_results: dict) -> dict: ] = max( simulation_results[f"Time busy server {others_servers}"] ) + simulation_results[ + f"Server {others_servers} attended this element?" + ][idx] = 0 # Assign last attended as this one last_attended = max(simulation_results["Attention Order"]) @@ -773,10 +824,14 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: # Determine all the arrival hours if the number of people do not exceed the maximum or the arriving time is less than the Max. Simulation Time. Also this line determines the priority of each element while arriving_time < simulation_time and population < self.__n: - arrivals.append(self.__a.ppf(random.random())) - all_priorities.append(self.__label.ppf(random.random())) - arriving_time += arrivals[-1] - population += 1 + arr = self.__a.ppf(random.random()) + if arriving_time + arr < simulation_time: + arrivals.append(arr) + all_priorities.append(self.__label.ppf(random.random())) + arriving_time += arrivals[-1] + population += 1 + else: + break # Start simulation for each arrival for index_arrival, arrival in enumerate(arrivals): @@ -847,6 +902,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: # Keep same finish time to other servers for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(-1) + simulation_results[ + f"Server {server} attended this element?" + ].append(-1) else: @@ -877,6 +935,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[ f"Time busy server {first_server_available}" ].append(simulation_results["Leave Time"][-1]) + simulation_results[ + f"Server {first_server_available} attended this element?" + ].append(1) # Keep same finish time to other servers for server in range(1, self.__c + 1): @@ -886,6 +947,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[f"Time busy server {server}"] ) ) + simulation_results[ + f"Server {server} attended this element?" + ].append(0) # Update order list order_idx[max(simulation_results["Attention Order"])] = ( len(simulation_results["Attention Order"]) - 1 @@ -902,6 +966,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: # Keep same finish time to other servers for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(-1) + simulation_results[ + f"Server {server} attended this element?" + ].append(-1) # Bigger numbers are first priority, smaller numbers are less priority priority_list = sorted( @@ -976,6 +1043,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[ f"Time busy server {first_server_available}" ][idx] = simulation_results["Leave Time"][idx] + simulation_results[ + f"Server {first_server_available} attended this element?" + ][idx] = 1 # Keep same finish time to other servers for others_servers in range(1, self.__c + 1): @@ -987,6 +1057,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: f"Time busy server {others_servers}" ] ) + simulation_results[ + f"Server {others_servers} attended this element?" + ][idx] = 0 # Assign last attended as this one last_attended = max( @@ -1005,6 +1078,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: # Keep same finish time to other servers for server in range(1, self.__c + 1): simulation_results[f"Time busy server {server}"].append(np.nan) + simulation_results[ + f"Server {server} attended this element?" + ].append(np.nan) ## Assign Missing elements # Bigger numbers are first priority, smaller numbers are less priority @@ -1060,6 +1136,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: simulation_results[f"Time busy server {first_server_available}"][ idx ] = simulation_results["Leave Time"][idx] + simulation_results[ + f"Server {first_server_available} attended this element?" + ][idx] = 1 # Keep same finish time to other servers for others_servers in range(1, self.__c + 1): @@ -1069,6 +1148,9 @@ def __pbs(self, simulation_time: int, simulation_results: dict) -> dict: ] = max( simulation_results[f"Time busy server {others_servers}"] ) + simulation_results[ + f"Server {others_servers} attended this element?" + ][idx] = 0 # Assign last attended as this one last_attended = max(simulation_results["Attention Order"]) @@ -1306,6 +1388,29 @@ def probability_to_wait_in_line(self) -> float: return result.sum() / len(self.__result_simulation) + def servers_utilization(self) -> pd.DataFrame: + """Determine the server utilization according to the simulation result + + Returns: + pd.DataFrame: Utilization of all servers, you can find the server number in the rows + """ + # Calculate server utilization + serv_util_dict = { + f"Utilization Server #{server}": self.__result_simulation[ + f"Server {server} attended this element?" + ].sum() + / len(self.__result_simulation) + for server in range(1, self.__c + 1) + } + # Convert into a DataFrame + df = pd.DataFrame.from_dict(serv_util_dict, orient="index").rename( + columns={0: "Value"} + ) + + df.index.name = "Metrics" + + return df.reset_index() + def number_probability_summary(self) -> pd.DataFrame: """Returns the probability for each element. The probability is Exact, less or equals or greater or equals; represented in each column. @@ -1367,7 +1472,13 @@ def metrics_summary(self) -> pd.DataFrame: df.index.name = "Metrics" - return df.reset_index() + df = df.reset_index() + + df_2 = self.servers_utilization() + + df = pd.concat([df, df_2], axis=0) + + return df.reset_index(drop=True) def confidence_interval_metrics( self, From 603898a704d7499848882b4b7f61b94123d30f84 Mon Sep 17 00:00:00 2001 From: cargar_github Date: Mon, 14 Oct 2024 20:45:27 -0500 Subject: [PATCH 07/11] simulation with tests and all in init --- phitter/__init__.py | 1 + phitter/simulation/__init__.py | 3 + .../simulation/own_distribution/__init__.py | 1 + .../simulation/process_simulation/__init__.py | 1 + .../queueing_simulation/__init__.py | 1 + .../queueing_simulation.py | 2 + .../test_own_dist_and_queue_simulation.ipynb | 1274 +++++++++++++++++ .../simulation/test_process_simulations.ipynb | 108 ++ tests/pytest/test_process_simulation.py | 104 ++ tests/pytest/test_queue_simulation_and_own.py | 48 + 10 files changed, 1543 insertions(+) create mode 100644 phitter/simulation/own_distribution/__init__.py create mode 100644 phitter/simulation/process_simulation/__init__.py create mode 100644 phitter/simulation/queueing_simulation/__init__.py create mode 100644 tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb create mode 100644 tests/phitter_local/simulation/test_process_simulations.ipynb create mode 100644 tests/pytest/test_process_simulation.py create mode 100644 tests/pytest/test_queue_simulation_and_own.py diff --git a/phitter/__init__.py b/phitter/__init__.py index e942af0..18f14f1 100644 --- a/phitter/__init__.py +++ b/phitter/__init__.py @@ -3,3 +3,4 @@ from .main import PHITTER from phitter import continuous from phitter import discrete +from phitter import simulation diff --git a/phitter/simulation/__init__.py b/phitter/simulation/__init__.py index e69de29..a99ce3d 100644 --- a/phitter/simulation/__init__.py +++ b/phitter/simulation/__init__.py @@ -0,0 +1,3 @@ +from phitter.simulation.own_distribution import OwnDistributions +from phitter.simulation.process_simulation import ProcessSimulation +from phitter.simulation.queueing_simulation import QueueingSimulation diff --git a/phitter/simulation/own_distribution/__init__.py b/phitter/simulation/own_distribution/__init__.py new file mode 100644 index 0000000..64b31dd --- /dev/null +++ b/phitter/simulation/own_distribution/__init__.py @@ -0,0 +1 @@ +from .own_distribution import OwnDistributions diff --git a/phitter/simulation/process_simulation/__init__.py b/phitter/simulation/process_simulation/__init__.py new file mode 100644 index 0000000..8244d2b --- /dev/null +++ b/phitter/simulation/process_simulation/__init__.py @@ -0,0 +1 @@ +from .process_simulation import ProcessSimulation diff --git a/phitter/simulation/queueing_simulation/__init__.py b/phitter/simulation/queueing_simulation/__init__.py new file mode 100644 index 0000000..9d72396 --- /dev/null +++ b/phitter/simulation/queueing_simulation/__init__.py @@ -0,0 +1 @@ +from .queueing_simulation import QueueingSimulation diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py index 4d81846..b75282d 100644 --- a/phitter/simulation/queueing_simulation/queueing_simulation.py +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -6,6 +6,8 @@ import math +from phitter.simulation.own_distribution import OwnDistributions + class QueueingSimulation: def __init__( diff --git a/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb b/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb new file mode 100644 index 0000000..13ee9af --- /dev/null +++ b/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb @@ -0,0 +1,1274 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0,\"../../../\")\n", + "import phitter" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Attention OrderArrival TimeTotal Number of peopleNumber of people in LineTime in LineTime in serviceLeave TimeJoin the system?Time busy server 1Time busy server 2Time busy server 3Server 1 attended this element?Server 2 attended this element?Server 3 attended this element?PriorityFinish after closed
010.292612100.00.0039610.29657310.2965730.0000000.00000010000
120.450717100.00.0094280.46014510.4601450.0000000.00000010000
230.947384100.00.0596901.00707411.0070740.0000000.00000010000
340.951672200.00.0434260.99509811.0070740.9950980.00000001000
451.287054100.00.0159091.30296311.3029630.9950980.00000010000
...................................................
10016100171999.017841100.00.0155601999.03340011999.0334001997.5238351994.21502310020
10017100181999.185363100.00.0968091999.28217211999.2821721997.5238351994.21502310010
10018100191999.376887100.00.0133131999.39020011999.3902001997.5238351994.21502310020
10019100201999.917300100.00.0376931999.95499311999.9549931997.5238351994.21502310000
10020100211999.947407200.00.0103831999.95779011999.9549931999.9577901994.21502301000
\n", + "

10021 rows × 16 columns

\n", + "
" + ], + "text/plain": [ + " Attention Order Arrival Time Total Number of people \n", + "0 1 0.292612 1 \\\n", + "1 2 0.450717 1 \n", + "2 3 0.947384 1 \n", + "3 4 0.951672 2 \n", + "4 5 1.287054 1 \n", + "... ... ... ... \n", + "10016 10017 1999.017841 1 \n", + "10017 10018 1999.185363 1 \n", + "10018 10019 1999.376887 1 \n", + "10019 10020 1999.917300 1 \n", + "10020 10021 1999.947407 2 \n", + "\n", + " Number of people in Line Time in Line Time in service Leave Time \n", + "0 0 0.0 0.003961 0.296573 \\\n", + "1 0 0.0 0.009428 0.460145 \n", + "2 0 0.0 0.059690 1.007074 \n", + "3 0 0.0 0.043426 0.995098 \n", + "4 0 0.0 0.015909 1.302963 \n", + "... ... ... ... ... \n", + "10016 0 0.0 0.015560 1999.033400 \n", + "10017 0 0.0 0.096809 1999.282172 \n", + "10018 0 0.0 0.013313 1999.390200 \n", + "10019 0 0.0 0.037693 1999.954993 \n", + "10020 0 0.0 0.010383 1999.957790 \n", + "\n", + " Join the system? Time busy server 1 Time busy server 2 \n", + "0 1 0.296573 0.000000 \\\n", + "1 1 0.460145 0.000000 \n", + "2 1 1.007074 0.000000 \n", + "3 1 1.007074 0.995098 \n", + "4 1 1.302963 0.995098 \n", + "... ... ... ... \n", + "10016 1 1999.033400 1997.523835 \n", + "10017 1 1999.282172 1997.523835 \n", + "10018 1 1999.390200 1997.523835 \n", + "10019 1 1999.954993 1997.523835 \n", + "10020 1 1999.954993 1999.957790 \n", + "\n", + " Time busy server 3 Server 1 attended this element? \n", + "0 0.000000 1 \\\n", + "1 0.000000 1 \n", + "2 0.000000 1 \n", + "3 0.000000 0 \n", + "4 0.000000 1 \n", + "... ... ... \n", + "10016 1994.215023 1 \n", + "10017 1994.215023 1 \n", + "10018 1994.215023 1 \n", + "10019 1994.215023 1 \n", + "10020 1994.215023 0 \n", + "\n", + " Server 2 attended this element? Server 3 attended this element? \n", + "0 0 0 \\\n", + "1 0 0 \n", + "2 0 0 \n", + "3 1 0 \n", + "4 0 0 \n", + "... ... ... \n", + "10016 0 0 \n", + "10017 0 0 \n", + "10018 0 0 \n", + "10019 0 0 \n", + "10020 1 0 \n", + "\n", + " Priority Finish after closed \n", + "0 0 0 \n", + "1 0 0 \n", + "2 0 0 \n", + "3 0 0 \n", + "4 0 0 \n", + "... ... ... \n", + "10016 2 0 \n", + "10017 1 0 \n", + "10018 2 0 \n", + "10019 0 0 \n", + "10020 0 0 \n", + "\n", + "[10021 rows x 16 columns]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parameters={0: 0.5, 1: 0.3, 2: 0.2}\n", + "simulation = phitter.simulation.QueueingSimulation(\"exponential\", {\"lambda\": 5}, \"exponential\", {\"lambda\": 20}, 3, d=\"PBS\", pbs_distribution=\"own_distribution\", pbs_parameters=parameters)\n", + "simulation.run(2000)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "simulation = phitter.simulation.QueueingSimulation(\"exponential\", {\"lambda\": 5}, \"exponential\", {\"lambda\": 20}, 3, d=\"FIFO\")\n", + "a, b = simulation.confidence_interval_metrics(2000, replications=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Number of elementsLB - Prob. Less or EqualsAVG - Prob. Less or EqualsUB - Prob. Less or EqualsLB - Exact ProbabilityAVG - Exact ProbabilityUB - Exact ProbabilityLB - Prob. Greater or equalsAVG - Prob. Greater or equalsUB - Prob. Greater or equals
000.7764050.7785060.7806080.7764050.7785060.7806081.0000001.0000001.000000
110.9724920.9730950.9736980.1930270.1945890.1961510.2193920.2214940.223595
220.9975410.9977230.9979040.0240930.0246280.0251620.0263020.0269050.027508
330.9997750.9998290.9998830.0019450.0021060.0022680.0020960.0022770.002459
440.9999830.9999910.9999990.0001060.0001620.0002180.0001170.0001710.000225
551.0000001.0000001.0000000.0000170.0000230.0000280.0000170.0000230.000028
\n", + "
" + ], + "text/plain": [ + " Number of elements LB - Prob. Less or Equals AVG - Prob. Less or Equals \n", + "0 0 0.776405 0.778506 \\\n", + "1 1 0.972492 0.973095 \n", + "2 2 0.997541 0.997723 \n", + "3 3 0.999775 0.999829 \n", + "4 4 0.999983 0.999991 \n", + "5 5 1.000000 1.000000 \n", + "\n", + " UB - Prob. Less or Equals LB - Exact Probability AVG - Exact Probability \n", + "0 0.780608 0.776405 0.778506 \\\n", + "1 0.973698 0.193027 0.194589 \n", + "2 0.997904 0.024093 0.024628 \n", + "3 0.999883 0.001945 0.002106 \n", + "4 0.999999 0.000106 0.000162 \n", + "5 1.000000 0.000017 0.000023 \n", + "\n", + " UB - Exact Probability LB - Prob. Greater or equals \n", + "0 0.780608 1.000000 \\\n", + "1 0.196151 0.219392 \n", + "2 0.025162 0.026302 \n", + "3 0.002268 0.002096 \n", + "4 0.000218 0.000117 \n", + "5 0.000028 0.000017 \n", + "\n", + " AVG - Prob. Greater or equals UB - Prob. Greater or equals \n", + "0 1.000000 1.000000 \n", + "1 0.221494 0.223595 \n", + "2 0.026905 0.027508 \n", + "3 0.002277 0.002459 \n", + "4 0.000171 0.000225 \n", + "5 0.000023 0.000028 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MetricsLB - ValueAVG - ValueUB - Value
0Average Elements in Queue0.0001390.0001840.000230
1Average Elements in System0.2477740.2506010.253428
2Average Time in Queue0.0000280.0000370.000046
3Average Time in Service0.0496920.0500630.050435
4Average Time in System0.0497280.0501000.050472
5Probability to Wait in Line0.0018640.0021800.002497
6Probability to finish after Time-0.0000100.0000100.000029
7Probability to join the System1.0000001.0000001.000000
8Std. Dev. Time in Queue0.0008190.0010850.001351
9Std. Dev. Time in Service0.0494560.0498300.050203
10Std. Dev. Time in System0.0494670.0498460.050224
11Utilization Server #10.7995330.8012130.802892
12Utilization Server #20.1737560.1748300.175904
13Utilization Server #30.0229730.0239570.024941
\n", + "
" + ], + "text/plain": [ + " Metrics LB - Value AVG - Value UB - Value\n", + "0 Average Elements in Queue 0.000139 0.000184 0.000230\n", + "1 Average Elements in System 0.247774 0.250601 0.253428\n", + "2 Average Time in Queue 0.000028 0.000037 0.000046\n", + "3 Average Time in Service 0.049692 0.050063 0.050435\n", + "4 Average Time in System 0.049728 0.050100 0.050472\n", + "5 Probability to Wait in Line 0.001864 0.002180 0.002497\n", + "6 Probability to finish after Time -0.000010 0.000010 0.000029\n", + "7 Probability to join the System 1.000000 1.000000 1.000000\n", + "8 Std. Dev. Time in Queue 0.000819 0.001085 0.001351\n", + "9 Std. Dev. Time in Service 0.049456 0.049830 0.050203\n", + "10 Std. Dev. Time in System 0.049467 0.049846 0.050224\n", + "11 Utilization Server #1 0.799533 0.801213 0.802892\n", + "12 Utilization Server #2 0.173756 0.174830 0.175904\n", + "13 Utilization Server #3 0.022973 0.023957 0.024941" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Attention OrderArrival TimeTotal Number of peopleNumber of people in LineTime in LineTime in serviceLeave TimeJoin the system?Time busy server 1Time busy server 2Time busy server 3Server 1 attended this element?Server 2 attended this element?Server 3 attended this element?Finish after closed
010.094968100.00.1314810.22644910.2264490.0000000.0000001000
120.435671100.00.1157370.55140810.5514080.0000000.0000001000
230.893621100.00.0482030.94182410.9418240.0000000.0000001000
340.952125100.00.1012601.05338511.0533850.0000000.0000001000
451.000481200.00.0620671.06254811.0533851.0625480.0000000100
................................................
959617.541317200.00.05642117.597738117.56716417.59773810.1893380100
969717.684506100.00.12485217.809358117.80935817.59773810.1893381000
979818.454894100.00.10829218.563186118.56318617.59773810.1893381000
989918.870044100.00.04079918.910843118.91084317.59773810.1893381000
9910019.105111100.00.00632819.111439119.11143917.59773810.1893381000
\n", + "

100 rows × 15 columns

\n", + "
" + ], + "text/plain": [ + " Attention Order Arrival Time Total Number of people \n", + "0 1 0.094968 1 \\\n", + "1 2 0.435671 1 \n", + "2 3 0.893621 1 \n", + "3 4 0.952125 1 \n", + "4 5 1.000481 2 \n", + ".. ... ... ... \n", + "95 96 17.541317 2 \n", + "96 97 17.684506 1 \n", + "97 98 18.454894 1 \n", + "98 99 18.870044 1 \n", + "99 100 19.105111 1 \n", + "\n", + " Number of people in Line Time in Line Time in service Leave Time \n", + "0 0 0.0 0.131481 0.226449 \\\n", + "1 0 0.0 0.115737 0.551408 \n", + "2 0 0.0 0.048203 0.941824 \n", + "3 0 0.0 0.101260 1.053385 \n", + "4 0 0.0 0.062067 1.062548 \n", + ".. ... ... ... ... \n", + "95 0 0.0 0.056421 17.597738 \n", + "96 0 0.0 0.124852 17.809358 \n", + "97 0 0.0 0.108292 18.563186 \n", + "98 0 0.0 0.040799 18.910843 \n", + "99 0 0.0 0.006328 19.111439 \n", + "\n", + " Join the system? Time busy server 1 Time busy server 2 \n", + "0 1 0.226449 0.000000 \\\n", + "1 1 0.551408 0.000000 \n", + "2 1 0.941824 0.000000 \n", + "3 1 1.053385 0.000000 \n", + "4 1 1.053385 1.062548 \n", + ".. ... ... ... \n", + "95 1 17.567164 17.597738 \n", + "96 1 17.809358 17.597738 \n", + "97 1 18.563186 17.597738 \n", + "98 1 18.910843 17.597738 \n", + "99 1 19.111439 17.597738 \n", + "\n", + " Time busy server 3 Server 1 attended this element? \n", + "0 0.000000 1 \\\n", + "1 0.000000 1 \n", + "2 0.000000 1 \n", + "3 0.000000 1 \n", + "4 0.000000 0 \n", + ".. ... ... \n", + "95 10.189338 0 \n", + "96 10.189338 1 \n", + "97 10.189338 1 \n", + "98 10.189338 1 \n", + "99 10.189338 1 \n", + "\n", + " Server 2 attended this element? Server 3 attended this element? \n", + "0 0 0 \\\n", + "1 0 0 \n", + "2 0 0 \n", + "3 0 0 \n", + "4 1 0 \n", + ".. ... ... \n", + "95 1 0 \n", + "96 0 0 \n", + "97 0 0 \n", + "98 0 0 \n", + "99 0 0 \n", + "\n", + " Finish after closed \n", + "0 0 \n", + "1 0 \n", + "2 0 \n", + "3 0 \n", + "4 0 \n", + ".. ... \n", + "95 0 \n", + "96 0 \n", + "97 0 \n", + "98 0 \n", + "99 0 \n", + "\n", + "[100 rows x 15 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation = phitter.simulation.QueueingSimulation(\"exponential\", {\"lambda\": 5}, \"exponential\", {\"lambda\": 20}, 3, n=100, k=3, d=\"LIFO\")\n", + "simulation.run(2000)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MetricsValue
0Average Time in System0.054853
1Average Time in Queue0.000000
2Average Time in Service0.054853
3Std. Dev. Time in System0.053867
4Std. Dev. Time in Queue0.000000
5Std. Dev. Time in Service0.053867
6Average Elements in System0.002743
7Average Elements in Queue0.000000
8Probability to join the System1.000000
9Probability to finish after Time0.000000
10Probability to Wait in Line0.000000
11Utilization Server #10.770000
12Utilization Server #20.220000
13Utilization Server #30.010000
\n", + "
" + ], + "text/plain": [ + " Metrics Value\n", + "0 Average Time in System 0.054853\n", + "1 Average Time in Queue 0.000000\n", + "2 Average Time in Service 0.054853\n", + "3 Std. Dev. Time in System 0.053867\n", + "4 Std. Dev. Time in Queue 0.000000\n", + "5 Std. Dev. Time in Service 0.053867\n", + "6 Average Elements in System 0.002743\n", + "7 Average Elements in Queue 0.000000\n", + "8 Probability to join the System 1.000000\n", + "9 Probability to finish after Time 0.000000\n", + "10 Probability to Wait in Line 0.000000\n", + "11 Utilization Server #1 0.770000\n", + "12 Utilization Server #2 0.220000\n", + "13 Utilization Server #3 0.010000" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation.metrics_summary()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Number of elementsProb. Less or EqualsExact ProbabilityProb. Greter or equals
000.9988010.9988011.000000
110.9998420.0010410.001199
221.0000000.0001580.000158
\n", + "
" + ], + "text/plain": [ + " Number of elements Prob. Less or Equals Exact Probability \n", + "0 0 0.998801 0.998801 \\\n", + "1 1 0.999842 0.001041 \n", + "2 2 1.000000 0.000158 \n", + "\n", + " Prob. Greter or equals \n", + "0 1.000000 \n", + "1 0.001199 \n", + "2 0.000158 " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation.number_probability_summary()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/phitter_local/simulation/test_process_simulations.ipynb b/tests/phitter_local/simulation/test_process_simulations.ipynb new file mode 100644 index 0000000..944066b --- /dev/null +++ b/tests/phitter_local/simulation/test_process_simulations.ipynb @@ -0,0 +1,108 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0,\"../../../\")\n", + "import phitter" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "simulation = phitter.simulation.ProcessSimulation()\n", + "simulation.add_process(\"normal\", {\"mu\": 5, \"sigma\": 2}, \"first\", new_branch=True, number_of_products=10)\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"second\", previous_ids=[\"first\"])\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"ni_idea\", previous_ids=[\"first\"])\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"ni_idea_2\", previous_ids=[\"first\"])\n", + "\n", + "simulation.add_process(\"gamma\", {\"alpha\": 15, \"beta\": 3}, \"third\", new_branch=True)\n", + "# simulation.add_process(\"exponential\", {\"lambda\": 4.3}, \"nn\", previous_ids=[\"third\"])\n", + "simulation.add_process(\"beta\", {\"alpha\": 1, \"beta\": 1, \"A\": 2, \"B\": 3}, \"fourth\", previous_ids=[\"second\", \"third\"], number_of_products=2)\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4.3}, \"nn\", previous_ids=[\"third\"])\n", + "\n", + "\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4.3}, \"fifth\", new_branch=True)\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4.3}, \"sixth\", previous_ids=[\"fourth\", \"ni_idea\", \"nn\"])\n", + "simulation.add_process(\"beta\", {\"alpha\": 1, \"beta\": 1, \"A\": 2, \"B\": 3}, \"seventh\", previous_ids=[\"fifth\", \"sixth\", \"ni_idea_2\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[60.910389060638735,\n", + " 71.7036413860762,\n", + " 63.180822548765725,\n", + " 61.67380804299192,\n", + " 57.95645127570466,\n", + " 58.428061132122146,\n", + " 63.30357172070581,\n", + " 70.73770263716327,\n", + " 60.48492019029255,\n", + " 59.26121489392577]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation.run(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(60.88028267603926, 61.12153218365918, 61.362781691279096, 0.6741848235617004)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation.run_confidence_interval(number_of_simulations=100)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/pytest/test_process_simulation.py b/tests/pytest/test_process_simulation.py new file mode 100644 index 0000000..6509478 --- /dev/null +++ b/tests/pytest/test_process_simulation.py @@ -0,0 +1,104 @@ +import pytest +import phitter + + +def wrong_simulation_name(): + simulation = phitter.simulation.ProcessSimulation() + with pytest.raises(ValueError): + simulation.add_process( + "non_real_function", + {"error": 13}, + "first", + new_branch=True, + number_of_products=10, + ) + + +def simulation_running_assert(): + simulation = phitter.simulation.ProcessSimulation() + simulation.add_process( + "normal", {"mu": 5, "sigma": 2}, "first", new_branch=True, number_of_products=10 + ) + simulation.add_process( + "exponential", {"lambda": 4}, "second", previous_ids=["first"] + ) + simulation.add_process( + "exponential", {"lambda": 4}, "ni_idea", previous_ids=["first"] + ) + simulation.add_process( + "exponential", {"lambda": 4}, "ni_idea_2", previous_ids=["first"] + ) + + simulation.add_process("gamma", {"alpha": 15, "beta": 3}, "third", new_branch=True) + # simulation.add_process("exponential", {"lambda": 4.3}, "nn", previous_ids=["third"]) + simulation.add_process( + "beta", + {"alpha": 1, "beta": 1, "A": 2, "B": 3}, + "fourth", + previous_ids=["second", "third"], + number_of_products=2, + ) + simulation.add_process("exponential", {"lambda": 4.3}, "nn", previous_ids=["third"]) + + simulation.add_process("exponential", {"lambda": 4.3}, "fifth", new_branch=True) + simulation.add_process( + "exponential", + {"lambda": 4.3}, + "sixth", + previous_ids=["fourth", "ni_idea", "nn"], + ) + simulation.add_process( + "beta", + {"alpha": 1, "beta": 1, "A": 2, "B": 3}, + "seventh", + previous_ids=["fifth", "sixth", "ni_idea_2"], + ) + + result = simulation.run(10) + + assert len(result) == 10 and type(result) == list + + +def simulation_confidence_interval_assert(): + simulation = phitter.simulation.ProcessSimulation() + simulation.add_process( + "normal", {"mu": 5, "sigma": 2}, "first", new_branch=True, number_of_products=10 + ) + simulation.add_process( + "exponential", {"lambda": 4}, "second", previous_ids=["first"] + ) + simulation.add_process( + "exponential", {"lambda": 4}, "ni_idea", previous_ids=["first"] + ) + simulation.add_process( + "exponential", {"lambda": 4}, "ni_idea_2", previous_ids=["first"] + ) + + simulation.add_process("gamma", {"alpha": 15, "beta": 3}, "third", new_branch=True) + # simulation.add_process("exponential", {"lambda": 4.3}, "nn", previous_ids=["third"]) + simulation.add_process( + "beta", + {"alpha": 1, "beta": 1, "A": 2, "B": 3}, + "fourth", + previous_ids=["second", "third"], + number_of_products=2, + ) + simulation.add_process("exponential", {"lambda": 4.3}, "nn", previous_ids=["third"]) + + simulation.add_process("exponential", {"lambda": 4.3}, "fifth", new_branch=True) + simulation.add_process( + "exponential", + {"lambda": 4.3}, + "sixth", + previous_ids=["fourth", "ni_idea", "nn"], + ) + simulation.add_process( + "beta", + {"alpha": 1, "beta": 1, "A": 2, "B": 3}, + "seventh", + previous_ids=["fifth", "sixth", "ni_idea_2"], + ) + + result = simulation.run_confidence_interval(number_of_simulations=100) + + assert len(result) == 4 and type(result) == tuple diff --git a/tests/pytest/test_queue_simulation_and_own.py b/tests/pytest/test_queue_simulation_and_own.py new file mode 100644 index 0000000..2bc6e12 --- /dev/null +++ b/tests/pytest/test_queue_simulation_and_own.py @@ -0,0 +1,48 @@ +import pandas +import phitter + + +def test_own_distribution_and_pbs(): + parameters = {0: 0.5, 1: 0.3, 2: 0.2} + simulation = phitter.simulation.QueueingSimulation( + "exponential", + {"lambda": 5}, + "exponential", + {"lambda": 20}, + 3, + d="PBS", + pbs_distribution="own_distribution", + pbs_parameters=parameters, + ) + simulation.run(2000) + assert simulation.probability_to_finish_after_time() <= 1 + + +def test_confidence_interval_fifo(): + simulation = phitter.simulation.QueueingSimulation( + "exponential", {"lambda": 5}, "exponential", {"lambda": 20}, 3, d="FIFO" + ) + a, b = simulation.confidence_interval_metrics(2000, replications=10) + assert ( + type(a) == pandas.DataFrame + and len(a) > 0 + and type(b) == pandas.DataFrame + and len(b) > 0 + ) + + +def test_lifo_metrics(): + simulation = phitter.simulation.QueueingSimulation( + "exponential", + {"lambda": 5}, + "exponential", + {"lambda": 20}, + 3, + n=100, + k=3, + d="LIFO", + ) + simulation.run(2000) + metrics = simulation.metrics_summary() + prob = simulation.number_probability_summary() + assert len(metrics) > 0 and len(prob) > 0 From 38fe9b555c4c457ee2ccc4981a01ebd733f99d5d Mon Sep 17 00:00:00 2001 From: cargar_github Date: Mon, 14 Oct 2024 22:05:59 -0500 Subject: [PATCH 08/11] Simulation Updates --- LICENSE | 4 +- SIMULATION.md | 43 +++++++++++++++++++ .../own_distribution/own_distribution.py | 1 + .../process_simulation/process_simulation.py | 6 ++- pyproject.toml | 2 +- tests/pytest/test_process_simulation.py | 17 +++++--- tests/pytest/test_queue_simulation_and_own.py | 33 +++++++++----- 7 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 SIMULATION.md diff --git a/LICENSE b/LICENSE index 720b11a..4e6ea4d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2024 Sebastián José Herrera Monterrosa +Copyright (c) 2024 Sebastián José Herrera Monterrosa, Carlos Andrés Másmela Pinilla Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. diff --git a/SIMULATION.md b/SIMULATION.md new file mode 100644 index 0000000..d354b14 --- /dev/null +++ b/SIMULATION.md @@ -0,0 +1,43 @@ +

+ + + + phitter-dark-logo + +

+ +

+ + Downloads + + + License + + + Supported Python versions + + + Tests + +

+ +

+ Phitter analyzes datasets and determines the best analytical probability distributions that represent them. Phitter studies over 80 probability distributions, both continuous and discrete, 3 goodness-of-fit tests, and interactive visualizations. For each selected probability distribution, a standard modeling guide is provided along with spreadsheets that detail the methodology for using the chosen distribution in data science, operations research, and artificial intelligence. +

+

+ This repository contains the implementation of the python library and the kernel of Phitter Web +

+ +## Installation + +### Requirements + +```console +python: >=3.9 +``` + +### PyPI + +```console +pip install phitter +``` diff --git a/phitter/simulation/own_distribution/own_distribution.py b/phitter/simulation/own_distribution/own_distribution.py index b1cd129..000886b 100644 --- a/phitter/simulation/own_distribution/own_distribution.py +++ b/phitter/simulation/own_distribution/own_distribution.py @@ -1,4 +1,5 @@ import random + import numpy as np diff --git a/phitter/simulation/process_simulation/process_simulation.py b/phitter/simulation/process_simulation/process_simulation.py index 159c955..4b576b1 100644 --- a/phitter/simulation/process_simulation/process_simulation.py +++ b/phitter/simulation/process_simulation/process_simulation.py @@ -1,10 +1,12 @@ -import phitter +import math import random + import numpy as np -import math from graphviz import Digraph from IPython.display import display +import phitter + class ProcessSimulation: def __init__(self) -> None: diff --git a/pyproject.toml b/pyproject.toml index 2d712de..54e8096 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "phitter" version = "0.0.8" description = "Find the best probability distribution for your dataset" -authors = [{name = "Sebastián José Herrera Monterrosa", email = "phitter.email@gmail.com"}] +authors = [{name = "Sebastián José Herrera Monterrosa", email = "phitter.email@gmail.com"}, {name = "Carlos Andrés Másmela Pinilla", email = "phitter.email@gmail.com"}] readme = "README.md" requires-python = ">=3.9" license = {file = "LICENSE"} diff --git a/tests/pytest/test_process_simulation.py b/tests/pytest/test_process_simulation.py index 6509478..7f6d9f6 100644 --- a/tests/pytest/test_process_simulation.py +++ b/tests/pytest/test_process_simulation.py @@ -2,7 +2,8 @@ import phitter -def wrong_simulation_name(): +# Test that adding a process with a wrong function name raises a ValueError +def test_wrong_simulation_name(): simulation = phitter.simulation.ProcessSimulation() with pytest.raises(ValueError): simulation.add_process( @@ -14,7 +15,8 @@ def wrong_simulation_name(): ) -def simulation_running_assert(): +# Test to ensure the simulation runs correctly and returns a list of the correct length +def test_simulation_running(): simulation = phitter.simulation.ProcessSimulation() simulation.add_process( "normal", {"mu": 5, "sigma": 2}, "first", new_branch=True, number_of_products=10 @@ -30,7 +32,6 @@ def simulation_running_assert(): ) simulation.add_process("gamma", {"alpha": 15, "beta": 3}, "third", new_branch=True) - # simulation.add_process("exponential", {"lambda": 4.3}, "nn", previous_ids=["third"]) simulation.add_process( "beta", {"alpha": 1, "beta": 1, "A": 2, "B": 3}, @@ -56,10 +57,12 @@ def simulation_running_assert(): result = simulation.run(10) - assert len(result) == 10 and type(result) == list + assert len(result) == 10 + assert isinstance(result, list) -def simulation_confidence_interval_assert(): +# Test to check if the confidence interval results are as expected +def test_simulation_confidence_interval(): simulation = phitter.simulation.ProcessSimulation() simulation.add_process( "normal", {"mu": 5, "sigma": 2}, "first", new_branch=True, number_of_products=10 @@ -75,7 +78,6 @@ def simulation_confidence_interval_assert(): ) simulation.add_process("gamma", {"alpha": 15, "beta": 3}, "third", new_branch=True) - # simulation.add_process("exponential", {"lambda": 4.3}, "nn", previous_ids=["third"]) simulation.add_process( "beta", {"alpha": 1, "beta": 1, "A": 2, "B": 3}, @@ -101,4 +103,5 @@ def simulation_confidence_interval_assert(): result = simulation.run_confidence_interval(number_of_simulations=100) - assert len(result) == 4 and type(result) == tuple + assert len(result) == 4 + assert isinstance(result, tuple) diff --git a/tests/pytest/test_queue_simulation_and_own.py b/tests/pytest/test_queue_simulation_and_own.py index 2bc6e12..ff8200d 100644 --- a/tests/pytest/test_queue_simulation_and_own.py +++ b/tests/pytest/test_queue_simulation_and_own.py @@ -1,7 +1,9 @@ -import pandas +import pytest +import pandas as pd import phitter +# Test para simulación con distribución propia y política PBS def test_own_distribution_and_pbs(): parameters = {0: 0.5, 1: 0.3, 2: 0.2} simulation = phitter.simulation.QueueingSimulation( @@ -14,23 +16,26 @@ def test_own_distribution_and_pbs(): pbs_distribution="own_distribution", pbs_parameters=parameters, ) + # Ejecutar la simulación con 2000 iteraciones simulation.run(2000) + # Verificar que la probabilidad de terminar después de un cierto tiempo es <= 1 assert simulation.probability_to_finish_after_time() <= 1 +# Test para verificar que la política FIFO genera intervalos de confianza correctos def test_confidence_interval_fifo(): simulation = phitter.simulation.QueueingSimulation( "exponential", {"lambda": 5}, "exponential", {"lambda": 20}, 3, d="FIFO" ) + # Obtener los intervalos de confianza para las métricas con 10 réplicas a, b = simulation.confidence_interval_metrics(2000, replications=10) - assert ( - type(a) == pandas.DataFrame - and len(a) > 0 - and type(b) == pandas.DataFrame - and len(b) > 0 - ) + + # Verificar que ambos resultados son DataFrames de pandas y no están vacíos + assert isinstance(a, pd.DataFrame) and len(a) > 0 + assert isinstance(b, pd.DataFrame) and len(b) > 0 +# Test para verificar que la política LIFO genera métricas y probabilidades correctas def test_lifo_metrics(): simulation = phitter.simulation.QueueingSimulation( "exponential", @@ -38,11 +43,17 @@ def test_lifo_metrics(): "exponential", {"lambda": 20}, 3, - n=100, - k=3, - d="LIFO", + n=100, # Número de eventos + k=3, # Capacidad del sistema + d="LIFO", # Política LIFO ) + # Ejecutar la simulación con 2000 iteraciones simulation.run(2000) + + # Obtener el resumen de métricas y la probabilidad numérica metrics = simulation.metrics_summary() prob = simulation.number_probability_summary() - assert len(metrics) > 0 and len(prob) > 0 + + # Verificar que las métricas y probabilidades no están vacías + assert len(metrics) > 0 + assert len(prob) > 0 From 45841d2983c4e5685846daba8a2bde65eba9a7ae Mon Sep 17 00:00:00 2001 From: cargar_github Date: Wed, 16 Oct 2024 22:15:30 -0500 Subject: [PATCH 09/11] =?UTF-8?q?Simulation=20updating=20general=20readme?= =?UTF-8?q?=20and=20simulation=20readme.=20Adding=20dependencies=20and=20a?= =?UTF-8?q?dding=20comments=20to=20process=20simulation.=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 153 ++++---- SIMULATION.md | 183 +++++++++ multimedia/simulation_process_graph.png | Bin 0 -> 41497 bytes .../process_simulation/process_simulation.py | 30 +- .../queueing_simulation.py | 2 +- pyproject.toml | 4 +- .../test_own_dist_and_queue_simulation.ipynb | 347 +++++++++++------- 7 files changed, 516 insertions(+), 203 deletions(-) create mode 100644 multimedia/simulation_process_graph.png diff --git a/README.md b/README.md index 1840ff6..de2e60e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@

Phitter analyzes datasets and determines the best analytical probability distributions that represent them. Phitter studies over 80 probability distributions, both continuous and discrete, 3 goodness-of-fit tests, and interactive visualizations. For each selected probability distribution, a standard modeling guide is provided along with spreadsheets that detail the methodology for using the chosen distribution in data science, operations research, and artificial intelligence. + + In addition, Phitter offers the capability to perform process simulations, allowing users to graph and observe minimum times for specific observations. It also supports queue simulations with flexibility to configure various parameters, such as the number of servers, maximum population size, system capacity, and different queue disciplines, including First-In-First-Out (FIFO), Last-In-First-Out (LIFO), and priority-based service (PBS). +

This repository contains the implementation of the python library and the kernel of Phitter Web @@ -257,81 +260,81 @@ distribution.mode # -> 733.3333333333333 #### 2. Resources Continuous Distributions -| Distribution | Phitter Playground | Excel File | Google Sheets Files | -| :------------------------ | :----------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------- | -| alpha | ▶️[phitter:alpha](https://phitter.io/distributions/continuous/alpha) | 📊[alpha.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/alpha.xlsx) | 🌐[gs:alpha](https://docs.google.com/spreadsheets/d/1yRovxx1YbqgEul65DjjXetysc_4qgX2a_2NQQA1AxCA) | -| arcsine | ▶️[phitter:arcsine](https://phitter.io/distributions/continuous/arcsine) | 📊[arcsine.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/arcsine.xlsx) | 🌐[gs:arcsine](https://docs.google.com/spreadsheets/d/1q8SKX4gmSbpGzimRvjopzaZ4KrEV5NY1EPmf1G1T7NQ) | -| argus | ▶️[phitter:argus](https://phitter.io/distributions/continuous/argus) | 📊[argus.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/argus.xlsx) | 🌐[gs:argus](https://docs.google.com/spreadsheets/d/1u2x7IFUSB7rEyhs7s6-C2btT1Bk5aCr4WiUYEML-8xs) | -| beta | ▶️[phitter:beta](https://phitter.io/distributions/continuous/beta) | 📊[beta.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/beta.xlsx) | 🌐[gs:beta](https://docs.google.com/spreadsheets/d/1P7NDy-9toV3dv64gabnr8l2NjB1xt_Ani5IVMTx3gyU) | -| beta_prime | ▶️[phitter:beta_prime](https://phitter.io/distributions/continuous/beta_prime) | 📊[beta_prime.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/beta_prime.xlsx) | 🌐[gs:beta_prime](https://docs.google.com/spreadsheets/d/1-8cKeS9D6YixQE_uLig7UarXcoQoE-341yHDj8sfXA8) | -| beta_prime_4p | ▶️[phitter:beta_prime_4p](https://phitter.io/distributions/continuous/beta_prime_4p) | 📊[beta_prime_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/beta_prime_4p.xlsx) | 🌐[gs:beta_prime_4p](https://docs.google.com/spreadsheets/d/1vlaZrj_jX9oNGwjW0o4Z1AUTuUTGE8Z-Akis_wb7Jq4) | -| bradford | ▶️[phitter:bradford](https://phitter.io/distributions/continuous/bradford) | 📊[bradford.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/bradford.xlsx) | 🌐[gs:bradford](https://docs.google.com/spreadsheets/d/1kI8b05IXur3I9SUJdrbYIdv7zMdzVxVGPWx6sK6YmuU) | -| burr | ▶️[phitter:burr](https://phitter.io/distributions/continuous/burr) | 📊[burr.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/burr.xlsx) | 🌐[gs:burr](https://docs.google.com/spreadsheets/d/1vhY3l3VAgBj9BQT1yE3meRTmEZP3HXjjm30nxDKCwCI) | -| burr_4p | ▶️[phitter:burr_4p](https://phitter.io/distributions/continuous/burr_4p) | 📊[burr_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/burr_4p.xlsx) | 🌐[gs:burr_4p](https://docs.google.com/spreadsheets/d/1tEk3O2yvANj_PlLqACuwvRSqYYGQVRFH1SPMdLGYnz4) | -| cauchy | ▶️[phitter:cauchy](https://phitter.io/distributions/continuous/cauchy) | 📊[cauchy.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/cauchy.xlsx) | 🌐[gs:cauchy](https://docs.google.com/spreadsheets/d/1xoJJvuSvfg-umC7Ogio9fde1l4TiWuAlR2IxucYK0y8) | -| chi_square | ▶️[phitter:chi_square](https://phitter.io/distributions/continuous/chi_square) | 📊[chi_square.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/chi_square.xlsx) | 🌐[gs:chi_square](https://docs.google.com/spreadsheets/d/1VatJuUON_2qghjPEYMdcjGE7TYbYqduzgdYe5YNyVf4) | -| chi_square_3p | ▶️[phitter:chi_square_3p](https://phitter.io/distributions/continuous/chi_square_3p) | 📊[chi_square_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/chi_square_3p.xlsx) | 🌐[gs:chi_square_3p](https://docs.google.com/spreadsheets/d/15tf3ZKbEgR3JWQRbMT2OaNij3INTGGUuNsR01NCDFJw) | -| dagum | ▶️[phitter:dagum](https://phitter.io/distributions/continuous/dagum) | 📊[dagum.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/dagum.xlsx) | 🌐[gs:dagum](https://docs.google.com/spreadsheets/d/1qct7LByxY_z2-Rl-pWFG1LQsUxW8VQaCgLizn93YPxk) | -| dagum_4p | ▶️[phitter:dagum_4p](https://phitter.io/distributions/continuous/dagum_4p) | 📊[dagum_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/dagum_4p.xlsx) | 🌐[gs:dagum_4p](https://docs.google.com/spreadsheets/d/1ZkKqvVdy7CvhvXwK830F6GWJrdNxoXBxJYeFD6XC2DM) | -| erlang | ▶️[phitter:erlang](https://phitter.io/distributions/continuous/erlang) | 📊[erlang.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/erlang.xlsx) | 🌐[gs:erlang](https://docs.google.com/spreadsheets/d/1uG3Otntnm3cvMSkhkEiBVKuFn1pCLSWmiCxfN01D824) | -| erlang_3p | ▶️[phitter:erlang_3p](https://phitter.io/distributions/continuous/erlang_3p) | 📊[erlang_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/erlang_3p.xlsx) | 🌐[gs:erlang_3p](https://docs.google.com/spreadsheets/d/1EvFPyOAL-TPQyNf7sAXfqgHqap8sGynH0XxrLRVP12M) | -| error_function | ▶️[phitter:error_function](https://phitter.io/distributions/continuous/error_function) | 📊[error_function.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/error_function.xlsx) | 🌐[gs:error_function](https://docs.google.com/spreadsheets/d/1QT1vSgTWVgDmNz4FrH3fhwRGpgvPohgqZSCADHfBXkM) | -| exponential | ▶️[phitter:exponential](https://phitter.io/distributions/continuous/exponential) | 📊[exponential.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/exponential.xlsx) | 🌐[gs:exponential](https://docs.google.com/spreadsheets/d/1c8aCgHTq3fEyIkVM1Ph3fzebxQMuourz1UkWbH4h3HA) | -| exponential_2p | ▶️[phitter:exponential_2p](https://phitter.io/distributions/continuous/exponential_2p) | 📊[exponential_2p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/exponential_2p.xlsx) | 🌐[gs:exponential_2p](https://docs.google.com/spreadsheets/d/1XtrdS8iSCM1l33rbaXSz1uWZ3vnQsYPK-07NYE-ZYBs) | -| f | ▶️[phitter:f](https://phitter.io/distributions/continuous/f) | 📊[f.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/f.xlsx) | 🌐[gs:f](https://docs.google.com/spreadsheets/d/137gYI8B6MDnqFoQ4bY1crdpFSKtPzRgaJS564SY_CUY) | -| f_4p | ▶️[phitter:f_4p](https://phitter.io/distributions/continuous/f_4p) | 📊[f_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/f_4p.xlsx) | 🌐[gs:f_4p](https://docs.google.com/spreadsheets/d/11MgyMqzOyGNtFLdGviRTeNhAQMYBCJ8QRMHGxoPCzwM) | -| fatigue_life | ▶️[phitter:fatigue_life](https://phitter.io/distributions/continuous/fatigue_life) | 📊[fatigue_life.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/fatigue_life.xlsx) | 🌐[gs:fatigue_life](https://docs.google.com/spreadsheets/d/1j-U_YMX89VHe2jVq3pazpzqYeA1j1zopW22C9yJcPS0) | -| folded_normal | ▶️[phitter:folded_normal](https://phitter.io/distributions/continuous/folded_normal) | 📊[folded_normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/folded_normal.xlsx) | 🌐[gs:folded_normal](https://docs.google.com/spreadsheets/d/17NlSnru_46J8pSjxMPLDlzxoG2fPKWjeFvTh0ydfX4k) | -| frechet | ▶️[phitter:frechet](https://phitter.io/distributions/continuous/frechet) | 📊[frechet.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/frechet.xlsx) | 🌐[gs:frechet](https://docs.google.com/spreadsheets/d/1PNGvHImwOFIragM_hHrQJcTN7OcqCKFoHKXlPq76fnI) | -| gamma | ▶️[phitter:gamma](https://phitter.io/distributions/continuous/gamma) | 📊[gamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gamma.xlsx) | 🌐[gs:gamma](https://docs.google.com/spreadsheets/d/1HgD3a1zOml7Hy9PMVvFwQwrbmbs8iPbH-zQMowH0LVE) | -| gamma_3p | ▶️[phitter:gamma_3p](https://phitter.io/distributions/continuous/gamma_3p) | 📊[gamma_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gamma_3p.xlsx) | 🌐[gs:gamma_3p](https://docs.google.com/spreadsheets/d/1NkyFZFOMzk2V9qkFEI_zhGUGWiGV-K9vU-RLaFB7ip8) | -| generalized_extreme_value | ▶️[phitter:gen_extreme_value](https://phitter.io/distributions/continuous/generalized_extreme_value) | 📊[gen_extreme_value.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_extreme_value.xlsx) | 🌐[gs:gen_extreme_value](https://docs.google.com/spreadsheets/d/19qHvnTJGVVZ7zhi-yhauCOGhu0iAdkYJ5FFgwv1q5OI) | -| generalized_gamma | ▶️[phitter:gen_gamma](https://phitter.io/distributions/continuous/generalized_gamma) | 📊[gen_gamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_gamma.xlsx) | 🌐[gs:gen_gamma](https://docs.google.com/spreadsheets/d/1xx8b_VSG4jznZzaKq2yKumw5VcNX5Wj86YqLO7n4S5A) | -| generalized_gamma_4p | ▶️[phitter:gen_gamma_4p](https://phitter.io/distributions/continuous/generalized_gamma_4p) | 📊[gen_gamma_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_gamma_4p.xlsx) | 🌐[gs:gen_gamma_4p](https://docs.google.com/spreadsheets/d/1TN72MSkZ2bRyoNy29h4VIxFudXAroSi1PnmFijPvO0M) | -| generalized_logistic | ▶️[phitter:gen_logistic](https://phitter.io/distributions/continuous/generalized_logistic) | 📊[gen_logistic.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_logistic.xlsx) | 🌐[gs:gen_logistic](https://docs.google.com/spreadsheets/d/1vwppGjHbwEA3xd3OtV51sPZhpOWyzmPIOV_Tued-I1Y) | -| generalized_normal | ▶️[phitter:gen_normal](https://phitter.io/distributions/continuous/generalized_normal) | 📊[gen_normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_normal.xlsx) | 🌐[gs:gen_normal](https://docs.google.com/spreadsheets/d/1_77JSp0mhHxqvQugVRRWIoQOTa91WdyNqNmOfDNuSfA) | -| generalized_pareto | ▶️[phitter:gen_pareto](https://phitter.io/distributions/continuous/generalized_pareto) | 📊[gen_pareto.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_pareto.xlsx) | 🌐[gs:gen_pareto](https://docs.google.com/spreadsheets/d/1E28WYhX4Ba9Nj-JNxqAm-Gh7o1EOOIOwXIdCFl1PXI0) | -| gibrat | ▶️[phitter:gibrat](https://phitter.io/distributions/continuous/gibrat) | 📊[gibrat.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gibrat.xlsx) | 🌐[gs:gibrat](https://docs.google.com/spreadsheets/d/1pM7skBPnH8V3GCJo0iSst46Oc2OzqWdX2qATYBqc_GQ) | -| gumbel_left | ▶️[phitter:gumbel_left](https://phitter.io/distributions/continuous/gumbel_left) | 📊[gumbel_left.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gumbel_left.xlsx) | 🌐[gs:gumbel_left](https://docs.google.com/spreadsheets/d/1WoW97haebsHk1sB8smC4Zq8KqW8leJY0bPK757B2IdI) | -| gumbel_right | ▶️[phitter:gumbel_right](https://phitter.io/distributions/continuous/gumbel_right) | 📊[gumbel_right.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gumbel_right.xlsx) | 🌐[gs:gumbel_right](https://docs.google.com/spreadsheets/d/1CpzfSwAdptFrI8DhV3tWRsEFd9cr6h3Jaj7t3gigims) | -| half_normal | ▶️[phitter:half_normal](https://phitter.io/distributions/continuous/half_normal) | 📊[half_normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/half_normal.xlsx) | 🌐[gs:half_normal](https://docs.google.com/spreadsheets/d/1HQpNSNIhZPzMQvWWKyShnYNH74d1Bhs_d6k9La52V9M) | -| hyperbolic_secant | ▶️[phitter:hyperbolic_secant](https://phitter.io/distributions/continuous/hyperbolic_secant) | 📊[hyperbolic_secant.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/hyperbolic_secant.xlsx) | 🌐[gs:hyperbolic_secant](https://docs.google.com/spreadsheets/d/1lTcLlwX0fmgUjhT4ljvKL_dqSReK_lEthsZNBtDxAF8) | -| inverse_gamma | ▶️[phitter:inverse_gamma](https://phitter.io/distributions/continuous/inverse_gamma) | 📊[inverse_gamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gamma.xlsx) | 🌐[gs:inverse_gamma](https://docs.google.com/spreadsheets/d/1uOgfUvhBHKAXhbYATUwdHRQnBMIMnu6rWecqKx6MoIA) | -| inverse_gamma_3p | ▶️[phitter:inverse_gamma_3p](https://phitter.io/distributions/continuous/inverse_gamma_3p) | 📊[inverse_gamma_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gamma_3p.xlsx) | 🌐[gs:inverse_gamma_3p](https://docs.google.com/spreadsheets/d/16LCC6j_j1Cm7stc7LEd-C0ObUcZ-agL51ALGYxoZtrI) | -| inverse_gaussian | ▶️[phitter:inverse_gaussian](https://phitter.io/distributions/continuous/inverse_gaussian) | 📊[inverse_gaussian.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gaussian.xlsx) | 🌐[gs:inverse_gaussian](https://docs.google.com/spreadsheets/d/10LaEnmnRxNESViLTlw6FDyt1YSWNbMlBXaWc9t4q5qA) | -| inverse_gaussian_3p | ▶️[phitter:inverse_gaussian_3p](https://phitter.io/distributions/continuous/inverse_gaussian_3p) | 📊[inverse_gaussian_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gaussian_3p.xlsx) | 🌐[gs:inverse_gaussian_3p](https://docs.google.com/spreadsheets/d/1wkcSlXnUdMe4by2N9nPA_Cdsz3D0kHL7MVchsjl_CTQ) | -| johnson_sb | ▶️[phitter:johnson_sb](https://phitter.io/distributions/continuous/johnson_sb) | 📊[johnson_sb.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/johnson_sb.xlsx) | 🌐[gs:johnson_sb](https://docs.google.com/spreadsheets/d/1H3bpJd729k0VK3LtvgxvKJiduIdP04UkHhgJoq4ayHQ) | -| johnson_su | ▶️[phitter:johnson_su](https://phitter.io/distributions/continuous/johnson_su) | 📊[johnson_su.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/johnson_su.xlsx) | 🌐[gs:johnson_su](https://docs.google.com/spreadsheets/d/15kw_NZr3RFjN9orvF844ITWXroWRsCFkY7Uvq0NZ4K8) | -| kumaraswamy | ▶️[phitter:kumaraswamy](https://phitter.io/distributions/continuous/kumaraswamy) | 📊[kumaraswamy.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/kumaraswamy.xlsx) | 🌐[gs:kumaraswamy](https://docs.google.com/spreadsheets/d/10YJUDlAEygfOn07YxHBJxDqiXxygv8jKpJ8WvCZhe84) | -| laplace | ▶️[phitter:laplace](https://phitter.io/distributions/continuous/laplace) | 📊[laplace.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/laplace.xlsx) | 🌐[gs:laplace](https://docs.google.com/spreadsheets/d/110gPFTHOnQqecbXrjq3Wqv52I5Cw93UjL7eoSVC1DIs) | -| levy | ▶️[phitter:levy](https://phitter.io/distributions/continuous/levy) | 📊[levy.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/levy.xlsx) | 🌐[gs:levy](https://docs.google.com/spreadsheets/d/1OIA4C6iqhwK0Y17wb_O5ce9YXy4JIBf1yq3TqcmDp3U) | -| loggamma | ▶️[phitter:loggamma](https://phitter.io/distributions/continuous/loggamma) | 📊[loggamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/loggamma.xlsx) | 🌐[gs:loggamma](https://docs.google.com/spreadsheets/d/1SXCmxXs7hkajo_W_qL-e0MJQEaUJqTpUno1nYGXxmxI) | -| logistic | ▶️[phitter:logistic](https://phitter.io/distributions/continuous/logistic) | 📊[logistic.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/logistic.xlsx) | 🌐[gs:logistic](https://docs.google.com/spreadsheets/d/1WokfLcAM2f2TE9xcZwwuy3qjl4itw-y0cwAb7fyKxb0) | -| loglogistic | ▶️[phitter:loglogistic](https://phitter.io/distributions/continuous/loglogistic) | 📊[loglogistic.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/loglogistic.xlsx) | 🌐[gs:loglogistic](https://docs.google.com/spreadsheets/d/1WWXRuI6AP9n_n47ikOHWUjkfCYUOQgzhDjRsKBKEHXA) | -| loglogistic_3p | ▶️[phitter:loglogistic_3p](https://phitter.io/distributions/continuous/loglogistic_3p) | 📊[loglogistic_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/loglogistic_3p.xlsx) | 🌐[gs:loglogistic_3p](https://docs.google.com/spreadsheets/d/1RaLZ5L0rTrv9_fAi6izElf02ucuFy9LwagL_gQn3R0Y) | -| lognormal | ▶️[phitter:lognormal](https://phitter.io/distributions/continuous/lognormal) | 📊[lognormal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/lognormal.xlsx) | 🌐[gs:lognormal](https://docs.google.com/spreadsheets/d/1lS1cR4C2R45ug0ZyLxBlRBtcXH6hNPE1L-5wP68gUpA) | -| maxwell | ▶️[phitter:maxwell](https://phitter.io/distributions/continuous/maxwell) | 📊[maxwell.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/maxwell.xlsx) | 🌐[gs:maxwell](https://docs.google.com/spreadsheets/d/15tPw2RM2_a0vJMjVwNgsJnJUKFk9xbcEALqOf1m5qH0) | -| moyal | ▶️[phitter:moyal](https://phitter.io/distributions/continuous/moyal) | 📊[moyal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/moyal.xlsx) | 🌐[gs:moyal](https://docs.google.com/spreadsheets/d/1_58zWuk_-wSEesJbCc2FTHxv4HO5WouGwlStIZitt1I) | -| nakagami | ▶️[phitter:nakagami](https://phitter.io/distributions/continuous/nakagami) | 📊[nakagami.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/nakagami.xlsx) | 🌐[gs:nakagami](https://docs.google.com/spreadsheets/d/1fY8ID5gz1R6oWFm4w91GFdQMCd0wJ5ZRgfWi-yQtGqs) | -| non_central_chi_square | ▶️[phitter:non_central_chi_square](https://phitter.io/distributions/continuous/non_central_chi_square) | 📊[non_central_chi_square.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/non_central_chi_square.xlsx) | 🌐[gs:non_central_chi_square](https://docs.google.com/spreadsheets/d/17KWXPKOuMfTG0w4Gqe3lU3vWY2e9k31AX22PXTzOrFk) | -| non_central_f | ▶️[phitter:non_central_f](https://phitter.io/distributions/continuous/non_central_f) | 📊[non_central_f.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/non_central_f.xlsx) | 🌐[gs:non_central_f](https://docs.google.com/spreadsheets/d/14mZ563hIw2vXNM89DUncpsOdGgBXEUIIxJNa3-MVNIM) | -| non_central_t_student | ▶️[phitter:non_central_t_student](https://phitter.io/distributions/continuous/non_central_t_student) | 📊[non_central_t_student.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/non_central_t_student.xlsx) | 🌐[gs:non_central_t_student](https://docs.google.com/spreadsheets/d/1u8pseBDM3brw0AXlru1cprOsfQuHMWfvfDbz2XxKoOY) | -| normal | ▶️[phitter:normal](https://phitter.io/distributions/continuous/normal) | 📊[normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/normal.xlsx) | 🌐[gs:normal](https://docs.google.com/spreadsheets/d/18QTB3YYprvdFhr6PJI-DFcZOnYAuffdH8JHOtH1f83I) | -| pareto_first_kind | ▶️[phitter:pareto_first_kind](https://phitter.io/distributions/continuous/pareto_first_kind) | 📊[pareto_first_kind.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/pareto_first_kind.xlsx) | 🌐[gs:pareto_first_kind](https://docs.google.com/spreadsheets/d/1T-Sjp0yCxbJpP9njbovOiFpbP8PrwI5jlj66odxAw5E) | -| pareto_second_kind | ▶️[phitter:pareto_second_kind](https://phitter.io/distributions/continuous/pareto_second_kind) | 📊[pareto_second_kind.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/pareto_second_kind.xlsx) | 🌐[gs:pareto_second_kind](https://docs.google.com/spreadsheets/d/1hnBOqkbcRNuyRxaLP8eHei5MRwUFDb1bgdcZYkpYKio) | -| pert | ▶️[phitter:pert](https://phitter.io/distributions/continuous/pert) | 📊[pert.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/pert.xlsx) | 🌐[gs:pert](https://docs.google.com/spreadsheets/d/1NeKJKq4D_BB-ouefgJ35FzcORA7fH1OQwC5dCZKI_38) | -| power_function | ▶️[phitter:power_function](https://phitter.io/distributions/continuous/power_function) | 📊[power_function.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/power_function.xlsx) | 🌐[gs:power_function](https://docs.google.com/spreadsheets/d/1Hbi-XZiCK--JGFnoY-8iDLmNgYclDo5L4LKYKCCxfzw) | -| rayleigh | ▶️[phitter:rayleigh](https://phitter.io/distributions/continuous/rayleigh) | 📊[rayleigh.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/rayleigh.xlsx) | 🌐[gs:rayleigh](https://docs.google.com/spreadsheets/d/1UWtjOwokob4x43OcMLLFbNTYUqOo5dJWqSTfWbS-yyw) | -| reciprocal | ▶️[phitter:reciprocal](https://phitter.io/distributions/continuous/reciprocal) | 📊[reciprocal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/reciprocal.xlsx) | 🌐[gs:reciprocal](https://docs.google.com/spreadsheets/d/1ghFeCj8Q_hbpWqv9xXaNl1UKUe-5kOomZPWyI1JsoGA) | -| rice | ▶️[phitter:rice](https://phitter.io/distributions/continuous/rice) | 📊[rice.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/rice.xlsx) | 🌐[gs:rice](https://docs.google.com/spreadsheets/d/1hGVFWbF0w7D0l54t_p0vUId0rO2s61BRdrgslDYTnWc) | -| semicircular | ▶️[phitter:semicircular](https://phitter.io/distributions/continuous/semicircular) | 📊[semicircular.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/semicircular.xlsx) | 🌐[gs:semicircular](https://docs.google.com/spreadsheets/d/195c9VbAKtvEndJKnFp52TrENYK2iytMzIXLMKFAGgx4) | -| t_student | ▶️[phitter:t_student](https://phitter.io/distributions/continuous/t_student) | 📊[t_student.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/t_student.xlsx) | 🌐[gs:t_student](https://docs.google.com/spreadsheets/d/1fGxJfFL5eXAWk8xNI6HgCX9SQuXi-m5mR83N1dMLJrg) | -| t_student_3p | ▶️[phitter:t_student_3p](https://phitter.io/distributions/continuous/t_student_3p) | 📊[t_student_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/t_student_3p.xlsx) | 🌐[gs:t_student_3p](https://docs.google.com/spreadsheets/d/1K8bpbc-0mwe0mvRYXUQmoE8vaTigciJWDS4CPXmJodU) | -| trapezoidal | ▶️[phitter:trapezoidal](https://phitter.io/distributions/continuous/trapezoidal) | 📊[trapezoidal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/trapezoidal.xlsx) | 🌐[gs:trapezoidal](https://docs.google.com/spreadsheets/d/1Gsk5M_R2q9Or8RTggKtTkqEk-cN6IuDgYqbmhFm5Xlw) | -| triangular | ▶️[phitter:triangular](https://phitter.io/distributions/continuous/triangular) | 📊[triangular.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/triangular.xlsx) | 🌐[gs:triangular](https://docs.google.com/spreadsheets/d/1nirKOt7O7rUf2nlYu61cnNYT91GKSzb6pVlc1-pzzGw) | -| uniform | ▶️[phitter:uniform](https://phitter.io/distributions/continuous/uniform) | 📊[uniform.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/uniform.xlsx) | 🌐[gs:uniform](https://docs.google.com/spreadsheets/d/1TSaKNHOsVLYUobyKTpHR6qCuCAgfkKmRSETvdeZLcw4) | -| weibull | ▶️[phitter:weibull](https://phitter.io/distributions/continuous/weibull) | 📊[weibull.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/weibull.xlsx) | 🌐[gs:weibull](https://docs.google.com/spreadsheets/d/1DdNwWHmu0PZAhMYf475EMU3scTMXok3wOhzsg7gn8Ek) | -| weibull_3p | ▶️[phitter:weibull_3p](https://phitter.io/distributions/continuous/weibull_3p) | 📊[weibull_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/weibull_3p.xlsx) | 🌐[gs:weibull_3p](https://docs.google.com/spreadsheets/d/1agwpFGpXm62srDxgPOoDQGN8nGd8zaoztXg84Bgedlo) | +| Distribution | Phitter Playground | Excel File | Google Sheets Files | +| :------------------------ | :----------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | +| alpha | ▶️[phitter:alpha](https://phitter.io/distributions/continuous/alpha) | 📊[alpha.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/alpha.xlsx) | 🌐[gs:alpha](https://docs.google.com/spreadsheets/d/1yRovxx1YbqgEul65DjjXetysc_4qgX2a_2NQQA1AxCA) | +| arcsine | ▶️[phitter:arcsine](https://phitter.io/distributions/continuous/arcsine) | 📊[arcsine.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/arcsine.xlsx) | 🌐[gs:arcsine](https://docs.google.com/spreadsheets/d/1q8SKX4gmSbpGzimRvjopzaZ4KrEV5NY1EPmf1G1T7NQ) | +| argus | ▶️[phitter:argus](https://phitter.io/distributions/continuous/argus) | 📊[argus.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/argus.xlsx) | 🌐[gs:argus](https://docs.google.com/spreadsheets/d/1u2x7IFUSB7rEyhs7s6-C2btT1Bk5aCr4WiUYEML-8xs) | +| beta | ▶️[phitter:beta](https://phitter.io/distributions/continuous/beta) | 📊[beta.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/beta.xlsx) | 🌐[gs:beta](https://docs.google.com/spreadsheets/d/1P7NDy-9toV3dv64gabnr8l2NjB1xt_Ani5IVMTx3gyU) | +| beta_prime | ▶️[phitter:beta_prime](https://phitter.io/distributions/continuous/beta_prime) | 📊[beta_prime.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/beta_prime.xlsx) | 🌐[gs:beta_prime](https://docs.google.com/spreadsheets/d/1-8cKeS9D6YixQE_uLig7UarXcoQoE-341yHDj8sfXA8) | +| beta_prime_4p | ▶️[phitter:beta_prime_4p](https://phitter.io/distributions/continuous/beta_prime_4p) | 📊[beta_prime_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/beta_prime_4p.xlsx) | 🌐[gs:beta_prime_4p](https://docs.google.com/spreadsheets/d/1vlaZrj_jX9oNGwjW0o4Z1AUTuUTGE8Z-Akis_wb7Jq4) | +| bradford | ▶️[phitter:bradford](https://phitter.io/distributions/continuous/bradford) | 📊[bradford.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/bradford.xlsx) | 🌐[gs:bradford](https://docs.google.com/spreadsheets/d/1kI8b05IXur3I9SUJdrbYIdv7zMdzVxVGPWx6sK6YmuU) | +| burr | ▶️[phitter:burr](https://phitter.io/distributions/continuous/burr) | 📊[burr.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/burr.xlsx) | 🌐[gs:burr](https://docs.google.com/spreadsheets/d/1vhY3l3VAgBj9BQT1yE3meRTmEZP3HXjjm30nxDKCwCI) | +| burr_4p | ▶️[phitter:burr_4p](https://phitter.io/distributions/continuous/burr_4p) | 📊[burr_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/burr_4p.xlsx) | 🌐[gs:burr_4p](https://docs.google.com/spreadsheets/d/1tEk3O2yvANj_PlLqACuwvRSqYYGQVRFH1SPMdLGYnz4) | +| cauchy | ▶️[phitter:cauchy](https://phitter.io/distributions/continuous/cauchy) | 📊[cauchy.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/cauchy.xlsx) | 🌐[gs:cauchy](https://docs.google.com/spreadsheets/d/1xoJJvuSvfg-umC7Ogio9fde1l4TiWuAlR2IxucYK0y8) | +| chi_square | ▶️[phitter:chi_square](https://phitter.io/distributions/continuous/chi_square) | 📊[chi_square.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/chi_square.xlsx) | 🌐[gs:chi_square](https://docs.google.com/spreadsheets/d/1VatJuUON_2qghjPEYMdcjGE7TYbYqduzgdYe5YNyVf4) | +| chi_square_3p | ▶️[phitter:chi_square_3p](https://phitter.io/distributions/continuous/chi_square_3p) | 📊[chi_square_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/chi_square_3p.xlsx) | 🌐[gs:chi_square_3p](https://docs.google.com/spreadsheets/d/15tf3ZKbEgR3JWQRbMT2OaNij3INTGGUuNsR01NCDFJw) | +| dagum | ▶️[phitter:dagum](https://phitter.io/distributions/continuous/dagum) | 📊[dagum.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/dagum.xlsx) | 🌐[gs:dagum](https://docs.google.com/spreadsheets/d/1qct7LByxY_z2-Rl-pWFG1LQsUxW8VQaCgLizn93YPxk) | +| dagum_4p | ▶️[phitter:dagum_4p](https://phitter.io/distributions/continuous/dagum_4p) | 📊[dagum_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/dagum_4p.xlsx) | 🌐[gs:dagum_4p](https://docs.google.com/spreadsheets/d/1ZkKqvVdy7CvhvXwK830F6GWJrdNxoXBxJYeFD6XC2DM) | +| erlang | ▶️[phitter:erlang](https://phitter.io/distributions/continuous/erlang) | 📊[erlang.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/erlang.xlsx) | 🌐[gs:erlang](https://docs.google.com/spreadsheets/d/1uG3Otntnm3cvMSkhkEiBVKuFn1pCLSWmiCxfN01D824) | +| erlang_3p | ▶️[phitter:erlang_3p](https://phitter.io/distributions/continuous/erlang_3p) | 📊[erlang_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/erlang_3p.xlsx) | 🌐[gs:erlang_3p](https://docs.google.com/spreadsheets/d/1EvFPyOAL-TPQyNf7sAXfqgHqap8sGynH0XxrLRVP12M) | +| error_function | ▶️[phitter:error_function](https://phitter.io/distributions/continuous/error_function) | 📊[error_function.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/error_function.xlsx) | 🌐[gs:error_function](https://docs.google.com/spreadsheets/d/1QT1vSgTWVgDmNz4FrH3fhwRGpgvPohgqZSCADHfBXkM) | +| exponential | ▶️[phitter:exponential](https://phitter.io/distributions/continuous/exponential) | 📊[exponential.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/exponential.xlsx) | 🌐[gs:exponential](https://docs.google.com/spreadsheets/d/1c8aCgHTq3fEyIkVM1Ph3fzebxQMuourz1UkWbH4h3HA) | +| exponential_2p | ▶️[phitter:exponential_2p](https://phitter.io/distributions/continuous/exponential_2p) | 📊[exponential_2p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/exponential_2p.xlsx) | 🌐[gs:exponential_2p](https://docs.google.com/spreadsheets/d/1XtrdS8iSCM1l33rbaXSz1uWZ3vnQsYPK-07NYE-ZYBs) | +| f | ▶️[phitter:f](https://phitter.io/distributions/continuous/f) | 📊[f.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/f.xlsx) | 🌐[gs:f](https://docs.google.com/spreadsheets/d/137gYI8B6MDnqFoQ4bY1crdpFSKtPzRgaJS564SY_CUY) | +| f_4p | ▶️[phitter:f_4p](https://phitter.io/distributions/continuous/f_4p) | 📊[f_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/f_4p.xlsx) | 🌐[gs:f_4p](https://docs.google.com/spreadsheets/d/11MgyMqzOyGNtFLdGviRTeNhAQMYBCJ8QRMHGxoPCzwM) | +| fatigue_life | ▶️[phitter:fatigue_life](https://phitter.io/distributions/continuous/fatigue_life) | 📊[fatigue_life.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/fatigue_life.xlsx) | 🌐[gs:fatigue_life](https://docs.google.com/spreadsheets/d/1j-U_YMX89VHe2jVq3pazpzqYeA1j1zopW22C9yJcPS0) | +| folded_normal | ▶️[phitter:folded_normal](https://phitter.io/distributions/continuous/folded_normal) | 📊[folded_normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/folded_normal.xlsx) | 🌐[gs:folded_normal](https://docs.google.com/spreadsheets/d/17NlSnru_46J8pSjxMPLDlzxoG2fPKWjeFvTh0ydfX4k) | +| frechet | ▶️[phitter:frechet](https://phitter.io/distributions/continuous/frechet) | 📊[frechet.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/frechet.xlsx) | 🌐[gs:frechet](https://docs.google.com/spreadsheets/d/1PNGvHImwOFIragM_hHrQJcTN7OcqCKFoHKXlPq76fnI) | +| gamma | ▶️[phitter:gamma](https://phitter.io/distributions/continuous/gamma) | 📊[gamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gamma.xlsx) | 🌐[gs:gamma](https://docs.google.com/spreadsheets/d/1HgD3a1zOml7Hy9PMVvFwQwrbmbs8iPbH-zQMowH0LVE) | +| gamma_3p | ▶️[phitter:gamma_3p](https://phitter.io/distributions/continuous/gamma_3p) | 📊[gamma_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gamma_3p.xlsx) | 🌐[gs:gamma_3p](https://docs.google.com/spreadsheets/d/1NkyFZFOMzk2V9qkFEI_zhGUGWiGV-K9vU-RLaFB7ip8) | +| generalized_extreme_value | ▶️[phitter:gen_extreme_value](https://phitter.io/distributions/continuous/generalized_extreme_value) | 📊[gen_extreme_value.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_extreme_value.xlsx) | 🌐[gs:gen_extreme_value](https://docs.google.com/spreadsheets/d/19qHvnTJGVVZ7zhi-yhauCOGhu0iAdkYJ5FFgwv1q5OI) | +| generalized_gamma | ▶️[phitter:gen_gamma](https://phitter.io/distributions/continuous/generalized_gamma) | 📊[gen_gamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_gamma.xlsx) | 🌐[gs:gen_gamma](https://docs.google.com/spreadsheets/d/1xx8b_VSG4jznZzaKq2yKumw5VcNX5Wj86YqLO7n4S5A) | +| generalized_gamma_4p | ▶️[phitter:gen_gamma_4p](https://phitter.io/distributions/continuous/generalized_gamma_4p) | 📊[gen_gamma_4p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_gamma_4p.xlsx) | 🌐[gs:gen_gamma_4p](https://docs.google.com/spreadsheets/d/1TN72MSkZ2bRyoNy29h4VIxFudXAroSi1PnmFijPvO0M) | +| generalized_logistic | ▶️[phitter:gen_logistic](https://phitter.io/distributions/continuous/generalized_logistic) | 📊[gen_logistic.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_logistic.xlsx) | 🌐[gs:gen_logistic](https://docs.google.com/spreadsheets/d/1vwppGjHbwEA3xd3OtV51sPZhpOWyzmPIOV_Tued-I1Y) | +| generalized_normal | ▶️[phitter:gen_normal](https://phitter.io/distributions/continuous/generalized_normal) | 📊[gen_normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_normal.xlsx) | 🌐[gs:gen_normal](https://docs.google.com/spreadsheets/d/1_77JSp0mhHxqvQugVRRWIoQOTa91WdyNqNmOfDNuSfA) | +| generalized_pareto | ▶️[phitter:gen_pareto](https://phitter.io/distributions/continuous/generalized_pareto) | 📊[gen_pareto.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/generalized_pareto.xlsx) | 🌐[gs:gen_pareto](https://docs.google.com/spreadsheets/d/1E28WYhX4Ba9Nj-JNxqAm-Gh7o1EOOIOwXIdCFl1PXI0) | +| gibrat | ▶️[phitter:gibrat](https://phitter.io/distributions/continuous/gibrat) | 📊[gibrat.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gibrat.xlsx) | 🌐[gs:gibrat](https://docs.google.com/spreadsheets/d/1pM7skBPnH8V3GCJo0iSst46Oc2OzqWdX2qATYBqc_GQ) | +| gumbel_left | ▶️[phitter:gumbel_left](https://phitter.io/distributions/continuous/gumbel_left) | 📊[gumbel_left.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gumbel_left.xlsx) | 🌐[gs:gumbel_left](https://docs.google.com/spreadsheets/d/1WoW97haebsHk1sB8smC4Zq8KqW8leJY0bPK757B2IdI) | +| gumbel_right | ▶️[phitter:gumbel_right](https://phitter.io/distributions/continuous/gumbel_right) | 📊[gumbel_right.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/gumbel_right.xlsx) | 🌐[gs:gumbel_right](https://docs.google.com/spreadsheets/d/1CpzfSwAdptFrI8DhV3tWRsEFd9cr6h3Jaj7t3gigims) | +| half_normal | ▶️[phitter:half_normal](https://phitter.io/distributions/continuous/half_normal) | 📊[half_normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/half_normal.xlsx) | 🌐[gs:half_normal](https://docs.google.com/spreadsheets/d/1HQpNSNIhZPzMQvWWKyShnYNH74d1Bhs_d6k9La52V9M) | +| hyperbolic_secant | ▶️[phitter:hyperbolic_secant](https://phitter.io/distributions/continuous/hyperbolic_secant) | 📊[hyperbolic_secant.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/hyperbolic_secant.xlsx) | 🌐[gs:hyperbolic_secant](https://docs.google.com/spreadsheets/d/1lTcLlwX0fmgUjhT4ljvKL_dqSReK_lEthsZNBtDxAF8) | +| inverse_gamma | ▶️[phitter:inverse_gamma](https://phitter.io/distributions/continuous/inverse_gamma) | 📊[inverse_gamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gamma.xlsx) | 🌐[gs:inverse_gamma](https://docs.google.com/spreadsheets/d/1uOgfUvhBHKAXhbYATUwdHRQnBMIMnu6rWecqKx6MoIA) | +| inverse_gamma_3p | ▶️[phitter:inverse_gamma_3p](https://phitter.io/distributions/continuous/inverse_gamma_3p) | 📊[inverse_gamma_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gamma_3p.xlsx) | 🌐[gs:inverse_gamma_3p](https://docs.google.com/spreadsheets/d/16LCC6j_j1Cm7stc7LEd-C0ObUcZ-agL51ALGYxoZtrI) | +| inverse_gaussian | ▶️[phitter:inverse_gaussian](https://phitter.io/distributions/continuous/inverse_gaussian) | 📊[inverse_gaussian.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gaussian.xlsx) | 🌐[gs:inverse_gaussian](https://docs.google.com/spreadsheets/d/10LaEnmnRxNESViLTlw6FDyt1YSWNbMlBXaWc9t4q5qA) | +| inverse_gaussian_3p | ▶️[phitter:inverse_gaussian_3p](https://phitter.io/distributions/continuous/inverse_gaussian_3p) | 📊[inverse_gaussian_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/inverse_gaussian_3p.xlsx) | 🌐[gs:inverse_gaussian_3p](https://docs.google.com/spreadsheets/d/1wkcSlXnUdMe4by2N9nPA_Cdsz3D0kHL7MVchsjl_CTQ) | +| johnson_sb | ▶️[phitter:johnson_sb](https://phitter.io/distributions/continuous/johnson_sb) | 📊[johnson_sb.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/johnson_sb.xlsx) | 🌐[gs:johnson_sb](https://docs.google.com/spreadsheets/d/1H3bpJd729k0VK3LtvgxvKJiduIdP04UkHhgJoq4ayHQ) | +| johnson_su | ▶️[phitter:johnson_su](https://phitter.io/distributions/continuous/johnson_su) | 📊[johnson_su.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/johnson_su.xlsx) | 🌐[gs:johnson_su](https://docs.google.com/spreadsheets/d/15kw_NZr3RFjN9orvF844ITWXroWRsCFkY7Uvq0NZ4K8) | +| kumaraswamy | ▶️[phitter:kumaraswamy](https://phitter.io/distributions/continuous/kumaraswamy) | 📊[kumaraswamy.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/kumaraswamy.xlsx) | 🌐[gs:kumaraswamy](https://docs.google.com/spreadsheets/d/10YJUDlAEygfOn07YxHBJxDqiXxygv8jKpJ8WvCZhe84) | +| laplace | ▶️[phitter:laplace](https://phitter.io/distributions/continuous/laplace) | 📊[laplace.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/laplace.xlsx) | 🌐[gs:laplace](https://docs.google.com/spreadsheets/d/110gPFTHOnQqecbXrjq3Wqv52I5Cw93UjL7eoSVC1DIs) | +| levy | ▶️[phitter:levy](https://phitter.io/distributions/continuous/levy) | 📊[levy.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/levy.xlsx) | 🌐[gs:levy](https://docs.google.com/spreadsheets/d/1OIA4C6iqhwK0Y17wb_O5ce9YXy4JIBf1yq3TqcmDp3U) | +| loggamma | ▶️[phitter:loggamma](https://phitter.io/distributions/continuous/loggamma) | 📊[loggamma.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/loggamma.xlsx) | 🌐[gs:loggamma](https://docs.google.com/spreadsheets/d/1SXCmxXs7hkajo_W_qL-e0MJQEaUJqTpUno1nYGXxmxI) | +| logistic | ▶️[phitter:logistic](https://phitter.io/distributions/continuous/logistic) | 📊[logistic.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/logistic.xlsx) | 🌐[gs:logistic](https://docs.google.com/spreadsheets/d/1WokfLcAM2f2TE9xcZwwuy3qjl4itw-y0cwAb7fyKxb0) | +| loglogistic | ▶️[phitter:loglogistic](https://phitter.io/distributions/continuous/loglogistic) | 📊[loglogistic.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/loglogistic.xlsx) | 🌐[gs:loglogistic](https://docs.google.com/spreadsheets/d/1WWXRuI6AP9n_n47ikOHWUjkfCYUOQgzhDjRsKBKEHXA) | +| loglogistic_3p | ▶️[phitter:loglogistic_3p](https://phitter.io/distributions/continuous/loglogistic_3p) | 📊[loglogistic_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/loglogistic_3p.xlsx) | 🌐[gs:loglogistic_3p](https://docs.google.com/spreadsheets/d/1RaLZ5L0rTrv9_fAi6izElf02ucuFy9LwagL_gQn3R0Y) | +| lognormal | ▶️[phitter:lognormal](https://phitter.io/distributions/continuous/lognormal) | 📊[lognormal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/lognormal.xlsx) | 🌐[gs:lognormal](https://docs.google.com/spreadsheets/d/1lS1cR4C2R45ug0ZyLxBlRBtcXH6hNPE1L-5wP68gUpA) | +| maxwell | ▶️[phitter:maxwell](https://phitter.io/distributions/continuous/maxwell) | 📊[maxwell.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/maxwell.xlsx) | 🌐[gs:maxwell](https://docs.google.com/spreadsheets/d/15tPw2RM2_a0vJMjVwNgsJnJUKFk9xbcEALqOf1m5qH0) | +| moyal | ▶️[phitter:moyal](https://phitter.io/distributions/continuous/moyal) | 📊[moyal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/moyal.xlsx) | 🌐[gs:moyal](https://docs.google.com/spreadsheets/d/1_58zWuk_-wSEesJbCc2FTHxv4HO5WouGwlStIZitt1I) | +| nakagami | ▶️[phitter:nakagami](https://phitter.io/distributions/continuous/nakagami) | 📊[nakagami.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/nakagami.xlsx) | 🌐[gs:nakagami](https://docs.google.com/spreadsheets/d/1fY8ID5gz1R6oWFm4w91GFdQMCd0wJ5ZRgfWi-yQtGqs) | +| non_central_chi_square | ▶️[phitter:non_central_chi_square](https://phitter.io/distributions/continuous/non_central_chi_square) | 📊[non_central_chi_square.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/non_central_chi_square.xlsx) | 🌐[gs:non_central_chi_square](https://docs.google.com/spreadsheets/d/17KWXPKOuMfTG0w4Gqe3lU3vWY2e9k31AX22PXTzOrFk) | +| non_central_f | ▶️[phitter:non_central_f](https://phitter.io/distributions/continuous/non_central_f) | 📊[non_central_f.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/non_central_f.xlsx) | 🌐[gs:non_central_f](https://docs.google.com/spreadsheets/d/14mZ563hIw2vXNM89DUncpsOdGgBXEUIIxJNa3-MVNIM) | +| non_central_t_student | ▶️[phitter:non_central_t_student](https://phitter.io/distributions/continuous/non_central_t_student) | 📊[non_central_t_student.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/non_central_t_student.xlsx) | 🌐[gs:non_central_t_student](https://docs.google.com/spreadsheets/d/1u8pseBDM3brw0AXlru1cprOsfQuHMWfvfDbz2XxKoOY) | +| normal | ▶️[phitter:normal](https://phitter.io/distributions/continuous/normal) | 📊[normal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/normal.xlsx) | 🌐[gs:normal](https://docs.google.com/spreadsheets/d/18QTB3YYprvdFhr6PJI-DFcZOnYAuffdH8JHOtH1f83I) | +| pareto_first_kind | ▶️[phitter:pareto_first_kind](https://phitter.io/distributions/continuous/pareto_first_kind) | 📊[pareto_first_kind.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/pareto_first_kind.xlsx) | 🌐[gs:pareto_first_kind](https://docs.google.com/spreadsheets/d/1T-Sjp0yCxbJpP9njbovOiFpbP8PrwI5jlj66odxAw5E) | +| pareto_second_kind | ▶️[phitter:pareto_second_kind](https://phitter.io/distributions/continuous/pareto_second_kind) | 📊[pareto_second_kind.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/pareto_second_kind.xlsx) | 🌐[gs:pareto_second_kind](https://docs.google.com/spreadsheets/d/1hnBOqkbcRNuyRxaLP8eHei5MRwUFDb1bgdcZYkpYKio) | +| pert | ▶️[phitter:pert](https://phitter.io/distributions/continuous/pert) | 📊[pert.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/pert.xlsx) | 🌐[gs:pert](https://docs.google.com/spreadsheets/d/1NeKJKq4D_BB-ouefgJ35FzcORA7fH1OQwC5dCZKI_38) | +| power_function | ▶️[phitter:power_function](https://phitter.io/distributions/continuous/power_function) | 📊[power_function.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/power_function.xlsx) | 🌐[gs:power_function](https://docs.google.com/spreadsheets/d/1Hbi-XZiCK--JGFnoY-8iDLmNgYclDo5L4LKYKCCxfzw) | +| rayleigh | ▶️[phitter:rayleigh](https://phitter.io/distributions/continuous/rayleigh) | 📊[rayleigh.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/rayleigh.xlsx) | 🌐[gs:rayleigh](https://docs.google.com/spreadsheets/d/1UWtjOwokob4x43OcMLLFbNTYUqOo5dJWqSTfWbS-yyw) | +| reciprocal | ▶️[phitter:reciprocal](https://phitter.io/distributions/continuous/reciprocal) | 📊[reciprocal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/reciprocal.xlsx) | 🌐[gs:reciprocal](https://docs.google.com/spreadsheets/d/1ghFeCj8Q_hbpWqv9xXaNl1UKUe-5kOomZPWyI1JsoGA) | +| rice | ▶️[phitter:rice](https://phitter.io/distributions/continuous/rice) | 📊[rice.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/rice.xlsx) | 🌐[gs:rice](https://docs.google.com/spreadsheets/d/1hGVFWbF0w7D0l54t_p0vUId0rO2s61BRdrgslDYTnWc) | +| semicircular | ▶️[phitter:semicircular](https://phitter.io/distributions/continuous/semicircular) | 📊[semicircular.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/semicircular.xlsx) | 🌐[gs:semicircular](https://docs.google.com/spreadsheets/d/195c9VbAKtvEndJKnFp52TrENYK2iytMzIXLMKFAGgx4) | +| t_student | ▶️[phitter:t_student](https://phitter.io/distributions/continuous/t_student) | 📊[t_student.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/t_student.xlsx) | 🌐[gs:t_student](https://docs.google.com/spreadsheets/d/1fGxJfFL5eXAWk8xNI6HgCX9SQuXi-m5mR83N1dMLJrg) | +| t_student_3p | ▶️[phitter:t_student_3p](https://phitter.io/distributions/continuous/t_student_3p) | 📊[t_student_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/t_student_3p.xlsx) | 🌐[gs:t_student_3p](https://docs.google.com/spreadsheets/d/1K8bpbc-0mwe0mvRYXUQmoE8vaTigciJWDS4CPXmJodU) | +| trapezoidal | ▶️[phitter:trapezoidal](https://phitter.io/distributions/continuous/trapezoidal) | 📊[trapezoidal.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/trapezoidal.xlsx) | 🌐[gs:trapezoidal](https://docs.google.com/spreadsheets/d/1Gsk5M_R2q9Or8RTggKtTkqEk-cN6IuDgYqbmhFm5Xlw) | +| triangular | ▶️[phitter:triangular](https://phitter.io/distributions/continuous/triangular) | 📊[triangular.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/triangular.xlsx) | 🌐[gs:triangular](https://docs.google.com/spreadsheets/d/1nirKOt7O7rUf2nlYu61cnNYT91GKSzb6pVlc1-pzzGw) | +| uniform | ▶️[phitter:uniform](https://phitter.io/distributions/continuous/uniform) | 📊[uniform.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/uniform.xlsx) | 🌐[gs:uniform](https://docs.google.com/spreadsheets/d/1TSaKNHOsVLYUobyKTpHR6qCuCAgfkKmRSETvdeZLcw4) | +| weibull | ▶️[phitter:weibull](https://phitter.io/distributions/continuous/weibull) | 📊[weibull.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/weibull.xlsx) | 🌐[gs:weibull](https://docs.google.com/spreadsheets/d/1DdNwWHmu0PZAhMYf475EMU3scTMXok3wOhzsg7gn8Ek) | +| weibull_3p | ▶️[phitter:weibull_3p](https://phitter.io/distributions/continuous/weibull_3p) | 📊[weibull_3p.xlsx](https://github.com/phitterio/phitter-files/blob/main/continuous/weibull_3p.xlsx) | 🌐[gs:weibull_3p](https://docs.google.com/spreadsheets/d/1agwpFGpXm62srDxgPOoDQGN8nGd8zaoztXg84Bgedlo) | ## Discrete Distributions diff --git a/SIMULATION.md b/SIMULATION.md index d354b14..a7b35f9 100644 --- a/SIMULATION.md +++ b/SIMULATION.md @@ -23,6 +23,9 @@

Phitter analyzes datasets and determines the best analytical probability distributions that represent them. Phitter studies over 80 probability distributions, both continuous and discrete, 3 goodness-of-fit tests, and interactive visualizations. For each selected probability distribution, a standard modeling guide is provided along with spreadsheets that detail the methodology for using the chosen distribution in data science, operations research, and artificial intelligence. + + In addition, Phitter offers the capability to perform process simulations, allowing users to graph and observe minimum times for specific observations. It also supports queue simulations with flexibility to configure various parameters, such as the number of servers, maximum population size, system capacity, and different queue disciplines, including First-In-First-Out (FIFO), Last-In-First-Out (LIFO), and priority-based service (PBS). +

This repository contains the implementation of the python library and the kernel of Phitter Web @@ -41,3 +44,183 @@ python: >=3.9 ```console pip install phitter ``` + +# Simulation + +## Process Simulation + +This will help you to understand your processes. To use it, run the following line + +```python +from phitter import simulation + +# Create a simulation process instance +simulation = simulation.ProcessSimulation() + +``` + +### Add processes to your simulation instance + +There are two ways to add processes to your simulation instance: + +- Adding a **process _without_ preceding process (new branch)** +- Adding a **process _with_ preceding process (with previous ids)** + +#### Process _without_ preceding process (new branch) + +```python +# Add a new process without preceding process +simulation.add_process(prob_distribution = "normal", # Probability Distribution + parameters = {"mu": 5, "sigma": 2}, # Parameters + process_id = "first_process", # Process name + number_of_products = 10, # Number of products to be simulated in this stage + new_branch=True) # New branch + +``` + +#### Process _with_ preceding process (with previous ids) + +```python +# Add a new process with preceding process +simulation.add_process(prob_distribution = "exponential", # Probability Distribution + parameters = {"lambda": 4}, # Parameters + process_id = "second_process", # Process name + previous_ids = ["first_process"]) # Previous Process + +``` + +#### All together and adding some new process + +The order in which you add each process **_matters_**. You can add as many processes as you need. + +```python +# Add a new process without preceding process +simulation.add_process(prob_distribution = "normal", # Probability Distribution + parameters = {"mu": 5, "sigma": 2}, # Parameters + process_id = "first_process", # Process name + number_of_products = 10, # Number of products to be simulated in this stage + new_branch=True) # New branch + +# Add a new process with preceding process +simulation.add_process(prob_distribution = "exponential", # Probability Distribution + parameters = {"lambda": 4}, # Parameters + process_id = "second_process", # Process name + previous_ids = ["first_process"]) # Previous Process + +# Add a new process with preceding process +simulation.add_process(prob_distribution = "gamma", # Probability Distribution + parameters = {"alpha": 15, "beta": 3}, # Parameters + process_id = "third_process", # Process name + previous_ids = ["first_process"]) # Previous Process + +# Add a new process without preceding process +simulation.add_process(prob_distribution = "exponential", # Probability Distribution + parameters = {"lambda": 4.3}, # Parameters + process_id = "fourth_process", # Process name + new_branch=True) # New branch + + +# Add a new process with preceding process +simulation.add_process(prob_distribution = "beta", # Probability Distribution + parameters = {"alpha": 1, "beta": 1, "A": 2, "B": 3}, # Parameters + process_id = "fifth_process", # Process name + previous_ids = ["second_process", "fourth_process"]) # Previous Process - You can add several previous processes + +# Add a new process with preceding process +simulation.add_process(prob_distribution = "normal", # Probability Distribution + parameters = {"mu": 15, "sigma": 2}, # Parameters + process_id = "sixth_process", # Process name + previous_ids = ["third_process", "fifth_process"]) # Previous Process - You can add several previous processes + +``` + +### Visualize your processes + +You can visualize your processes to see if what you're trying to simulate is your actual process. + +```python +# Graph your process +simulation.process_graph() +``` + +![Simulation](./multimedia/simulation_process_graph.png) + +### Start Simulation + +You can simulate and have different simulation time values or you can create a confidence interval for your process + +#### Run Simulation + +```python +# Graph your process +simulation.run(number_of_simulations = 3) # -> [144.69982028694696, 121.8579230094202, 109.54433760798509] +``` + +#### Run confidence interval + +```python +# Graph your process +simulation.run_confidence_interval(confidence_level = 0.99, + number_of_simulations = 3, + replications = 10) +# -> (111.95874067073376, 114.76076000500356, 117.56277933927336, 3.439965191759079) - Lower bound, average, upper bound and standard deviation +``` + +## Queue Simulation + +If you need to simulate queues run the following code: + +```python +from phitter import simulation + +# Create a simulation process instance +simulation = simulation.QueueingSimulation(a = "exponential", + a_paramters = {"lambda": 5}, + s = "exponential", + s_parameters = {"lambda": 20}, + c = 3) + +``` + +In this case we are going to simulate **a** (arrivals) with _exponential distribution_ and **s** (service) as _exponential distribution_ with **c** equals to 3 different servers. + +By default Maximum Capacity **k** is _infinity_, total population **n** is _infinity_ and the queue discipline **d** is _FIFO_. As we are not selecting **d** equals to "PBS" we don't have any information to add for **pbs_distribution** nor **pbs_parameters** + +### Run the simulation + +If you want to have the simulation results + +```python +# Run simulation +simulation = simulation.run(simulation_time = 2000) +simulation +# -> df result +``` + +If you want to see some metrics and probabilities from this simulation you should use:: + +```python +# Calculate metrics +simulation.metrics_summary() +# -> df result + +# Calculate probabilities +number_probability_summary() +# -> df result +``` + +### Run Confidence Interval for metrics and probabilities + +If you want to have a confidence interval for your metrics and probabilities you should run the following line + +```python +# Calculate confidence interval for metrics and probabilities +probabilities, metrics = simulation.confidence_interval_metrics(simulation_time = 2000, + confidence_level = 0.99, + replications = 10) +probabilities +# -> df result + +metrics +# -> df result +``` diff --git a/multimedia/simulation_process_graph.png b/multimedia/simulation_process_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..6899e840e9a23c5e7a3414c3df874f3cf53cfd01 GIT binary patch literal 41497 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 None: + """Build the object Process Simulation""" self.process_prob_distr = dict() self.branches = dict() @@ -41,19 +42,23 @@ def add_process( new_branch (bool ,optional): Required if you want to start a new process that does not have previous processes. You cannot use this parameter at the same time with "previous_id". Defaults to False. previous_id (list[str], optional): Required if you have previous processes that are before this process. You cannot use this parameter at the same time with "new_branch". Defaults to None. """ - + # Verify if the probability is created in phitter if prob_distribution not in self.probability_distribution.keys(): raise ValueError( f"""Please select one of the following probability distributions: '{"', '".join(self.probability_distribution.keys())}'.""" ) else: + # Verify unique id name for each process if process_id not in self.order.keys(): + # Verify that at least is one element needed for the simulation in that stage if number_of_products >= 1: + # Verify that if you create a new branch, it's impossible to have a previous id (or preceding process). One of those is incorrect if new_branch == True and previous_ids != None: raise ValueError( f"""You cannot select 'new_branch' is equals to True if 'previous_id' is not empty. OR you cannot add 'previous_ids' if 'new_branch' is equals to True.""" ) else: + # If it is a new branch then initialize all the needed paramters if new_branch == True: branch_id = len(self.branches) self.branches[branch_id] = process_id @@ -65,6 +70,7 @@ def add_process( ) ) self.next_process[process_id] = 0 + # If it is NOT a new branch then initialize all the needed paramters elif previous_ids != None and all( id in self.order.keys() for id in previous_ids ): @@ -78,6 +84,7 @@ def add_process( self.next_process[process_id] = 0 for prev_id in previous_ids: self.next_process[prev_id] += 1 + # if something is incorrect then raise an error else: raise ValueError( f"""Please create a new_brach == True if you need a new process or specify the previous process/processes (previous_ids) that are before this one. Processes that have been added: '{"', '".join(self.order.keys())}'.""" @@ -100,34 +107,48 @@ def run(self, number_of_simulations: int = 1) -> list[float]: Returns: list[float]: Results of every simulation requested """ + # Create simulation list simulation_result = list() + # Start all possible simulations for simulation in range(number_of_simulations): + # Create dictionaries for identifing processes simulation_partial_result = dict() simulation_accumulative_result = dict() + # For every single "new branch" process for key in self.branches.keys(): partial_result = 0 + # Simulate the time it took to create each product needed for _ in range(self.number_of_products[self.branches[key]]): partial_result += self.process_prob_distr[self.branches[key]].ppf( random.random() ) + # Add all simulation time according to the time it took to create all products in that stage simulation_partial_result[self.branches[key]] = partial_result + # Because we are simulating the "new branch" or first processes, accumulative it's the same as partial result simulation_accumulative_result[self.branches[key]] = ( simulation_partial_result[self.branches[key]] ) + # For every process for key in self.process_prob_distr.keys(): + # Only consider the ones that are not "New Branches" if isinstance(self.order[key], list): partial_result = 0 + # Simulate all products time for _ in range(self.number_of_products[key]): partial_result += self.process_prob_distr[key].ppf( random.random() ) + # Save partial result simulation_partial_result[key] = partial_result + # Accumulate this partial result plus the previous processes of this process simulation_accumulative_result[key] = ( simulation_partial_result[key] + simulation_accumulative_result[ max(self.order[key], key=simulation_accumulative_result.get) ] ) + + # Save the max time of the simulation simulation_result.append( simulation_accumulative_result[ max( @@ -180,10 +201,13 @@ def process_graph( graph_direction (str, optional): You can show the graph in two ways: 'LR' left to right OR 'TB' top to bottom. Defaults to 'LR'. save_graph_pdf (bool, optional): You can save the process graph in a PDF file. Defaults to False. """ + # Create graph instance graph = Digraph(comment="Simulation Process Steb-by-Step") + # Add all nodes for node in set(self.order.keys()): print(node) + # Identify if this is a "New branch" if isinstance(self.order[node], int): graph.node( node, node, shape="circle", style="filled", fillcolor="lightgreen" @@ -200,6 +224,7 @@ def process_graph( graph.node(node, node, shape="box") for node in set(self.order.keys()): + # Identify if this is a "Previous id" if isinstance(self.order[node], list): for previous_node in self.order[node]: graph.edge( @@ -209,12 +234,15 @@ def process_graph( fontsize="10", ) + # Graph direction if graph_direction == "TB": graph.attr(rankdir="TB") else: graph.attr(rankdir="LR") + # If needed save graph in pdf if save_graph_pdf: graph.render("Simulation Process Steb-by-Step", view=True) + # Show graph display(graph) diff --git a/phitter/simulation/queueing_simulation/queueing_simulation.py b/phitter/simulation/queueing_simulation/queueing_simulation.py index b75282d..5b57f91 100644 --- a/phitter/simulation/queueing_simulation/queueing_simulation.py +++ b/phitter/simulation/queueing_simulation/queueing_simulation.py @@ -33,7 +33,7 @@ def __init__( c (int): Number of servers. This represents the total number of service channels available in the system. It indicates how many customers can be served simultaneously, affecting the system's capacity to handle incoming clients and impacting metrics like waiting times and queue lengths. k (float, optional): Maximum system capacity. This is the maximum number of customers that the system can accommodate at any given time, including both those in service and those waiting in the queue. It defines the limit beyond which arriving customers are either turned away or blocked from entering the system. Defaults to float("inf"). n (float, optional): Total population of potential customers. This denotes the overall number of potential customers who might require service from the system. It can be finite or infinite and affects the arrival rates and the modeling of the system, especially in closed queueing networks. Defaults to float("inf"). - d (str, optional): Queue discipline. This describes the rule or policy that determines the order in which customers are served. Common disciplines include First-In-First-Out ("FIFO"), Last-In-First-Out ("LIFO"), priority-based service ("PBS"). The queue discipline impacts waiting times and the overall fairness of the system.. Defaults to "FIFO". + d (str, optional): Queue discipline. This describes the rule or policy that determines the order in which customers are served. Common disciplines include First-In-First-Out ("FIFO"), Last-In-First-Out ("LIFO"), priority-based service ("PBS"). The queue discipline impacts waiting times and the overall fairness of the system. Defaults to "FIFO". pbs_distribution (str | None, optional): Discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". Distributions that can be used: 'own_distribution', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. Defaults to None. pbs_parameters (dict | None, optional): Parameters of the discrete distribution that identifies the label of the pbs, this parameter can only be used with "d='PBS'". If it is 'own-distribution' add labels in the following way (example): {0: 0.5, 1: 0.3, 2: 0.2}. Where the "key" corresponds to the label and the "value" the probability whose total sum must add up to 1; "keys" with greater importances are the greaters and always have to be numeric keys. You can add as labels as you need. """ diff --git a/pyproject.toml b/pyproject.toml index 54e8096..de7aad5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,9 @@ dependencies = [ "scipy>=1.1.0", "plotly>=5.14.0", "kaleido>=0.2.1", - "matplotlib>=3.3" + "matplotlib>=3.3", + "graphviz>=0.5.1", + "IPython>=0.13" ] [project.urls] diff --git a/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb b/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb index 13ee9af..973bdfc 100644 --- a/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb +++ b/tests/phitter_local/simulation/test_own_dist_and_queue_simulation.ipynb @@ -59,33 +59,33 @@ " \n", " 0\n", " 1\n", - " 0.292612\n", + " 0.386388\n", " 1\n", " 0\n", " 0.0\n", - " 0.003961\n", - " 0.296573\n", + " 0.015169\n", + " 0.401557\n", " 1\n", - " 0.296573\n", + " 0.401557\n", " 0.000000\n", " 0.000000\n", " 1\n", " 0\n", " 0\n", - " 0\n", + " 1\n", " 0\n", " \n", " \n", " 1\n", " 2\n", - " 0.450717\n", + " 1.261684\n", " 1\n", " 0\n", " 0.0\n", - " 0.009428\n", - " 0.460145\n", + " 0.002131\n", + " 1.263815\n", " 1\n", - " 0.460145\n", + " 1.263815\n", " 0.000000\n", " 0.000000\n", " 1\n", @@ -97,14 +97,14 @@ " \n", " 2\n", " 3\n", - " 0.947384\n", + " 1.305292\n", " 1\n", " 0\n", " 0.0\n", - " 0.059690\n", - " 1.007074\n", + " 0.094272\n", + " 1.399563\n", " 1\n", - " 1.007074\n", + " 1.399563\n", " 0.000000\n", " 0.000000\n", " 1\n", @@ -116,39 +116,39 @@ " \n", " 3\n", " 4\n", - " 0.951672\n", - " 2\n", + " 1.419468\n", + " 1\n", " 0\n", " 0.0\n", - " 0.043426\n", - " 0.995098\n", + " 0.016525\n", + " 1.435993\n", " 1\n", - " 1.007074\n", - " 0.995098\n", + " 1.435993\n", + " 0.000000\n", " 0.000000\n", - " 0\n", " 1\n", " 0\n", " 0\n", + " 2\n", " 0\n", " \n", " \n", " 4\n", " 5\n", - " 1.287054\n", + " 1.630826\n", " 1\n", " 0\n", " 0.0\n", - " 0.015909\n", - " 1.302963\n", + " 0.003773\n", + " 1.634599\n", " 1\n", - " 1.302963\n", - " 0.995098\n", + " 1.634599\n", + " 0.000000\n", " 0.000000\n", " 1\n", " 0\n", " 0\n", - " 0\n", + " 2\n", " 0\n", " \n", " \n", @@ -171,185 +171,185 @@ " ...\n", " \n", " \n", - " 10016\n", - " 10017\n", - " 1999.017841\n", + " 10147\n", + " 10148\n", + " 1999.428732\n", " 1\n", " 0\n", " 0.0\n", - " 0.015560\n", - " 1999.033400\n", + " 0.009866\n", + " 1999.438598\n", " 1\n", - " 1999.033400\n", - " 1997.523835\n", - " 1994.215023\n", + " 1999.438598\n", + " 1999.343473\n", + " 1990.171587\n", " 1\n", " 0\n", " 0\n", - " 2\n", + " 0\n", " 0\n", " \n", " \n", - " 10017\n", - " 10018\n", - " 1999.185363\n", + " 10148\n", + " 10149\n", + " 1999.649808\n", " 1\n", " 0\n", " 0.0\n", - " 0.096809\n", - " 1999.282172\n", + " 0.015753\n", + " 1999.665561\n", " 1\n", - " 1999.282172\n", - " 1997.523835\n", - " 1994.215023\n", + " 1999.665561\n", + " 1999.343473\n", + " 1990.171587\n", " 1\n", " 0\n", " 0\n", - " 1\n", + " 2\n", " 0\n", " \n", " \n", - " 10018\n", - " 10019\n", - " 1999.376887\n", + " 10149\n", + " 10150\n", + " 1999.843757\n", " 1\n", " 0\n", " 0.0\n", - " 0.013313\n", - " 1999.390200\n", + " 0.088890\n", + " 1999.932647\n", " 1\n", - " 1999.390200\n", - " 1997.523835\n", - " 1994.215023\n", + " 1999.932647\n", + " 1999.343473\n", + " 1990.171587\n", " 1\n", " 0\n", " 0\n", - " 2\n", + " 1\n", " 0\n", " \n", " \n", - " 10019\n", - " 10020\n", - " 1999.917300\n", - " 1\n", + " 10150\n", + " 10151\n", + " 1999.918421\n", + " 2\n", " 0\n", " 0.0\n", - " 0.037693\n", - " 1999.954993\n", - " 1\n", - " 1999.954993\n", - " 1997.523835\n", - " 1994.215023\n", + " 0.033917\n", + " 1999.952338\n", " 1\n", + " 1999.932647\n", + " 1999.952338\n", + " 1990.171587\n", " 0\n", + " 1\n", " 0\n", - " 0\n", + " 1\n", " 0\n", " \n", " \n", - " 10020\n", - " 10021\n", - " 1999.947407\n", + " 10151\n", + " 10152\n", + " 1999.935728\n", " 2\n", " 0\n", " 0.0\n", - " 0.010383\n", - " 1999.957790\n", + " 0.073120\n", + " 2000.008848\n", " 1\n", - " 1999.954993\n", - " 1999.957790\n", - " 1994.215023\n", - " 0\n", + " 2000.008848\n", + " 1999.952338\n", + " 1990.171587\n", " 1\n", " 0\n", " 0\n", " 0\n", + " 1\n", " \n", " \n", "\n", - "

10021 rows × 16 columns

\n", + "

10152 rows × 16 columns

\n", "" ], "text/plain": [ " Attention Order Arrival Time Total Number of people \n", - "0 1 0.292612 1 \\\n", - "1 2 0.450717 1 \n", - "2 3 0.947384 1 \n", - "3 4 0.951672 2 \n", - "4 5 1.287054 1 \n", + "0 1 0.386388 1 \\\n", + "1 2 1.261684 1 \n", + "2 3 1.305292 1 \n", + "3 4 1.419468 1 \n", + "4 5 1.630826 1 \n", "... ... ... ... \n", - "10016 10017 1999.017841 1 \n", - "10017 10018 1999.185363 1 \n", - "10018 10019 1999.376887 1 \n", - "10019 10020 1999.917300 1 \n", - "10020 10021 1999.947407 2 \n", + "10147 10148 1999.428732 1 \n", + "10148 10149 1999.649808 1 \n", + "10149 10150 1999.843757 1 \n", + "10150 10151 1999.918421 2 \n", + "10151 10152 1999.935728 2 \n", "\n", " Number of people in Line Time in Line Time in service Leave Time \n", - "0 0 0.0 0.003961 0.296573 \\\n", - "1 0 0.0 0.009428 0.460145 \n", - "2 0 0.0 0.059690 1.007074 \n", - "3 0 0.0 0.043426 0.995098 \n", - "4 0 0.0 0.015909 1.302963 \n", + "0 0 0.0 0.015169 0.401557 \\\n", + "1 0 0.0 0.002131 1.263815 \n", + "2 0 0.0 0.094272 1.399563 \n", + "3 0 0.0 0.016525 1.435993 \n", + "4 0 0.0 0.003773 1.634599 \n", "... ... ... ... ... \n", - "10016 0 0.0 0.015560 1999.033400 \n", - "10017 0 0.0 0.096809 1999.282172 \n", - "10018 0 0.0 0.013313 1999.390200 \n", - "10019 0 0.0 0.037693 1999.954993 \n", - "10020 0 0.0 0.010383 1999.957790 \n", + "10147 0 0.0 0.009866 1999.438598 \n", + "10148 0 0.0 0.015753 1999.665561 \n", + "10149 0 0.0 0.088890 1999.932647 \n", + "10150 0 0.0 0.033917 1999.952338 \n", + "10151 0 0.0 0.073120 2000.008848 \n", "\n", " Join the system? Time busy server 1 Time busy server 2 \n", - "0 1 0.296573 0.000000 \\\n", - "1 1 0.460145 0.000000 \n", - "2 1 1.007074 0.000000 \n", - "3 1 1.007074 0.995098 \n", - "4 1 1.302963 0.995098 \n", + "0 1 0.401557 0.000000 \\\n", + "1 1 1.263815 0.000000 \n", + "2 1 1.399563 0.000000 \n", + "3 1 1.435993 0.000000 \n", + "4 1 1.634599 0.000000 \n", "... ... ... ... \n", - "10016 1 1999.033400 1997.523835 \n", - "10017 1 1999.282172 1997.523835 \n", - "10018 1 1999.390200 1997.523835 \n", - "10019 1 1999.954993 1997.523835 \n", - "10020 1 1999.954993 1999.957790 \n", + "10147 1 1999.438598 1999.343473 \n", + "10148 1 1999.665561 1999.343473 \n", + "10149 1 1999.932647 1999.343473 \n", + "10150 1 1999.932647 1999.952338 \n", + "10151 1 2000.008848 1999.952338 \n", "\n", " Time busy server 3 Server 1 attended this element? \n", "0 0.000000 1 \\\n", "1 0.000000 1 \n", "2 0.000000 1 \n", - "3 0.000000 0 \n", + "3 0.000000 1 \n", "4 0.000000 1 \n", "... ... ... \n", - "10016 1994.215023 1 \n", - "10017 1994.215023 1 \n", - "10018 1994.215023 1 \n", - "10019 1994.215023 1 \n", - "10020 1994.215023 0 \n", + "10147 1990.171587 1 \n", + "10148 1990.171587 1 \n", + "10149 1990.171587 1 \n", + "10150 1990.171587 0 \n", + "10151 1990.171587 1 \n", "\n", " Server 2 attended this element? Server 3 attended this element? \n", "0 0 0 \\\n", "1 0 0 \n", "2 0 0 \n", - "3 1 0 \n", + "3 0 0 \n", "4 0 0 \n", "... ... ... \n", - "10016 0 0 \n", - "10017 0 0 \n", - "10018 0 0 \n", - "10019 0 0 \n", - "10020 1 0 \n", + "10147 0 0 \n", + "10148 0 0 \n", + "10149 0 0 \n", + "10150 1 0 \n", + "10151 0 0 \n", "\n", " Priority Finish after closed \n", - "0 0 0 \n", + "0 1 0 \n", "1 0 0 \n", "2 0 0 \n", - "3 0 0 \n", - "4 0 0 \n", + "3 2 0 \n", + "4 2 0 \n", "... ... ... \n", - "10016 2 0 \n", - "10017 1 0 \n", - "10018 2 0 \n", - "10019 0 0 \n", - "10020 0 0 \n", + "10147 0 0 \n", + "10148 2 0 \n", + "10149 1 0 \n", + "10150 1 0 \n", + "10151 0 1 \n", "\n", - "[10021 rows x 16 columns]" + "[10152 rows x 16 columns]" ] }, "execution_count": 2, @@ -363,6 +363,103 @@ "simulation.run(2000)" ] }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Number of elementsProb. Less or EqualsExact ProbabilityProb. Greter or equals
000.7512080.7512081.000000
110.9683440.2171360.248792
220.9975700.0292260.031656
330.9999100.0023400.002430
441.0000000.0000900.000090
\n", + "
" + ], + "text/plain": [ + " Number of elements Prob. Less or Equals Exact Probability \n", + "0 0 0.751208 0.751208 \\\n", + "1 1 0.968344 0.217136 \n", + "2 2 0.997570 0.029226 \n", + "3 3 0.999910 0.002340 \n", + "4 4 1.000000 0.000090 \n", + "\n", + " Prob. Greter or equals \n", + "0 1.000000 \n", + "1 0.248792 \n", + "2 0.031656 \n", + "3 0.002430 \n", + "4 0.000090 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation.number_probability_summary()" + ] + }, { "cell_type": "code", "execution_count": 3, From b3ab8d5f73f1e6f80a335bfed4f36617fda16d82 Mon Sep 17 00:00:00 2001 From: cargar_github Date: Thu, 17 Oct 2024 21:48:00 -0500 Subject: [PATCH 10/11] simulation updating process simulation with df results and by stage and Readme updated --- SIMULATION.md | 30 +- multimedia/simulation_process_graph.png | Bin 41497 -> 71489 bytes .../process_simulation/process_simulation.py | 347 +++++++--- .../simulation/test_process_simulations.ipynb | 610 +++++++++++++++++- 4 files changed, 888 insertions(+), 99 deletions(-) diff --git a/SIMULATION.md b/SIMULATION.md index a7b35f9..8504199 100644 --- a/SIMULATION.md +++ b/SIMULATION.md @@ -74,6 +74,7 @@ simulation.add_process(prob_distribution = "normal", # Probability Distribution parameters = {"mu": 5, "sigma": 2}, # Parameters process_id = "first_process", # Process name number_of_products = 10, # Number of products to be simulated in this stage + number_of_servers = 3, # Number of servers in that process new_branch=True) # New branch ``` @@ -99,6 +100,7 @@ simulation.add_process(prob_distribution = "normal", # Probability Distribution parameters = {"mu": 5, "sigma": 2}, # Parameters process_id = "first_process", # Process name number_of_products = 10, # Number of products to be simulated in this stage + number_of_servers = 3, # Number of servers in that process new_branch=True) # New branch # Add a new process with preceding process @@ -151,19 +153,37 @@ You can simulate and have different simulation time values or you can create a c #### Run Simulation +Simulate several scenarios of your complete process + ```python -# Graph your process -simulation.run(number_of_simulations = 3) # -> [144.69982028694696, 121.8579230094202, 109.54433760798509] +# Run Simulation +simulation.run(number_of_simulations = 100) +simulation + +# -> df +``` + +### Review Simulation Metrics by Stage + +If you want to review average time and standard deviation by stage run this line of code + +```python +# Review simulation metrics +simulation.simulation_metrics() + +# -> df ``` #### Run confidence interval +If you want to have a confidence interval for the simulation metrics, run the following line of code + ```python -# Graph your process +# Confidence interval for Simulation metrics simulation.run_confidence_interval(confidence_level = 0.99, - number_of_simulations = 3, + number_of_simulations = 100, replications = 10) -# -> (111.95874067073376, 114.76076000500356, 117.56277933927336, 3.439965191759079) - Lower bound, average, upper bound and standard deviation +# -> df ``` ## Queue Simulation diff --git a/multimedia/simulation_process_graph.png b/multimedia/simulation_process_graph.png index 6899e840e9a23c5e7a3414c3df874f3cf53cfd01..8ba64e0657682be88b207ed99db39c09ce783ea6 100644 GIT binary patch literal 71489 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 literal 41497 zcmY(q1yq$?+XkqBbVxS{NViBzNGpAiknZkokZw>~KvI4Veh}*|TS8A3umGK6?i1^X%Dk4MceGKSsFRLEx|F4vLZ@&x(gg zw!trOCc?79&z_YIt1?WiLhSn77ZdiaFi-(et)EP7 z@ig^TlOfae@X|}D=`!t+*J-!j;qdy0$r!KQYNy9bOz00J|JSKD#Ub{8zc9qhEn@xO zKmQy_ia~$k;7jLdF{v$BwHsUe6IqXUCKwyNZt+%`cZ12j-v^-*W)u{JGpUuo(&E$Z zrUXAXL_tE7Qdl;Njcqy7i7#Ki{L` zIh;hsC$#lmQWD{CvAJU|q$UJ0x^;K6mO5rYNLJ1&2g zs1Iw=aU;jEKJzb!A7RaozRNkGvN>)vua&05fFWOAjQ2pS!|{8Ef3Q4M_YUy_MdcEg zMV-LOd{X!apOZp~wm?<<*N~79x+?uEsw`K#<5ls!#U^4F4^7*i{jchbie-pl$#{V) ztU+|@Rdi>_J9+0nFLtgO=a1@qTd{o+`N#sEML!d340}(w_NLTQA4ytTniCbO*&G^F zAF;DR7M_j}dQhKYm-jfKsk&+fk3_b8+$Vq zr8PCd_Y5BVJ~t%s8p;y!lgbL!c4}^7J2;+P1kG~IM+^VLC2Pnt%_@FvEf&2?=C)yp zeg%$@A|!YS*$RKSztmyUE+=^R!zJr|9!#$JCtRVLM?$qz!+6P`q`!a2ueG2(uC)82 zurXK!Jb2w^+blKPvqH6b0yTQ}*=DbI|Ax}TvxB4CbJH+o8j`X^#*po8W}SEEnMpL9 zP}2$ozO}sfM!j}G1orQDwmsHQ1!NHtLk=uIOk`mC`m(UaZm)-3$xM=KR)mRgT zf+qTq|IargNn6)mJtxfTZyl2QTO*nO_u_W&Vg?%<8=3EXFIXD16xzyRcF=MWI?~ZL zv~OZIG15D%z7eAj358^Jh3n5J*=@~M70gRR-IJf~4l~E@@^Gp9>Ugcpuoo*XAA8o3Ji%mGUhQ&h6t&i8DfxS_2J@d}>6!({5iA)U!W^uSCd)BVRN3Q!|fln99!7f6&7ZEdC<9V_3qR=m1{q5DDl%g^oN)45Y zXIQxz+a)+VeDyYEI--r$SII&ZjVd}?%&h6=-4%{E@7ObW=$7wj0~3w`eBICU)DZIx|W=(Oub0uFp2t)ydz*figkJ&L3tF}#?i^9 z0WCQ~K}$k_iI2zS>OpVRJpwtqX?o{$N)vpQU7c`fo5-tAWd&V59Ym04%{>3_B?) z>1Wn*720J?gJ)g%Q2Q0p>nDdWlX-T@9hn$q#n z`x8M}nyeS7E@08ikR!$|ztov5XQ5DT`f+cz45iLt^Z&kp0lt9D?YO1jRQ~k>WzIoj zSZNi6yw64z$S>^iTGJLhzH-}WKXJD;2el>LS>Myta;v;Lm1FSn#~4 zM&Ys4TVLDRTcq#i{+~e3#vR z{ev1cRJz1+S0ZsDnzgpijX4Ju>g~*&Qps}!y17ZZ|M(J@*|v$C9WLppgaWzh2SZN< z|7aclCA8-DXLGBjA-zyhz0&26_TbcF)77~u%RhPW{<3cCbTRt(Aa7#gkOW+W&-Ekc zC`U?iK?CDRo(1iEquZya6Pu&JiQiK=E%2bRm)svuf{;>Oq^^WgOl|Y&m z-3%GhkQOE#KRsB)zYBSd;elaE+>F-Xau}48lT!%IFI^GL(8pVSVKME6;EGJDjE}Mj zCib*Hkv>kjyw~CPxRCjec@SyiP^}i~aitSk#KT)OUU@w1l}nxFsrb~ij~)Fc;CC%8 zgt~E8#DZXG%=gXV^z2GIGn{7q6_6}+kH*aDSXs+t+hY9%f=@uK^{^JP(Y>t}B+SEG;BQ z3inCve|3T5y@5twl1u(5n(UMwWXSyZ9t4{@Ke6=+Y0W4Q5V%DJB9h@Netr^c8rk;? zf=-Z9PhmCcum%Y#;oUNQQsb z&0?Pp``{0h7#bK5pnJ14rYAU^8r!JQXv$MdA$Oblr!SuI>7fwtOz+@#4uqLgW667= z-G1cJOLjJ{FKQ+ncLKXMYS4H9^0$;?*2H1D&x7#3cU5L~HW1QMiT0yAVlBZJTt<~1 zWT1u|ClED0nm*iJ_k3dw<{L$Uc=l=Vv&y{tu7BWH_e8cCDR6r%x8v0pc(cr6k!hTU zfpp#z2g=0Wn|Gydr^e8?WmQH%$-X=EdF+6iR5o!3IW;Q#MTvez*yl#WGN{TS>QLJd zOJLG~8Dsf1GFZ&WD0W;TF-62fR7$iXyQ9hbpI8zHEQK=h@DRMKK71cv ztqkbZQwB+hZ`-ld``-OOdZ8lxW;I`nsdiqTC1nvLBt=44VJNmS8DHj2)d6b2|H$nj z;d+;$-ydclPG^@?`NsE>-3G(}VW_{+73l#Hiam5n9}Nnck@#-^In{|N9*C4!ppXu08_0>+C-W^~H6bwwqvvBw!#e*_4tj#PRu=8%5Ps?tAfR;$ zolwu6*;on+il-OnK^+AKr55W8e-NrqT;n?ZFE?53CU@l~kU_G>qk(vzH@Tl9Q`Crj zz5P93E)vJT&X9^C^z-q#u(0s|98vFf`pZ3~LP734fsVIVi|sOaiVo_l-oOfwwl4PP zcC4IXht59s%A@;}kZWf{#c~6UeHkER=hu9!8|aYsA?MMM@2=JKXDj~mhUeX)c))z3 zDcNG+Qn}S<6(?%XB+i(~0XGgG*GLfFAv@iGwJ&7+u3s$c%7tlvL>0a+cR2Vw`ws4p zw1nQ@rjMqV8gQP!ZYkL1F3HJ|Te01QD7~BwRmmm}lbICCzf!y4&3o9&KZ(iw=`0aZ z(Qj(374uS_Z3y$2N;2fv4s^s)&p5~?Fkx|yX*lLZ-+PP=8fuO2>N+WVG#d^cYjKQ zPPkj#5YvVG*p2q$x?^tBmjZ{cjFPk^?qp-xZN9vN$-4CRJcWCX!mXEfB@n-9?=J5G zCyrFvw^ZgKG&w==Tab12*b9d8o|${7y>8HOuvsFd)T*^QdW|%GX}j>`Dz@vCD(z9P z&%3)5V@JsN0h2YY<*NqNIA))W133E6;W%w;>a;zT1-8S{*N&FOmTwP3K1TnepF@Kv z%|!6jkvq%YE134NRS9}&If+fr=v_RcmZ7gJ-a2wi+EBvy_B$8ivnbpjy&#^>`0@BR zs%s+!A}h-+)id0YTvey_z9hn&-;BP-arO}xcB;yuXBFC%y4w_RczK~w#T_H{cWRcq zhrYUXm%EflG}7*kXD4k7n{7vFPVn_s8rkQa$gR(*zg$d5FRNnqW%sZLJw#(J+v5k` zsAYxaS^9dB1>7_5FPm5f>fy&Pv6kZJx7Y=1?K>Tf21(NS+O>MRZsrCH$%Ps4Wwjfb z3|Jf<>ou;6N=nwwGlfnD)9Nk$C@bw*^LLVsf0hs5*>JSA61kThL_d7Xe^*rxf)kW>|fLTA}Dlo^E0Cm$u9CokiI2A_<}qSHat$ia+P#EEF`OBpynXw3A2aY)qRy-6vxwYg8#%GcLbTt%2g2H4!bZ9 z_QsCPyzQ#9a)gw7h$@vN6W3oOXSUl&nZCPxDX+h9$_DG`V5r(0*D!3Ky@NwN&Pf-F z3|pUhh_zwz!_l|qmU!!5f&r@2;wv9ngLPK5=Yy(FqJt-W9Hj>2n&s-PSyt8?zv=DQ zCC_I_AsAAwDNZU#QBq>K%kKcu zjhF<@5WV~ohc^~#dosgiPKv)a*g1_WSqlt_r}e+B!GhV)nh=HKxq1HUrZ4S7IZmLR z#)fl;5uBiRXshUAAA8_eoeI@&&&4GpyBfVXIjg>;90_hl%w)~|x+sXMnDyIsxy?Iy zd;zgnLacUj2pZ-c>L+M2j8*BX)$$_--@w?r(nGV!H6o-c#Y`q5h!$JmSi@YQ;F zMNAOpb9H7=MSk@<nQLW`Ep9P<6YmuCP0?>zPR%j7HJ&&URyJ@8TBmGYp3k&t zPXhg1Y?i1itb3tx?GHIzZ>3aM0x*eNDnHQM%4!TD_=#VxfI zL>+PCeyb=bD~Cy~7SFGrE-Y+<(q+rjH$?TMIRB+owaV%t1xD(`&|O|wYj(l&9>H21 zWD)#p*@$=p%@s`ELi%nvDkKLc5YBT;{_C$lG6FP@~10k;8 z#a6bCwE{FhS(gahlt%a?3UO|J&f4NNu}szt++l{I{w_-TMG;*mSVD4v7 zyJ@TT@ipzGfdUnN9YrM3>+WoNyN)?nmd9Ggm2HZ0V!~_@6Y9M)hWzhdite>jgi+N8 zNB-fnm+Dl=!?4!<5u z_E!uv@>du&(vC~sXm&IGtZeN$ihbos8tfFko2gg!K2@DCAh;_m%Y75iRPg;Bn!V3@ zw}v3n0;PQJ2^%8W*kzkn4msJsevGvx^UTrQWICd{Q3WJRvg*0zW(q`131tNvIg9ZE zvTH)DUEKP1H6(BEDfxw%F|^m9Z}l!y*_B|*u>&~$i)mhe%_GAkogddW{o*X{J5-rs?b z1U`C=g(H`-E9j)`c1F1C%bwIr17>m9+UY)MtQf3M1@<_!CB{-Xn6EqD8|46{aU;La z8Ht<2X8ivoanI`$idE$Rk~xXv)kqSmT>jrHqko7L=Nny*V6IQSGzX#cWkqi1+t*U> zL2Dx36^479IHfbfTEtp#mM7o6-8BG5y&TSHkCOW(YO}%XGyjiOW0gp?t;u>~8rkI6 z|3kWDG+CaOkcZ7~_VWZNtwPW^tMbc`aK6PE z6x!4nmnHMv7(#bnVyLD6>wcaJ!9tnuZ(MEF#`;9!Gy^ozg?s;^dIK?=L3<|{MvZDF zYn|pu?vh%T=hWmbtc&7-_PGC*kuRWrUnufo5b0tcrDj4SYyH)_XHW1H4YbC53JNlg zw*$!njMiy&^o(L3cW3TRYF+NhWMpIvq3!)&7--+5P-U(>&*LNt2rMFQPM*qg)q)nz zvrpa3Kz<6vH0@{TXBn}=bdY+ulZ}4bCsE=-D+U-24vzVHR}}dJfp4JUaJcVu8ZU-u z{7*k*K3SNzsD0j;zlcR0UxR6bxlH&)y~_MQG2;RbLSIQnkz94{;cN1`i>rjhNPLle zw%Ftq44wKu>Eh|3pOAZrTKlAx=$Oe3(L_B%%YZEL@c-ZE*%GxL`z%wp*{^Yz@xIWs zaD96+t^_EVSnLO^U>9(;e;Et9R449b7HjR=&_9vH|Mk^RO=#}5&EU#oNegJqH1RLA z-xu@OjbsSJ{?{P}fEEXn(`J!GHCm_3-vR@4-p87@bzc$&HTsyL?Uw~KfsER;-2bX> zOr3F(Pzk{b-a2tR?^LcdpFrm0<13f?e|1V}aqFRxy5Ow>q>8RXtFzAfl^hQb@4prg z4ccW|Ocj$Br#D3bkpsIs8shmO9DG6gpVFUZ0EN!`vveBGOmfzOK(R=>S##p^NVTxU z#KixR3!MQVIqv>NqvcK$>nuQ2qmbgEC6UVz=|W~>Xa9x^r8pc=9P>VrMeB&~jk)1d(LlC-}(yfSb$9p8d-Ww~aB8WwyQ28wEAcGQ7 z;t-2LgaYbEEWkuR_RPKc!E4kAG!ST=6W;3h`4OV5wl>3#jDBoPpv8m{m(FljBw&|N2zYNN4Zhop0A=^I zVtd&2`#rV0d+=Q+Y)UAfR=a-tQry~I=h}k(@RY2Ng-uyJe(AHdONf>-DglP za%P0#Po_T=phY0y?ilkzNOH-^ou@D3AW6T-@*pCL^A?j}?@kTNoQ0l-2U1s0+)J&NZdRSrCDi zv36kJ40%&!+D4iBc*zfTUUZCNkOh9PPiWp?BriMJCLaPG|IG1K3;H+~h6a#qS ziYVN<&Z>`9UK6K~}q@Y6RGUBw-PgDToCz}r7MKJfX z>#C{sw#Ys__r8r?r+o}LD#SOIt=C$cE6yWm69pChC3sIF>VigCeLhjAMv^0sKd zQ3nnAE!>e<54tK$X+G$-w79s~^zYBmLPf_XC7eMg$@Jq*9+x9WQ$dIx#6brs^;E&a z=yk!0DBa(pct8{aLWO5!WK_#x4V|Y}nx6`K?~DXyW6*dr>gVr2x?V%$dUAkN&+F)~ zEnNNV7oI+;P+8g%$UvjTTJO|yrvKwb9#mF2wl&rZ?KXqn9Zj2;BSu>I>x|#z4>IG- z$695Nv=1W(Q@FEbQ@F~dmKc7qWIFAP>sh=4j?W8-%xeXM*U>^F4meGW_rH~YDEySd z1VkotE=(>D3~|`n+lzpuoKgI$=ow4qteS{8!E0avC{THaf*H!^s_J&(OaHCPEbKlm z7RE~jsiVCIj>6q4^vj_o|1}lK@d8yq$<-gIhs3EsX??R$@6-W;_tEX%5+qv#O)Y1A z@85RH^t1<6`NLOS4Q?y(KxEu?GAYy1h5-A)_uzGd-U@<8G@$y9Lk=nS1DouwN6W|H zsbQUL$ELsb7}S!@?>zNvtfKRA3-m9C7U)z8ljcQ3h2`bt(cH5(`wRT&Ly5LvQFq~f`1B-wT_O`rw zP;-;Af=a6n;5b&Jg&q{9!Y%mA%WlL!Zl>cxo49j68c&+#)i6#V-**6tx z(~8hx`CEOk&jD*9HmNC=NW3xi*4F9w2brDa9nJ{o%z^<>g9moq6GI(7FG?TC1s7JF z`DB0eP>Us6U0d^i@VN*EbNaU_R;9_xH{fr;tgng0h5V-fjGsd z^pYb6EW&IAtWC!!!xTBC+VoTZZ=39i>_Zf_5bM%m5QtD%SjgsaVaZ9Su=5o7;$lGL z&Vgj%+IH$tEjzcOIk`^>$F?ND`g9><&+5fmQl)+6a?{e25&N9u_pl+s2iR#SgE8-* z3|h~W8P|j~bA6Fp;VSq2%?t2YtOq@_?vp@^ zfj|+5(}lV_L$TQ{CYf;FFa*^5Oo~r%DQ{?pMG`l2+uA+|r_Q*+C$y;P-_;)HsuU&9 zOPgUr-5FvJykCr5v)PjnplnndiU?J>#EJx~6lU_v=cU=}wmqtDRV&m_oc9crY_W1E8)YS87mZu zZ_iw(J(dTGIdV0N-UML~Mu7^fw@HlM;E4w*AnG$UD82yXcRd@M=u~*#W9i>IN8y94 zJ!*8n`6>5#S8QQuC(~PT4;gc{YPjGp$a9C*KjPp&sraKWczg5CzrmEx^^WujMW2Ii zT@JIsifJdQ^#pHY12a$KXhye66s>S13xa zZ#m_8v***O3|!$L`vbd`SXi8W!a~V+%0@P5Mn?@z^Tx@L)@FKn4qy~-S+rJ z=1BPon-?WA2B4U z0fL?F?NDr5x#cUL?g^GGKU31TbY@>PyX+}kM(2Cfcxca%w;|_|0ZtAsIID z;bLP)_z1r1#SvlJyq90M3mMg09wpM6%dJ_e`38H8=tCiu=n2-`XmjQKmBe%(h`)9u z5$9KS0|_T`h1psU4V}PR^p;7{aJp{A$_!ne}|FZ+B+$mtXM@ z-DlFf0L!V;zwuv88t^L zP9#q;N7vFa0<^Vl0O3X^9#{SpDY|gMT9BF;Km|krI0VFyV9+2_Ffqv#HMt(cl<2hl z0hTbn2B2-hD ziRkJBLFt7Av<3=U)F+pf0UUymNDri)wZ64KSL1qhl*!k)2Ihb}M^UOJ+Ib!C#NJ|d zynAEV^Gr)i3*fE9Nf*Cyn8>a7d=q_uyHWY?G!k$cL>~na@k>ifi#ub2FV>+b6tU-Z z%LVp5ucE_<*%yU;9`5Kqm%j48*`_^jDijOF+Uc~GkdWv}WKHLf5d?q+P;j6@BYT() zL6I8>V4pNDJIkd3 z&B0Mo=%5?|$QMMXpd+yMUprpwMnpsF0=al1bj5n1K4%DvWpTK(a78|iA3`o2&<>zP znLr6w0P+H9a(9x0!3QB8^bSOwzy!{U#FqJNm%q%$E`!};t;1&2EQ4y1Iwz;Aa^Yu) zmi9-cir4L0BG}Af_$->O0K;N?b7ls`7J^nz@iKHN2P6_UZf;3`c(n11?+GAFgX%yJ z8Y0S8R#to$8eBRiCX~_C09-}_EbYd1l`D*8laiDBsA3UDrM7_4XG=z}9<6kkzrrEn zwnha=Jm7*j;$b)t3+PM&9G6#Fvzle&8LPSLs1E!&?r{@dCExARe z^r2fUH-{~c`oOSk?d%{^n0RgHV96?(!es@8)3_P#K+$VrVi2(dEQSLM(N{pBIo|s4 za>q-L0HQK6Sq@At+i7>wbRs`~Hix6Qa~OL;tm`MMerg&)3BLU$%ag5UX1bnSmGKF( z@!#PtN+j=dcm?RfPyq{-dM7rb(d@~)x4%DOgeUg0yt+DSOq?9^q;r_p z3Bdb?Q`-VhH|;hrtm^Q4R5M=L?-IX~EN<-i6;M~mdzO*HN&IHpSRx1vmOC)WX+7Pr zkBLfj0;xdx+30$KBi|u%d^Rc?u+S2YK;d%4bN20qqw6>)G6KLid6l@>h2t5N$IF7< zrF0|EFz>jm=R?qlxt5pP{LQELN3#*!FZRAOBqr!y-J-JFDtkDs9%yZdUo89b^?!9{ zj+rXaSsR?=9)Ud28q;8L;D~Dni@hb?-QD%6eM4am4=2;JfpCKL0Bew6i7Sy_Jyzzr&q3n5tIaowjxlAm1MdO|q>E7oNm9$6A zrFR5Slb`GCsZ|O;XBP8L*VyPlVMafS-d24$U6BvqCnqO&lA1l;9A5J~$*ObTV9@5D zl96XL?2kuNb1(@kPGfmv+#iBkPk1C6M4oZ#A65UjFz4cWyTeSWEw(%{rs|4$10zpP zM{&H}o&Jr(Ma~H=rd+}Z((LJWF2rJIM9FRpic!ii)}=A(O3`3+^TZ;x3S%&pqQ)KF zw*!;P9m^m3s5R8gvan-|-8!mA;tv%-xtzGXX56h2p<=v{Caq&;mDCagf2KX&vOsvN(kN5IuUHXM< zAej16hz8|up3rM0qxdAU>Zp|Phkf^nawqFLx0sr6Z_c{@xNqs`$^6kI8 zgL-z<5xwLbzdCWMir7FJB*{&-qPHwM-{6jM$-2-@+VT#u0sj}-r)GV(@q7phfd49* z$YAZJ0jLVKi`_n!`{_SfCy24IV;BCeH> zO;OY-$J@7Yl7B2gluY8bNvluDG<`wBYyWe$f;C49dquvnr>jt~*rN+Kte_`}mntj; zPx70&xWZ3!Oq^mR0yTT;gJkk(Yr*!xF>bs2mfc$KRlef1+Wg6a2Swb3NahK8Q*j?7 zQ1Y*{xPugfOwh!s5FldLP9G#2!p^t)(*aZ=0HjL&cC_CLbgw(FY4sJcG4(sK(9)?` zrVF(i@GK^aMO!zd>6Du(^EoS9htG+#6^>(0a^p4!$~t)LbTJw)4ph3)xbCXh>zxk* z04|}QN}om@#$+HVvzXr0!Xn&m?N8pJ5xs0an$PXU{wI?|yZM8Vk>E@^0Cbj`U=951 zB%Libh;@>?PIYI-0}}*0U%g` z)->f!iT+~(0}_S(8t6a{?!8sw2mr&2A$hCom%n9~y&d}Tu4VQ-EM!$~K$q8i zoX)9SWORp?hV7bU6idjaO@!o~JKRS8gP(iRT()&Mm%Yu7GwT&Ry5$_AizEVny$`v7 z`+0jhEPMtSIG%*$cBWEfar^n70w$zgCMT(I?IMueWB|$%L^1dC6mdj($cRUSy-ofGHHBh-A(+E8a2kx zjys{N8`Gs3PSbA0no~?2xGkq!GrJhmQH1w;fAhD*xD`gLs)haGch=dA`V-!d_VhLuGhS(GbJ0U7&S0uq4$T>oV>Q%^~WmXdXAcGwfQnmLdGX2>m>73qVDN^$J zR&We1Z%yZBEw8oT=}BO%*BalSDAMp_Y?@cU%(UfFc`}2nN0Do7S46;?UE|=RwxZG% zwgC){d`d)YxBRNfZaSpIrb%h6>Qv*J{H*(1m*n!{K{O!W;B0Gjj|)0&ZU-5?*-XmO z!^6Yrn;B&o>1aDky-QGFAi(MN^+_FEe@H!vCK7<>cu0$S6VK3*Kdh@%{7L5^r(~wt z60>@G5VAOiY;aSVxAO**9dqG*7eOnq5&iAIIXmkry&uC6eUce*)|MMP2ys)C#X8#1 zAi4T|S~^q|sY?!I)(#7{`|F(N=FtS>DO8f^P&7D*u-}h5PcXiL+a+@Q|LE0UchdLF z)!0mwY;~%CVOFpFsu4#NVpK@?5E|Iib>pDFidjbDn-utlZ#2h!`UtbuYm;jhg9xcV zN?@HVEIgRPrPOGJiNtn3VA5)KHGQT+q1INf?vm=*)4~J|7r!EAtnj{9@Gf-QwCmM` zzH)fxN>315e)=2ZzaJUWEcnZO{&@fNNI@U_Ehh;|GbqkGs}q-uENWzAgia^Rh|(R@ z7Lv6-kIn9k7vbN9mzq3ieUNkD0SH{l#PIhOwMaUmPu9Dv+9UEGTgZIN!@{LH&8Ytd zHL~wV*n8qxTjr&gcGffRr+G|>$msOk59WQw!<9$1TD-N2$OSz=|G2)g*hQpCFeHA^ z)Y4)%yn5;6%+}AIkULfbVC_1mH(eIBGEQwP)WLdsX1+5O#=n!cO}0N7^~FK()v3cm z6BF?nls zJsLW5bbGNIO)HE9*6*JcaVIl_VlDYTd=Gw7oq>Y6$K@1C|#%|s0s87g*q){6Zy)q9)ei0 zF*3%na->IV?F{NRvq8nL-}9AXYwR}$ z9;-G}!AB~!Kq0#q4K6Kq!?&5CQwMttFN`U(;%RP=bUuoREuyciI)Y#n2xE06 zjjkF_cU|jxoHd&@d?cBN*`Wh=7Wh75_#*71A!TLkpv*+{^75*aTmx;*)@&1krpuv7 z&1kIdOsK&vR!8`6d0inh9Q6I!hZ($P;A{DrS_8(7AeIoA=*yyRuUo}vl zy3{~@#|N%Tkf}vfzhBz-TCtmjjn7wVPZRK1hh8tBltOYoT#O7tBhFGOQa5)Q-fBAp zO;7H{DJ##R59TM*RKhzf$Un;&cHIP0KD3&^q9W$mImatQ%7P`IN3DAoC?=eU(>wt5 z%Dn=ijA{Ww>;xynLXTo=-y%mHq~+IH+J9|dspQMiqdJpE!2EhK-X#c>O#gJ@0%A`y z*9JMo(d`r57 z15n;#01*FltAbK0N^BatNJ4+qw#@SV*)q4$j*=6q>uIr%?v{v$;m}Y{bByA~?7yVd zQrKZ_$qb1i^&O&!Z{RN1#~%@r1LjN#LLc{@B^j>B^J5xU4cOk!Sl!hf$}3$(8mM4N z1K4!-ZT-P?tr67#aZ5-HQn}eXCwl-IKA%wjwbmqm|kL~FIYzv;s=ypReuiI!ljgFvseHvAoB zv{ITCjEX5cqJ{K(?r=?YC*#%EVmQ*fjCrMI6$sSaSHS>n} z)S5myRTY>7$q)iRw}ei3mVS603vPOhTPVo?u!17$Lt-XDZKG#o1lD%Vp4awj_qkRc z7KJ)Bc-aYs!u!T5DsDmlum8+@q2mI>hY@3tCTiX|g( zSPy|bYJ`UwR~f}C!t7FHk4VA4XMbm5sE;=S76dCkkGJXda=Ic?aYwWdUwvT_vU+u7 zd9qF3Od2WU6Wp5z{2EMd@8}Q(*BiF_#f~`qi@@UVMkUK)xhouh0P)E_k|tu5r9N85J)6$5uyJx$8C~Fb!_H(4x+Q{l&P-gE)2|ASS=&06pv^nY zfJ6t;$`ocx9|N`O4^?N0(0b~TMk9x+M|Mn1uSohojV=CYp&Xk2A&Pm>dNkG#TA70& z|4liPz1pD6a{H0Z@!(ORPR>g$CoXrb$L^}iO42DJ{d1B~IVfNBTE9Fu=fPPM#1`=j zY5_vd0K2x_vES%S5*k2pE(?=RqRr*kBRiKWi0J4E%1_Y-QNM8tTsk`j5hCMbEKPYn z#lL|hE+=Gn*(+jW*<`R;Gi;(6dVdbzk|o>VTx-7(R%p)jx%An;fB$xvO28y6Vd5%Q zcjXXVpz9+aho6{L5$(@o2I3EsE8ts^{l#rM){M5;?8R6B+G{bKbTF{XjDp<-SKnq& zuxG%uVuF`r_Ket1XU+*o5)FPx+fVZvCF4gG+S64JB7Maq(0ymAmR#wyQwg{0eEv)@ zJ69=55i3%vrcYH67XvPSpr+t*)!H8W&ANkaX(DyODz>_`1XAzyU6tDWUX(d* z|D_AB)d5pq(m01w_@ddY{V#gpYLoa4D&dIFKM^?i7x~!NVk)50a2mp4XOH+iFC<9) z;c9n2heJdssNFrUs!%~HuEo&%e93n0SERWr9JE8DM=gex)lD=(je5~2xmCx`Cdfyu z!QvBhqtxucthZ+4i?(^Nd{638>0bT3y5SD9b_tE*BwSzqwx(xQhh$W1x2{i?lQhhw+^fD5c!`#u;G?T8h}r{=NngN?av$_4@^~ha0iB z9&J`Fid{z#HASwaA>ycYmTK`k6}b7fe{L+b*laI;eSaHlcGd2bfR7p(kx9#S&dk=e zk6wI^6nyV&bq4!pd%Nvbv^yNc@95$}WjN&#^wmkq)wFm+;{%96r5-fsxa6+vqA5GjlR^nPk69Fa+gmI6zP%%i&Nvl z`U{Dse=1wo-Q>{GY@zu;3b(wLzXyOvmY;a7GT$vUx?%gAUBmwRAv2NjJ}(^c%q?sa z@)#Mqs`0DPosGeOvIeZY%Emi98WVLWpT$VjJ8q9-#Tv1`*%qZTRkP zlDUc+&Tta}htY;D?cb!PygrBsBFQY>Ey`RgMALcr7lwdbaib z_$<$|TpTKSF(DP*AMAumzlH5utz2Y`3%?FSVgeWY_qs`}dxen`-So!ns4TuGzJh08 z6ug(XeQmdLNDLHo+?WQf`9HxNAW9PuhqORAevLA_heWa1Ce-&?a>MRNTsIv}ntzP()@TjFjm@~=2Lez0*^EM0PgsM`47W_bEz;D&8uC8H z`#Wb3)m=kv6e!W}L zV*7CCrgraqjQ4PZu@zl-qjr2t!r~aGUX?t7Y>hBK&XOT^%>1c3^@I#F2ai%0^sJf< z&S(|JgMOi*NPP*+1!)5zU4<>tsEgX4#+gTV803;k;&t)UwB6+Ewh7Dz(gX`@=x%MS zzr>$-R4Kw_5lweLz=yv`JHjEiYOzreNmrcHFACrobO{|tTp+!wT(8jg)gl?AXrOR^l0y&N7 zuhw4)d(Jm9NOfp~D~jHja`bs;Ub!gsE#D(=Xt-<<|B}Ga`Ie;+?wtpbTY2cq$JnJhq}UE@RlAWVBb6#*vWe6YlQrbS*3jE)u}bB(>1|jQolg8J97NC7Y%X zIS031tvstgsdu{Ch$DengQkEe%fl8FI}^{YnpUarLeJ?sd0PIBrBE$k9M8ta#e8e0 z2^`gzLUB4t1lgsRftozIpnOMpg`lL*dn2rba<36Kh;vNy+a$eNs2=fs{9zaqs&<9D zcVj-2anCz)noIiVm)kqeA?}6$z%@2iITpdxj1R)aNem_4#Bnvg3y^(!i9N4v^PAxA z@aTf=u$Py-TYBm;UI(3r@4DC4mY4(FrZZc*OfCvk$9!&AC(ec+8!&S;S6hO608}w& zF)UWcj$*W@2T{2|MOj10&o8Uxqi4E|`%SJT7P-K;Gu=fO)OuC!fV5_$LUo4BeaBY^ z0bN}8Mt*5&&+WrVE(cz6J<9kxVLMb39fbH>Xb5j!eV#tV2-YUsC+M&xn5g&y`L2@G z)P;G^K%Z%~t2Aw#;zv)47N7oWghS-Ai1jsOI zf-G8>KqP`(5?2ALB|?gb`~YK2@5@QiptjeY=*|r=6Qb?yjk*d-*YCO{XuLj0R0r18 zOI{bXAG{wPd3i$^7kTE+wrz%@;Wa22mcnlECj(#x(^GrGPq%$8Ye55t>TRYBDrD2q zCJX?p@@suR=#!|vs_z>f9zM%_<>Bd~2o^)FUo%BVY)wVua@BwfR^`vtu=H`!g$s+W z*(}p{VeBib)ugcN#NW^2f@_p{rF$C)r$*j8@rM@K9C}ezyg~b;K?Kx1PH);rj+|}J zRmrH@@hke>81(+ad}n>HtD~)Yp?R1zj2!9^LM}JEdcW(tVaqnQ*dZwfA9}ge9d>UA=oqp>=CDof1 z+|pASj(>Y~l}pYhe_Fuk>A! zRSGt0{7bsSe3+H$Uff)x!oASf1%ynUQZfbe%Y=By!>|{9?D2X(X9{}Y#y<9W#Hb3% z?q{*?$zzBb*!gFqujNlvJqBA z9MRzDuegx_0Gj1;xJbZlJx{v}^CxiVw&#abi*UaR^NPm=zCQ+90 zH;dhQ7?uiHK#f=7wOnWhTUPpsmom$tO-21U4PH258w@^$r9HF632FWvwA#^AKjxsI zcD>B~*$s_FtAnlk=Np*c&8?IuT7wUa=%x9bN;8Mi;<9Hi53Er{Pi+f**ip`%s%Ra3 zOKa$Y$Xq}!D*0#PAuESak&UuaBcEnV=lPpBBo<;)Tso($CXjct z+A@b9a$uh|(Yd%t))m&}KCRXy3A(a6KADt zff5zE7_Isp8Xu2q|L;z!26)d-tKgQJy}TwR39hdkItn_4tU zLQ6Q-jh0Up4#c)5;hdGqgO^l&w=vb#4}Z9(aUMKtBc+;EDRzccyP19!Jf92gKJqR! z8)zxHAkf%I`UqLVuRKiIwPErbD)lR+|9#ENjx;k(^rD*K&_!+%B5LSJVV%VBQ^>dh z_MhmU9{0S`7#3Z?b%)-p?ex%z|L75YlE4s=gxW?ktLbDR;n~?)sq?Ew^coz5f_kZ#5jxjiceRixp*PPe&xfmbW z4leLBFnr?xG#5M1VsDMBxpcS~1;6t>EoiT zVCY?f#fsfHqsp(4#>=}-?57StZmW(U3i_SfA;`V}(V_-jT7BUG8eS(MrSDf(hHbXh z$oDL+eqJ&{2l90gI!>5-s(OhE@Dnegs_S9 zTZA_(X?RQI_nkEaOj_tSrZ?t1Eh#|M_@?GS2d=U~WWCs^rA|-X$qz@_QJXO+OIfE{ zFDT;8pf8))Io0qsIl6)Wi4b(~sC=qHQLN1TmwM$kShQv|84IEd#enr0Wph?`84u?{ zERIR<$4Q(3WZn(vi!-Eh2{xlI^mZcWd^a&T=&^W-a#|QMx3)*O#-aV}?jB4_Kc%TQ>;vOnpZ$FQ zY+)frRrtdaxj2u`=LcWyHRm_}qsTn#Gx5TwhlFGZsQ60H(=Nj4`ILKNB?UOHk9(*l zv^jk0YQIpBlScpzQw9+GTOQpK`rYSp)$oWP9aAvs53L;4 zRk+IY5ag-#ki1)=^C2{lae<=m8lh?I*E9_K?ekkrpK}Lqh1y-+60av<4_FvSQlh2A zT)IN0&v!6+RLSJyuLKFV1SasQcdeBMdU5Z12HTQx)bkprv5RAtbNQF3-j2myvYZbx zpf^7-96rdOaT}b^AfAzJeOA5f3c~&Pa2k-?jB}Rto96Fk$WL2nqxe7! zBlSy4h$3NPY>LA0MLXmixm5U{HMY!kyZ^>4S`O#(U{0*82;f3rgLuR);4?tQoLKR- zKhtn5EP>Y~a~4~j1q29@_0f*hJbqJ5;oEcDbiQu2SU=< zlR4s* zF5pCS=LOOnxVGUh9J>_xeBMYhjrHy)XSkEl&Ly_a&%WTlCF)?1u~$_femr>=LxfKdUXQ8fWzR{`rfElTFDRsuL!p-qITCFcD=Uxlgl<)B`wibbXhb z%#rVQo!Ke3a`qg=A5QqoX1B71;~b#!HtzzREoF_8xwxLYR{b4-YZnVN_6f9^6p_67 z(;|NA-ke~Ws_M~*6(9MzD2KSRA1_?adlWi^Ze6K{)DliPvJPLt>T&a**C{r=ro(DJSEK(QU{vsYwz5g}rL0U*`(+D#P~kQkWKfi6JwW~nKH)4N_g3g` zrf_?QPacIV;%A11tp^Jh5X^vk_x>?o`p zQWAQIb<`QQlH+(WtoRVwtE-~sAvbDQ9=_pqc-3a&Ad(;jOB>-7ZHY}xgGr+ViCg(@ zwKAA|QNEqA$Ju?-+=3YYQ@J4_2~#A<-!_pXZQ?Gxp+n*x`EE&Ld0F_np`|4&pqHcU zeU68ubK0GA#{Dl&*u6J^_Nh}G63s02U7vYvrB6^j2Ur=^L9dIBg4m0r^E>-|&2wM| zF<*FKi9D#!@1m@H>!|=#e45eksDr>@J@wp>$9flSz#>7_Bf^k9jS!iM7X52J zBc(({2IuDqnPUqpq<-twu=>31cE^k>-|RS5r`QGKvMXKd&t0ayTAC!JCnue50gzal-k>0rZMk!Y zKN8k>^b#0^^)f2Ak47X#IG~oiel?VYgiib!v^W2tjz6%1N9n8S!p%$74ZGa~IScUx zLK~%eG1_$p;Y{6Y81u*Z*r*u(o*kiSNpheM|22&;Hc=QyC3$p?X z3~UvZpq<=a(y3`FfBEnJ-dOG~%_BG*x{)8o@OX1XujMwfSCu8i$~kKs2Pk}pWb+N*tyW)m2U*sN@IQ<1d7dgjg9-V7chqTvbqS0L{+YJ($~ z|IHxtr|OcC?6+a{jqS_kB$VOyreV6wU=RS0-IgjUTHH#ueO22Y>$covP&od=ANN^;xpdkbJ zwOp*-LFFEVZg>^KfpYb~ie!vzV}OY10>aaYBM9pY^U$D}5FVT|rFJT1Yi{c$1S|E@ z_Ve8-6G=|5iUc?=W#Y!@3t!GtA$?1Ylf@x|p~e_aKGl%Z6!bDEA&)crZ9@5Y2P7~SSP zvWXry>7X@Jky(BP2uY>;{GvR&Jsh7UpkvE!R%;VHt$)pny$$!4s;s6A7oOnl9+?4k zd~Q@)dy&(4*SFB$aso+@W-wkCnaV|#3O82l(1{BTFl0CvPX{o+Jk?xGU^op1LZJL; z>a*P`MT#P?qh(PrA9PvB|1C4nd&$}tdy>$suq+W{qbCocktykPM6&n*bj)w<79)TX zsXwe6m~*h*&lz_fY+nHymf7MgDG*e}Ym*wnVgI_nmfJ4yYh71p2DcCts^|TyuUOlN zkih5sC)G`F{!s~ok3r01kkmRf8ytiC>4wo#=g5eX%Z}6NN46Z8?MS*GKe6DWfpt9| zI#Q^?=5j~K@%E*h?#Bc$b2XW5fr+4FU?>AVri zp&N|d?JxGU{*yuV{Vg*P2YyLPAa#&bQu;>vM3TrC2f51NXe2}YWXmqMtFD zd)QLA5Y|GczG62PC!aT6kU39t(@1{92go{n){7_SOjsyl%;!hZn*GCsru?e03ArX= z9D1<^s+w~6hC!D-)wEYv><%WX-tx)3%6B(Q5BEgbbDNv+qYkXzG?5m5zKEzOtO{cT z4J+`h=t`iaC!kA`AxmoF`6@n@msw{f<-H!+o&VnTf zp%am=P*;Ko7}fn2d(qxo>_7XCxTzuUEq2)LsuK@&dphwC4ws#ebp$-Xz+t{9XxS^^ z3+H9kJ(E7~mNx;EH?@Ze+@xx2wFk1%in1~#2;O{^ReVwBUg3(OP7}ReG4BcCFudN_=aR=t;|7k50mc))>}SZ&;vr z?DQ%M2T)RF3Nw+~w-zM3%pB#?x|Yb0A8rptre|j}fo~ov^fPjAXQI%uG-b*6R$iT} zlTVqt!;oG#KdcDbrvXa(X}SH{DQy7Bz``Q`?Yz$)jgFjHhoL`fRh zyUW>1^x{D|0tFqQw{0}_ITBc&tIuV;=|g=+Lq9+>3w0YVg0)1tg2{pUy-UtpLF$0D z03%?vIZx7#0zUbUzCEp$*K7MPEpQj117(5A)ZQIwd07MT5^+<_Fy>|od;XXb_=jl~ zB1zPCzCM7BtF6u@v745Zl*FF}-}L5yhvNP9qQ2ChAJGI)N>5$}&c)sWUM`BlkFcwB zY%0`I@zg6^mtz9e-Z4E7xXws;Xu@wv=fg+$XZ}#1Ghm;M=P8R9gTqsvH`hk$+H_bO zw%FfvI(tWNTjh7;)EU6?eb}Jddgco*t&ILqGG7HQXZf$dgc+_kA|fI>XP4r!_2=M% z-u@gfiCSPSag@zc&XU?G=BM%9}t4Y6_8X_<>hcGGiz<>VynP1v5 zeC>GUG#9m6RoD2uOZR}LPPt63jjj0#H_Tm}2ztC=!YLe*O(s!;Q=TUP&IK(d&Xpl* zz^c0U0ov?jTN4|95A^Kv}(RS3gri^Lp-Gca8PQzJ1DZxz{60kA6Q5qvIp z7++D5#-s>39?nrxPyvPR?KUX-D%4Oba~kW%rYjlj1=pE_YRRG?vm)HCMTQ<#%{eEQR1sJt7(@2&)PTp#qSQDfuA%pNO_km9YBlhNgpzLI zCMH-EJ<>vPu_ZncvFQZ>O|2)wrwCeF8Tmvmi!Wag(0tEdC#wVjsZbt~Fj)ARnEoWy zvI`n@qXPwwb!hpmVG2+zoJwh#&&|z|<>cffH1uPL#4-ar^|4Wh$e6>*)>EA`V19pm z2LVM_@C|>&w$NGSiXBkZY1SH2OWj7s5oFeSIRC?;Q%n5idKa;Xu<9hyiq|>8MV<34 z7sFG`63vst;3_KtsOT)|fQpX$y9(AZUz|$&QfbS5LF8C?F%ZxzHxU>;vzjal1qzTn zPG8gq_$gM7!UI`;{={@*=uES&vprH|z@AiKv7DpAV$8VK}IT zkVShr=%i(cNdOQK=H(fAP+%B0XdS^=@v%`fH5;Lya~BA~aY<;PQ?mp#4&$sGf(q3! zd@BoR&cNzh@LF#!=EnTFvLoaZ1C@v`a}C$-aSmwz%E+bzBXPZJX~NTw-l2y-1wGyX z=X8O>Mh?a5XpW7x4h%c!yKCX}e)Q@>^oaD9Wr3C+%gO8yV9@CS1>8hxaycd}FbrR8 zy(CuoK~X_gc4=sFmNH0$)DXxKp?ekiHiLN7iVgCl;B_2x86vC(Kpp_+oas(8d5%Y< zD@mj@ z8DiMm{7}ji0iT50_nNi#8T?d13E$||R}O)v+;YsIbg)T({4-kPSdG)qoiBJ7kR#r` z<6x!uxO(eHXh@&KS})GIj^w}<2@%k3^nlvUVSkoe>uGy?`}j*<%Ri(ai+d0j84Ly} z{*u+b*=qIZ!PN4bU8Wf-m)wP(O*uwX`h<1SBiy~Y*pm}%i0D8Gu}|DE@P`!Eh{Uc+u*KM;#xwg@ZVvrn!PvJ_PTHmk2Wu*w-9k}) z@_KZ$b~ElZfI4?sg*|HFxN$f6fEG7So0JCI^iLedH7X&v8Lu?*Bp6-*XCW|R^r4Px zfu89QC=~nqq&b3G3N=b;N)7btB+vFw@cXkC!=~^b*IG;G^*#cZ)%f1dP`9?sE&zdP zr#H{@_$rCzY1ml6`w%JI&enYdG1VD3R>ge$@Eeo!>6Bpq zjzI6n4qc%EI8K8tqDi?Q#2$_LFZH50QeZvA_a#WsU_H@6{HoBMLA)2t^8p@eNCrwJ ze-e0FDa`gY40Y6qUUd1U$jg{I5)OneFE1;W2tKlCvz;GzM0QzG1nB7KVvAn_*T4bx ze&cv`vNrYbdMk7)>!ON8D`MpkOgMT|MH=R1nAuh?r>w~bC$Rk$hC0rSFftC;F_)O^ z*6t699O$1|!(_Z?PRu$|1l-otX^v7+DN^!}ek4)75NMT=mQFcw4I}b@Bs%(tI4m-U zxIECHVK#@(-yl5x-#4hM4zXQ;QzBt66S`k&yrnyk0Kw{~|6`!7RDE??cghCk9R*;1R{Z6yCR zq5VEzi$>+aLoGPJhiYb4CmPBqJH+gMwUse1qd(EJyzmvnoR&xA$kl)UmqD32Ti?&j zTSN+8O8H;!9vLwCahmT1+-&9l1;*u}c-E=@c@b7BXnzpVSZW-Kj=)JXevaC19Qs4O zTF1lwa#6P~o7xnPFYzQ|FZ$^6>e%R9ou{G1GwJ9{naI>jbj!6&+d3}Xe|Lxm+5hlp z%NPC+gW0lILgEL3_>{p~1+5+bgFW^&izdM1sq~tW^lm)$b-z0FT}E{{@7hcovoy0k zYwM3Q@TH;>^h4VS?di+D^MJmhD&2H0!i$3V`pO@lLX2kIMZ7~eXO}92PMnqm1BREk zi8Z=0DQ9VUQkmLZ;cw*FP92zyT7~KqnflzN*DnvZBh%?U7@oPg9md1bPIzh@f`-?T z>l7XSF(=Q-P3~F!Mwvsj_K(UT`f^f9wfZ0CaY;|j;~akK^gUUf01#_$d>5Tt;KbB1SdwM~MN8ofr zjT^V{0PVH7l<}WvDjjUrYwXfWq!aGkY#(`H&$IVU6F0Bn;H4qgG?=j7aI4j=qRi#Z zg>`zNRiGVr8*|&|%2T)(^1k%Nh_=~EZc5|EHQ9P#($grO7b{&RuovmiNY4u*byhWe zR*~XUsq<=mew^GzvzvdUmPp23ig_9cEG%W~1TnX9#IGrLmBrVUjaDYejIUN`Spl-N zQ_F5|t~i`=OgqcNsnu@fO;HkAkwgV@qlD`^OyQyKhOrT%79> zXLWm7c%Ko{h1faIL0MrFrZ82vKI8u%4k`ATbEE1~$+tkTeH4e4M>g}^VEwRxlaR2^ zr>x{BCwv^X`TyXM`NIp)LZ)|@e|zlR+}8QsceV8suI8d#!~@BxWv~?lSF3*c=+ZK6 zK5MH6+XI>#N?=|M{kyI@qIWiw%pR%FLMm zFiXCTUi}LjU{^_bMg;hk`xViBiN2kf6v4SCRgdNF!t048o+@NV6dFf-JF3EqWk!eD zQTFsZ;^g8*Ti=D^$dHJs z&0T(y-DImw4Sg%(&5?Wfz!u>MCIL+qTx7lsJ(_|^zev@^n_dL#vlD?pEze%iHd?Gw zy%(NTF>`>j0(XW-0P+j@2eQ-oU4wDq?29Ajw^g1YiQn(vCOh@))erSH>oa@K2&$#x z*{wyLP2(U5+E3Qf72qqSmTUPh+i+3r`;H;1h`8^u1SHC|DxmkMQ*`)QAU(R#T`FQ< zKN!Ii?mhgQn}Z zJ4l!BE;AC-&6>6e6HRo!{uJt2{`f1^f0ef|OxT{w5;6GcP~WZwl0YuHUuC}${=pk0 zpNE1d@001$uoEwjtjVSin!>v<-t)}Ymgm*tEtu=yL@A86!u*poR&6l^oG0D78kM85 zb-XA{=O%;QUDIyiSXQPb-EEva=^7QnmY0;llX0{YKA(o4R~yZVoZ5E^JuwiOkggQz zY5T-S=RY5%M75{IjwQfD+1Vu6_s{JPNvJ{NyU0JZ;o{ZnYc)+_OW{bwCxJU!nZHla z$Dt>=j!C>1{6#@yt99=74@OgBqc_a;z6VP!RG^lMpCJBpzt}iBP;w32qT_RGj2eFC zZQ1XXKXSSD6qFnw*A{PM{)8g7T%{+ya^#twXSdZ>tvhdJS*N8 zht}UKG4u<9;?I1toS)NX29#S(vAWO+`ev7uZdLXv>sWkA8&qFsg`_DQ0~L2hA(>0< zcz=B2X*m^VcT@O!ji;|MqN!e0XMlLkin}{e(bskdc>#-N$E5Rpd3TCBYN*q-l9k@e zxB+vN@>=Z->f4xiNLyNw5?GaSTMc7W>aot>+VYsXtt-V{)`@X$$l1b{AISvZ0awgK0$4G<-L1tM8e4Fv(G=2@t9lpvBT?|7i3JvN?>_3glmKhq zaC0FMMn092^cfGf+GqFp#>$^+hY>*%?9$!G(P4ByIG(nzgJU$M9!g4+<>5F^F$7~+ zA);0OAD(gh>z3Y=`5tTkTjg})mJ4&Cbo$I@dD`TEWRXUjTPf~0R~A32;Q5t4I|D&0 z^3s|_DhIi$2TRzO>$WsuZeU8<9)MEo#&+}4OlI5n?p(tKgP|JkLnU8|Uk%T8R##WQ z0-M`%hksI9PeB_L-HMFNy2)Nu9ak?w7E2C=BtUDSU(~6Z;Ioi+YWfE<`CWXmY9sXp zU;66pIMO{Xy_LVKlf+H$0tCEb1nxy%@DPXIbkSfu{Pj(nU4>YEV}bc3ukb2)t(J`U zaRp96Z`m7U`Lx0jXcM%mQvB%ppN(c!d+X3f@J(|CzUnrJQ?Ddr}$Oa_Fx? zX`i=*h{IWu;q*01^mZ{Wa*OkTmEUeD*i>q>ywc>=Jejxr_4Nrc$7%k##9FeeM81i1 zy3ebo2RuB?NQsr_Pnj6D2G_kZ-+YhFG%{Oj)x?+~}bBsK{;II#S&f5GqT4H5Gfp4OiK zd5VL~dvm;Fb@AzZGl9>S)FCy_na- z!Cml_+7~=oA-M5#$aFh%-zQiO@jo?^w}D;!FjIG!H5f8H7CSr+L*#dY(sHF~3XtzZ zKChc(;^==DAeqilNhTydmGMRQO^e0JzMVBtI!&@SzZLLiltS}`M(Zs~;nIKFX%-zK z|4zx{M3C@SLoD^-!#wKsv_dRY#4`9VG8GT6aM`yYKKO$YnWI7NN<-=H!_cv|&t{Yz%Lo!5HjqS? z0)Eb}Wh|MbTS^iWMk7q1Nl17!5sCeolxgQ(J3e+(m0TX3cw$VYgNh)j&&{D8=@2Nr zRz}x-2PnEFC#Re-JQ6PMMoGn3l$J>e1UL{ZYR!FO(tyf_D)}awCpA*xD~ikaODt6; zBIcJ`-qThe5sD$)oza1!SI#p%0mmEXNm!+YFCcNAO6@u3BfeDG>xvnBW8+I5lUz8T zUOwX8x~2j8fJa^l;>j(!`#v|H)-`SM3kyfD3q^s*ryym5LZh9cePmziOP^82yb19L zMYLAYT>n3&*Fni1upgJghaP#!LL8RO$|{BiRRjtcnWfN`{?mq~o~FCc6F1tHHKTb@ zCM8)pqT{Yjb&q>f^0tJa%d*O7>YKB~fGmv`mM1W#^K+tNx0DNRlBTHz$co~e86S;c z`D`E;rQTnzaFIp=z!j$6K6$lXgC!ACjc|C37E)*ZZ2o!X3+mDJ)3p&Bs_w>>MV1$- zy^-*1E|_8vGtCTCs*E^`XgT=F{u|_SXV-1zt3Ro-Yb`#wse2c|8d!{ohijeKg_KgNSiwG5IUsD17+X0dSxtv>J|MA+ww)2(%c>Spn22 z$Km)p&~>NU7#NXB6Bb&ODx?kDqgA%L;%7ci`1)pwZgp6c?06#b(y%cpi}J>nI21mV zRX)4k;e7U|Rk8Otkel|6F`-#sNv*5UMwwcZz8=Ss3d)y0jwxXwx}yl<uilt(VeT+I`nuO#ruI$ND}G>Ii#P` z$qf;CG&Q657I_g#H$fj?@Z#Q(Z%-D4ZD3FuXsZhFLIMYZ?Ye9!qbsDh8dgDJL)TC+?)dCkO0 zv778Nx~eJtapWFzHKDj;WL7FP3Rc`%-!k+v)5hxhG!UJhtCmWNr4-3%z3wPsJ)?6V zZ0qgrKjPLi5y|BfXeEb^9HQzH0ag;0xHMTEk1DGK`E1Y(qG3hCjD6FmryX(9i9TF@ zO`aDr`!`B}i%U#OS^#}C7q-;9?icv}H#zy#GD z;9yS$Ks;#4v&sg4AS%RD=fSyg>}9AkQTW~nV6lzKa7K4f7zP&3fvPDrv8;k3YDXG# z^)Jy>RlQ#pnxK-czAS#h2?SA|F)B;^w(Z#2aQ*YJM_`g~$;htK^R73()^(ouf;)$? z*(w+z9?Wj_08Jqqd(k z=S_)d)cJBUzgHUt@4NIpuEXF%wu23_!oOjyd&!H&OWE?e92AxdEJ`%`21dyAewDjT z#4jE075;f7`KuG1` z_Uq8}G;v&9+|Kwl1}T@4_MPYXP9R|5oZnVqQVQlBK=*y9M+rTN@R|qCsw9SeSwKl= zJiW+4%ug!-Fq5Gteb=ynVf2WKpHazpP3$?vjwNRoh>f|I9ki!5Th?^X+qJ29-`b!C zLs&S>$7seGf4wM$AeFCfMj}(fi1B8h=qqW1b++%X)pJSBQG%PYN zO>fD$&xrdJ=f9-d$Wr$+(fkdM1;4pHDUYhnjuQK@TJRtclP#3GaVM%=xP z5?tO!$8v!tT0`++6XgxtdHI;fCgd(s1mZ~_;hlv4R0aF;l^xz_JO?~m4;{9|^j8Hd zOda$4WJdODr<1i09}CsxZGBR!vlY4GLa^!MTpr<3fX=jr(+axO!D+C25?M_5SI~^o zDBW-@1FvAi#kl_3-EDs_nO_XXGXyEY@X$UPcia`p2H!NxWd%8LPM0{v)@S~h7Twjl zpLE_$a&cMRG_}*-QYP^1lAnaTz!Q_Dsc4IQxNyhbKF@kJMLO5!n`%lDTKZ$9)VK6I zvI{g&b?1{u@rY+FZce8JYv29kzA62RHYFS7q2U_$nI5(K!$X-(8zVV}()~%;{%E?i z9hVic1%^mh(_M6#w_h}PF=sele%~s3Q786uU4eT1aVHsA*>93=EX$R~PBB%M0BV(GXwo9uJyNfTk;p{<&L!~H@aOZZ0XoD6i;hP4T?tBo-WIlf> zi52+W4grV8s383IdyNsU5Pr9F|C0|O01CR(1w8PgXHHKzy6pg8Z0Bwo7jL%7_wLjF z2A|!cAfQT!+h8<0uA_oUTjv}H%uZ=>S*ER4t<3tftA^xkNmczGp?jl>;ja>?xQ7hdbuM2#n z3m|kSLL+_~qQgKBH)xoG>TG^qRXwD6VT&eub-dQ@mh76dtPyg;NuaAyGYlO)HHK%O~of zk|ex4mG-*Lev;etz&9!qoC(_q8P4vV8@+@T0x$C;U(Qihe6CLekYIq6%V(2zFq=$IAz--y_=NYM0F=5;`lk-H|ZlP z0b_u+W4DCOmeo>o(`=uat!>F#K#Cc7K`Q=RWehn zD9^eIw;=Dps)5*hH3eYWE-&k9Nm&oV@v^`Gy|;`K~ z-HuyfxyG(!1n5Wu0OG88#w#F@1q3#lhfXBGdf3OP44>UCBh@Ax5YyaxrS%l-Q^SFm zyrZp6bibN>#;haleOT^c%0@)mnXVKO#n&0wu=*_p9xV@)A4fLt2}8&&1IxQGtSG<$ z7WXzb?`eC4%Y!{^aA07dAY;KH8f*pm*!9cZ7Y0q^-_P8-eaWRyLzytmb>|38s;>-W z2IVSp%`8N8g=&;dXh>>S@A4c1V*{uAFn~=&I9Q3YF~$g zM9P)cuk!zKY@fV-R0s+9y0K?%pu3EYgR=b(qw-Fs(9;NCre8Tv7!)op_vaurT%BrN zJa&tD5FSJ82y+a4&UK3}K6qe5+x~C9fkLLsrGQs^eoWRFMfyC)#EVg~=|@B*fgwmM-DmvKHY9>d;0kfPqj^=c+Ip_PY?5{x{{C@)+{> zwbH_`exlG|G*hzTnajclj{7sfy5B*V>lG%u?<_b1zx4DPy)w`@2J*%7&AM*9hVH#p zV>Susad!0IHQbk72+6$~zq7zImU2T&Cyia~2OOARovzSy%n*WqL=pq)OCOr5%YZ)c z##}bVjPS|T2tOmaH)LsUU)t0gwsy&ik@{WsF<(V8g*gW(%PLUEOH_$|o)C#0e&CF) zv~6xsg5z=e%rdkodS6)HolilhE6uPJ(u65YDu`ApR}_0e3NcXeHx@cWj*m|G7 z&3VhdcrSe1k>;JO3Z}qd%uf99n6y_o_Z=z=j`H{WWQA}-LV!|o-Z?9ewd)MQv7g-! zBT={&=0kzp=xMMecOe&c^MXt|e%0c*gxWm)wl8S|XAD5mqIGW?DnN3Oe%d$mc!<&c zirpi=;v4`7rbV5(wkHeO0^Yh~~hfe$fvmES&> zwr2cTG*%|gsxVS@*1rBB1+vfjCYGx2;t1f^p`4bfHrrwDU38W~ueDRw4hc2e@fXNQ zU#S)|XpZYay&>L27vjMdm`AFqNa1lZt*rADtXXp0OScFdF3%^!$Xh$_626dVFMw&a z6hGU9 zV(z@9Cr^W)gW2mILK;SQhc=c-!%IdST9H}2BcR(XfEsEc?Z=xB-jZ@4Dis1))Q znd{DFx1Re=GBQ?~|8A^+eKI$Jn_RHrXkP}q@ABuTghI>sMdYXG?7e7LhbKK0Zjh@a z^sbml(-GA)y7kK~?*yy^M##(*NA$^P_uW z)0Enp*V-$s7$!^digFwtp{jkSB-)QVsux6lettXgS8jOFS({_pi|}%#RV(<_F;#P? zmCP+a9elh_`p!%JK<{Vt#BAkX*dk7G3)FWLA*u!?S+(nX`e@3{eI}4^ z)Sv57K(Uh9_ruHi6s*6sb-=Ddc5|trwkIT}FWMrQBFw|A@MlqY=mA zODfPoCKs$AlYko+(#J9A@gCXi{j>eh~SzBB$^o3tmALY1vCj3bZZ+VJWz zQAdU(_REs62zWK6gx|7d=1Z#9NlGwit>OLbHBfN2!wqi%48o61eYFN1h*gr16Pn87 zRU`jZq~>Aa>PVl|jK}ykwU;`Rd{5;Eu}xNYal0KbVN@h9&3>&nyqWm@r3&|rPZJy` zpNI(^!JiI@`$V5$%wskYHB(yBys zJD684wO35$#o^9!;rq}mhFu|D7aH8%JIwi6^n=G`Wji)akSe1Av!_>qs4|YMPZuga zGAw65{hEZ@&%WKtEhwTS2orFr9tLFLe9oEe z-JT4XzM~W5%;<>>z2rH!E{oCY`W!}{IYnyrZo~m~_K&HHa#~$)WFH-XYm&r`jnDlA zZ`gyG1lO;cdw~H^xLR;0$tcgC3Vu09lN(JEA!S}4Q1EC?bmA#l(0W=KE^aX2O*uOn zErIU_+A$z&8fh&qi1?wXFo~pD8nX|C91A$@VKXOc_Of8Gv9Zm!!2TOs#wz-7!+$0w z7U+s79)_%p1=1sKCJ$%G@4f}Ow<`P`9^qHa?>)qh-^kRy`SO8k-82+$p3x3;6sS?ppD2LB}8jm|@5#f-Y9UNHZ<}C=71&gWV^gv+X!sLxG!; zaKrZ(8i;~|f_649HodvwD3dnD4&B=kj|yzR9y_kUO# zm)I|vKXP)e`@dX+U(@A>5Zjq2GkW$LDIwN{`ow%5WkAuYjnTjrE?LObPM{h(hw z>5~qeiX}-tT;#^BVC?lXK6<_`d~VzB8%-6j2#}^Dy+RqHt(D{kpsMYN)zKjAP@QUHVQ+cu05LaEXyh)B z#Cn|jm%Kh_AL-3vH9O~w)6Z#=$ zWFvOz&lMJk$YhA&!+5DTQX^4s6n=!Piv6yAq*jB)KaSvQ97|%yW}ZN*peG2GSvt7# z6SA~&1n?K2ZZ9fkGBv#a0aW$?a~Kd5usWcC`0l16I!Q58^N-<9EE_n1W1PW*a%e9u zxTWuK{Mi3iJ+OPA5Z(tR^Q*3`3lZ?o{N|Qjv;OzFlB_sg+p2(HAjhiIaO7tr8K#Z1 zzMxVkA5@Ne7^U#*_Z+=^E_~a2GX2wTOj8dGeQO(}mXi&3HiHc77M0cSdNG6zrIi5& zz3d4jTkmqq`1TghdY%ecxkwFRue*&gEI~2inxl6MTMm1cI1tp-l}u~``;o(MYkOsjRJd8KYgz#&S6ljhpbA26bL?MrD?}~ z7*38g$#d2H*IYl*ahh91#a=D<gv*DYY|LTkA95pX zICwn^p(o|oYy+kc@M7+dWBD#Dj_7_z#*L5b6aH|~keRK<a|y;gY9I}8D6DyOCoI&v zB0OiYEF>bPQ7qy0xp4-}B`iR{h>cC?Dp*(Cnyexldhnv8CVHi)Cx142jL~y^wxa61 zKdbf_EgkZgkV7joHI))rUFeudC--MpA}%@R8&4brUn=WREHqId0!i=R_RXy=WG1F^ z+=KWoet0OAdZsgqd&vbz?#^q}@G7i)Likbd8&Ife6N^ohJGx<$dazUBd>0$=d8Ez( zA)3M@jPVnGa7tE?SY|D6{TNP)fNP94DMT^#V}p0K#pyW;krBmxfN#_73W#EIzdr(B z+5&{GiKi>R1o1mnEYLnWi}`6&c(z6WhjyQQ%v-2aIesFo=Fn$HS*0=`jlfJUCuQ{cP? zrcod;|2OJ?Ywr{`%lICjEI9dX+iFb?y=qQGk$DKWJ0MKHtN}@DD{O$8MGW86C)aX( zv7$W5ocVhXq$C{)Yz2$LFn>Hos%8F`|2oou$+lR#Izr^Dw) z3c+f~7^Z3hk8Wc(rLcD%W>eT#-arupAm_c}=!mk{rl%{Vdxpuv9}x7F)X@X!aTM}F z?(-$^Hz=BR?Jw1^!4+>?0Tfh=`ljB8Od~7v!@44!XL(Ica8(sIXcfI?f)Z{X#nZ|X z!ns%3Y6soKrzs*G6?cc~QdPC@q|nJglot4j0Z|HPI}>Q&5m0RiwhEn3-k9Zf-DZRww%V!}I^2rW|PL~O6_ zE|-*Be0?5s=V)j|$$wwJ19SX;VHW{ixa8luUxy?u<&hBSI$;0O%_r@xtXVk;C`- zp&?e8U1&!T#%TIzsl8SmxY&ktlY#RM78FvLRN%e`F%*w>8GdRr9dD#SI(aFl5!aq#n3&g(-s_R-X)KZ=brBXhu}5 zGv#r1NxcP5)Q<%p8Pu(AB`*e76cgT9pu!j6D^-)Spj$O9$nvp0qzx(JA?ag^nPFe$TGeHIWn;BU~V56K1gVceHC znpL|(p3dzgB{D7sm)B}*s;&uBT0KPjF)AwR?|<%F4!sx;Ex_h5V2#ScG4e??0%wQi zT(<@O(b5S!C)}#KE$M+De+vOaYXHkP9mrLs6Lnb-?ylUK1YF&z6eNz1p6xcmV+ml0 z1)JIB^T&UdllZNpk+?SGzL7|JLE_JdG&Ce5E#=HR7TViV^=VU9kfW z^=eevTOA>{va&+u^ICRXb$4*U8MVSvw#Wix-~)#+83@+2=VxocgB)z}tmclscRxI6 zmo^JB4u9o}&>Rj?GSt=Z!P@d2+RrVidxwUaBUcAXJqHYqnB3oMD|IqplcU|l8m0$w z+rxwDmOWvZFp^RHAjNtt=Kc1qP-*0xT&;u6U|m;&m_!ZsN^}zgZj2homi+HCN(D{P z`1?6VsWl25J%t8eQRrY%P0OJE{CLdo*(zsJs~@RhIB+KC_Q}3|6XrYLyq`W~wM7TaPi7P&@_W26>6?DIGEIW^mj zKt5LDXgP*Oq{eXn6##BJIVs@QcIKMn^o*e4vIu${0rkcC&l&z`XwkO}3Z2UW9Ohcs zDRer(Z4WFUN$rCX<7 zwI<}g>CyIVlBAE&#SNyHb5_l;4p?|wemLi_!nnkt>OecC#uz&~;|sq}U=B{;G7Z#F z*F5!)fnr{*NxbG3+Sxd!X?ehXD-mo2L**`TK|IVn)T((OmbkC$e07{O&=K@MUZe$6 zBjLS!_fWq!DcMemxSo`K?>mdm+A6ARGh!=;({}Oy;e{;Ge+4^8f$r8{^sRqas;FjK z(e(KE6UoXEZ|tck=Lao$|E4**ss;ex3SO>S35l&2mOeX=$0IxzdGhVsw<8!2bOtbA z!W~U{oqD6dpn4dS$<&!Nxa3D0F4g0=ZN9_|{w<~0)pKPm-%T|T)9SBL{wUwf#>U~0ZH~k+ztz{7LK6;C^+nBC5HX1ga zMF*`+*ViNcGw?45pdl_(Z>=Fiy>E+t`osswbDo}f7&H9>%RI-&jMbi-{{cTd&j%MJxe5&uRAA zP)7TSvW!6b*S|>HL>;yL2r`WF6yI6;hJ*xz zhldA>@;>F)MYVN6;@eE*@J6Q4WetRaHKJ zQ_E*rqJTiXsHtDV$~eaDy@D#B!P1gnU^dYX(QeC5A@ngikO5=ne;D6494$v-@0cLw z%27Tnbxqf!%(B`Pdy#Ta@|iApf{cuSnVnBXc-9>0Z^Uc1V4Spb=%zpPv(`uWgP?uz zznY)TVC)V>A3(`HkkI{tDiNvXl&)on)A~MB3rhL8J*LdUf8aB9Z<%mZBpPHYD5iK1wN2qY8%ey(mrYAQ_d2G}qI-j`*j z>rU6o06|&8%jJswfl=}CGW!syw41?pO+<70MwUDvFllsUWj34Vjzb^;&E=WuNnLQ~ z{n|nXl-3=3lh6xXf|1|2|0usoFs*^c+2o$jG^kmfqvm=Du@pMM@i(vMmG>ITQ0YLJ zj4?Cf)JqkdXa0RnMlj*#3?r2LsWmva^9LUPq|CR6REakI8zuwi4>40sF5`=Kg)s5A z#>Ji*s!Q>FW&I^U&Y&R^`&50tn_0NCt%(Hk!4)7d|LQ@MBy!^QDKG2dOtAPwAy z9|pn_aKmk#X#s!(})&Ca#r@o3zKVJMV4ZOt@=LGET-dJ3IFeNE{7B zy-0?8QX`>F%zXCr7Xo;KS8lS}jaDEKcb=fZ!;of0l zvrWhVZSKBY*9;bdQq;WCiuFhkO&d#q=2L(R|EVx#z70 z$qZYDhWfj)kJzFvQH06%i_C}Gzy3Zd5_0ofnYLg;k=U@da)iTrT~%~d1e1-!oQ^bu zb3&A!w&FtuQFbhm%q139N)O2xW2@4>D1(I~e=9z!`ollB_#0K~&JO*cwm{3>0uQTR zFmueWc53FKjnglZ($CcTZ{5VINa(0NjoRB?&6_hK(DBe|Av+NynkBl#Kp9#xWSYcd zdSRHL&}TO7HSUyGnq%?eA|pviU&86WjERXU{iQx~&j_Z?URG*yps%`KbpKHH@)LD{ zr@wwQ^UsJMXQxd6bSs(qYzw)q^fu~wJsLx{5fzbaPggi+M|-@WCbm4H5)u+RxffFN zrS#9%c&Wp%Py(kmae7^P3z_$0dAs1JTN6ip@&X5Tk&vrfm2}3fb(_3!@6RR0 z$KwSA1eh=0O#aOENy}ch7~NAgjc)J8>tQ;bJeIp`a3uOpjXs1)5}U~=IXN*AnHZwN z5kKtEk%^s|F{wrW&c7P&m_fknFeab^psB^wj*pum4zuGit)pHjCu{RF^q)&<%*49F zx+>`+*MMsv2&nYX&^(b?>1|x;3gl3GCo}Qp)ewgUNk0r`dA@nLB4xsJ;=h+%Ds!e5 zW}Q`PwPYPCMpHC!86Naj$kW!@rjY$Z7jbd%&9&9VMN4E2y&T7B*x;a6R}wECpxa(4 zJJ5cmA0t+scCbUflNe1N6{*;`LB=JC|7AMa^t5%J*KtC`{4eW1{O+~08z0dF1De-E z7HB!)1u!g@RiUwY60f$^pBV7@EyR+=Tz6^ae|z!IjG(A#xD7Kc?bS}!3W|B2&pg1# z$G3S@UgozyzY(r=)aeRf2pm8!AQO>}u)+Gf9vvLY*>?7~M9*y+{1SZQcRjcy(GrVA)@&XG<^4W)9~UP^||W?2-jsTy1*p>O7h%V|^5U7;njLGAYBBSp-dXROfA(W>QKA+fDFEak zs2!lVz$cDrx6d>KxdxrEzXWD!DQmfecjadUyUY(3)<48T_tGQYHVA^ahaW5CGi+|8 zy9?Xk<1?w8Iwv3(1{IC_$C3qjf%YYL%L94jO+aZzrT)cIRlv94COy8ugKOh_Ek#+1 zVrlZv!X7N-K1xEQ%#8Gt<4rRkDH$~V4c2Dabnl0dTnmH>&}vvb$*X|JR#qHRfA;u& zcba`)2tEtMybjJ*OgU)blWywXPpAL@tLSeZ%l!A3;dHHk3$iJGc zdUqtk<&Jy?AxLvCk_TJ|cCh4An$5XkzA{*JQNO=`Kk`CS5 zF;ca^Z8V(x&th@QHWY^bvqwn5K~dnx*Am0*gp@!Og&|TUtI z7gw>t%Fnv&p69XTxCc+xv<%`fXEYSQ2J|@Q;0Y=D)^kRa>PD8v?<92)f{{z(klNlF zB`mD2w?w`p(dEu%aceTzFgj#(G*oUanQh`dX(FG+U>rAIOu&u0$F>cHfH4Q-3!WP4 zEa>U@oGY4Ej&DLjSWopt=FI2zD+^z9rP_%!HV&L8!;qB}n-#z6{T9ddtYf?(Kb2Z3 zIkCT1Ej}eJ0(o7(ttKuk#a7svbwAi+IiVnRb!WBW1%?In1?67s$?9|@v_Ir&zL1uj zuYLK4EgSAtp4!jY|M*kzRoFpQpqc}+NIo6x_{WDKC6J^+8q?ypcJ$a#Z!3_#>cJFW z7Z1+}Z6bXBVqE@|u7)5wk<+worqMuLf3

xYGBSFIOcwOVH1y(*(xLbMRZtm07gv`?{$SIM7r<5}^`l(F zOjQ>A_@y_lx1Mljmp-KaMP7BV$mlwB3=inv_?7V%w~SZhI@;PAKxUHosG=~FWE__5Z#<`Rlu%~zFW-w;cm21cym%{j;r-^3tgUQ%-BUuv%sFU-vckP zY7Pr5F0QWoZfa5tjEsVh4!5SK^|-|7uvkLHLkOy5yuHPWjO(MHlsn$Md-pPAVuyNf z6>x+w_D5R4J1ejnMmX4^!A?kLx7@Om z21!ctJ(sYUqWe@l%QWS{W^jyok#YAv>Ry|-eD zTv7{+!Efw-@{Pq}nM{(XU~%$NS;|RPQ4F`8uq|;XpH2%#mz!(x0^-c=>gO27#`f&9 z2L^V;h6OruhmMoItT)a%)$9b9@&~xguyz~La2dS$`c;6m0}>!OlXoPe9MxY1D1`eYy;wsKy1aWS5{8}|$=UY3;bK&*0g zR^coR=;#xESFXrWldEYJt-T7@o>1@mMBE{+6I`3ky0O8ie$aRrN00uTMmJUON$!bq zoIfZ`sYrBY6z(scJ4qDCF;?k|uNBbbdrUR&yxGXFu`2z*ejo&?ND6EGFXhw!b$@$7$h>MSV&MTcQ0`cGv#8Dfrg2*vB!Ur8;inqE{~ceVfzH2cJ*0t_lwB439A0v z7;hi}9-8%p$JxtkYx%mu86k$Gs)~wzTWLv2p%oP(wRK+L*)8O~>LREWm^b*($?@^( zY7X?>6(zd;|D~OPoL2UE9!^fB*GU3VvGKx0c3y06M8(9=jg99I4h~An%7V(u$~+bm zD5$(hjoB93N8;^sAJg96f7Sn1-i~0m%|3_U8u&1RHp7rOo|>BSE{y-x`d5-GE6W`z z;Hs#k6xePdV`rC7rV2%D1$p^kGR+?y9sQDgB2Tlz3 None: phitter.continuous.CONTINUOUS_DISTRIBUTIONS | phitter.discrete.DISCRETE_DISTRIBUTIONS ) + self.servers = dict() + + self.simulation_result = dict() + + def __str__(self) -> str: + """Print dataset + + Returns: + str: Dataframe in str mode + """ + sim = pd.DataFrame(self.simulation_result) + return sim.to_string() + + def _repr_html_(self) -> pd.DataFrame: + """Print DataFrames in jupyter notebooks + + Returns: + pd.DataFrame: Simulation result + """ + sim = pd.DataFrame(self.simulation_result) + return sim._repr_html_() + + def __getitem__(self, key): + sim = pd.DataFrame(self.simulation_result) + return sim[key] def add_process( self, @@ -29,6 +55,7 @@ def add_process( parameters: dict, process_id: str, number_of_products: int = 1, + number_of_servers: int = 1, new_branch: bool = False, previous_ids: list[str] = None, ) -> None: @@ -38,7 +65,8 @@ def add_process( prob_distribution (str): Probability distribution to be used. You can use one of the following: 'alpha', 'arcsine', 'argus', 'beta', 'beta_prime', 'beta_prime_4p', 'bradford', 'burr', 'burr_4p', 'cauchy', 'chi_square', 'chi_square_3p', 'dagum', 'dagum_4p', 'erlang', 'erlang_3p', 'error_function', 'exponential', 'exponential_2p', 'f', 'fatigue_life', 'folded_normal', 'frechet', 'f_4p', 'gamma', 'gamma_3p', 'generalized_extreme_value', 'generalized_gamma', 'generalized_gamma_4p', 'generalized_logistic', 'generalized_normal', 'generalized_pareto', 'gibrat', 'gumbel_left', 'gumbel_right', 'half_normal', 'hyperbolic_secant', 'inverse_gamma', 'inverse_gamma_3p', 'inverse_gaussian', 'inverse_gaussian_3p', 'johnson_sb', 'johnson_su', 'kumaraswamy', 'laplace', 'levy', 'loggamma', 'logistic', 'loglogistic', 'loglogistic_3p', 'lognormal', 'maxwell', 'moyal', 'nakagami', 'non_central_chi_square', 'non_central_f', 'non_central_t_student', 'normal', 'pareto_first_kind', 'pareto_second_kind', 'pert', 'power_function', 'rayleigh', 'reciprocal', 'rice', 'semicircular', 'trapezoidal', 'triangular', 't_student', 't_student_3p', 'uniform', 'weibull', 'weibull_3p', 'bernoulli', 'binomial', 'geometric', 'hypergeometric', 'logarithmic', 'negative_binomial', 'poisson'. parameters (dict): Parameters of the probability distribution. process_id (str): Unique name of the process to be simulated - number_of_products (int ,optional): Number of elements that are need to simulate in that stage. Value has to be greater than 0. Defaults equals to 1. + number_of_products (int ,optional): Number of elements that are needed to simulate in that stage. Value has to be greater than 0. Defaults equals to 1. + number_of_servers (int, optional): Number of servers that process has and are needed to simulate in that stage. Value has to be greater than 0. Defaults equals to 1. new_branch (bool ,optional): Required if you want to start a new process that does not have previous processes. You cannot use this parameter at the same time with "previous_id". Defaults to False. previous_id (list[str], optional): Required if you have previous processes that are before this process. You cannot use this parameter at the same time with "new_branch". Defaults to None. """ @@ -52,43 +80,56 @@ def add_process( if process_id not in self.order.keys(): # Verify that at least is one element needed for the simulation in that stage if number_of_products >= 1: - # Verify that if you create a new branch, it's impossible to have a previous id (or preceding process). One of those is incorrect - if new_branch == True and previous_ids != None: - raise ValueError( - f"""You cannot select 'new_branch' is equals to True if 'previous_id' is not empty. OR you cannot add 'previous_ids' if 'new_branch' is equals to True.""" - ) - else: - # If it is a new branch then initialize all the needed paramters - if new_branch == True: - branch_id = len(self.branches) - self.branches[branch_id] = process_id - self.order[process_id] = branch_id - self.number_of_products[process_id] = number_of_products - self.process_prob_distr[process_id] = ( - self.probability_distribution[prob_distribution]( - parameters - ) - ) - self.next_process[process_id] = 0 - # If it is NOT a new branch then initialize all the needed paramters - elif previous_ids != None and all( - id in self.order.keys() for id in previous_ids - ): - self.order[process_id] = previous_ids - self.number_of_products[process_id] = number_of_products - self.process_prob_distr[process_id] = ( - self.probability_distribution[prob_distribution]( - parameters - ) - ) - self.next_process[process_id] = 0 - for prev_id in previous_ids: - self.next_process[prev_id] += 1 - # if something is incorrect then raise an error - else: + # Verify if the number of servers is greater or equals than 1 + if number_of_servers >= 1: + # Verify that if you create a new branch, it's impossible to have a previous id (or preceding process). One of those is incorrect + if new_branch == True and previous_ids != None: raise ValueError( - f"""Please create a new_brach == True if you need a new process or specify the previous process/processes (previous_ids) that are before this one. Processes that have been added: '{"', '".join(self.order.keys())}'.""" + f"""You cannot select 'new_branch' is equals to True if 'previous_id' is not empty. OR you cannot add 'previous_ids' if 'new_branch' is equals to True.""" ) + else: + # If it is a new branch then initialize all the needed paramters + if new_branch == True: + branch_id = len(self.branches) + self.branches[branch_id] = process_id + self.order[process_id] = branch_id + self.number_of_products[process_id] = number_of_products + self.servers[process_id] = number_of_servers + self.process_prob_distr[process_id] = ( + self.probability_distribution[prob_distribution]( + parameters + ) + ) + self.next_process[process_id] = 0 + # Create id of that process in the simulation result + self.simulation_result[process_id] = [] + + # If it is NOT a new branch then initialize all the needed paramters + elif previous_ids != None and all( + id in self.order.keys() for id in previous_ids + ): + self.order[process_id] = previous_ids + self.number_of_products[process_id] = number_of_products + self.servers[process_id] = number_of_servers + self.process_prob_distr[process_id] = ( + self.probability_distribution[prob_distribution]( + parameters + ) + ) + self.next_process[process_id] = 0 + # Create id of that process in the simulation result + self.simulation_result[process_id] = [] + for prev_id in previous_ids: + self.next_process[prev_id] += 1 + # if something is incorrect then raise an error + else: + raise ValueError( + f"""Please create a new_brach == True if you need a new process or specify the previous process/processes (previous_ids) that are before this one. Processes that have been added: '{"', '".join(self.order.keys())}'.""" + ) + else: + raise ValueError( + f"""You must add number_of_servers grater or equals than 1.""" + ) else: raise ValueError( f"""You must add number_of_products grater or equals than 1.""" @@ -107,6 +148,9 @@ def run(self, number_of_simulations: int = 1) -> list[float]: Returns: list[float]: Results of every simulation requested """ + + self.simulation_result = {key: list() for key in self.simulation_result.keys()} + # Create simulation list simulation_result = list() # Start all possible simulations @@ -114,39 +158,132 @@ def run(self, number_of_simulations: int = 1) -> list[float]: # Create dictionaries for identifing processes simulation_partial_result = dict() simulation_accumulative_result = dict() + # For every single "new branch" process for key in self.branches.keys(): partial_result = 0 - # Simulate the time it took to create each product needed - for _ in range(self.number_of_products[self.branches[key]]): - partial_result += self.process_prob_distr[self.branches[key]].ppf( - random.random() + # If there is only one server in that process + if self.servers[self.branches[key]] == 1: + # Simulate the time it took to create each product needed + for _ in range(self.number_of_products[self.branches[key]]): + partial_result += self.process_prob_distr[ + self.branches[key] + ].ppf(random.random()) + # Add all simulation time according to the time it took to create all products in that stage + simulation_partial_result[self.branches[key]] = partial_result + # Add this partial result to see the average time of this specific process + self.simulation_result[self.branches[key]].append( + simulation_partial_result[self.branches[key]] ) - # Add all simulation time according to the time it took to create all products in that stage - simulation_partial_result[self.branches[key]] = partial_result - # Because we are simulating the "new branch" or first processes, accumulative it's the same as partial result - simulation_accumulative_result[self.branches[key]] = ( - simulation_partial_result[self.branches[key]] - ) + # Because we are simulating the "new branch" or first processes, accumulative it's the same as partial result + simulation_accumulative_result[self.branches[key]] = ( + simulation_partial_result[self.branches[key]] + ) + # If there are more than one servers in that process + else: + # Simulate the time it took to create each product needed + products_times = [ + self.process_prob_distr[self.branches[key]].ppf(random.random()) + for _ in range(self.number_of_products[self.branches[key]]) + ] + + # Initialize dictionary + servers_dictionary = { + server: 0 for server in range(self.servers[self.branches[key]]) + } + + # Organize times according to the number of machines you have + for product in products_times: + # Identify server with the shortest time of all + min_server_time = min( + servers_dictionary, key=servers_dictionary.get + ) + # Add product time to that server + servers_dictionary[min_server_time] += product + + # Identify the "partial result" as the maximum time of all servers to create all products + partial_result = max(servers_dictionary.values()) + + # Add all simulation time according to the time it took to create all products in that stage + simulation_partial_result[self.branches[key]] = partial_result + # Add this partial result to see the average time of this specific process + self.simulation_result[self.branches[key]].append( + simulation_partial_result[self.branches[key]] + ) + # Because we are simulating the "new branch" or first processes, accumulative it's the same as partial result + simulation_accumulative_result[self.branches[key]] = ( + simulation_partial_result[self.branches[key]] + ) + # For every process for key in self.process_prob_distr.keys(): # Only consider the ones that are not "New Branches" if isinstance(self.order[key], list): partial_result = 0 - # Simulate all products time - for _ in range(self.number_of_products[key]): - partial_result += self.process_prob_distr[key].ppf( - random.random() + # If there is only one server in that process + if self.servers[key] == 1: + # Simulate all products time + for _ in range(self.number_of_products[key]): + partial_result += self.process_prob_distr[key].ppf( + random.random() + ) + # Save partial result + simulation_partial_result[key] = partial_result + # Add this partial result to see the average time of this specific process + self.simulation_result[key].append( + simulation_partial_result[key] ) - # Save partial result - simulation_partial_result[key] = partial_result - # Accumulate this partial result plus the previous processes of this process - simulation_accumulative_result[key] = ( - simulation_partial_result[key] - + simulation_accumulative_result[ - max(self.order[key], key=simulation_accumulative_result.get) + # Accumulate this partial result plus the previous processes of this process + simulation_accumulative_result[key] = ( + simulation_partial_result[key] + + simulation_accumulative_result[ + max( + self.order[key], + key=simulation_accumulative_result.get, + ) + ] + ) + # If there are more than one servers in that process + else: + # Simulate the time it took to create each product needed + products_times = [ + self.process_prob_distr[key].ppf(random.random()) + for _ in range(self.number_of_products[key]) ] - ) + + # Initialize dictionary + servers_dictionary = { + server: 0 for server in range(self.servers[key]) + } + + # Organize times according to the number of machines you have + for product in products_times: + # Identify server with the shortest time of all + min_server_time = min( + servers_dictionary, key=servers_dictionary.get + ) + # Add product time to that server + servers_dictionary[min_server_time] += product + + # Identify the "partial result" as the maximum time of all servers to create all products + partial_result = max(servers_dictionary.values()) + + # Save partial result + simulation_partial_result[key] = partial_result + # Add this partial result to see the average time of this specific process + self.simulation_result[key].append( + simulation_partial_result[key] + ) + # Accumulate this partial result plus the previous processes of this process + simulation_accumulative_result[key] = ( + simulation_partial_result[key] + + simulation_accumulative_result[ + max( + self.order[key], + key=simulation_accumulative_result.get, + ) + ] + ) # Save the max time of the simulation simulation_result.append( @@ -157,7 +294,36 @@ def run(self, number_of_simulations: int = 1) -> list[float]: ) ] ) - return simulation_result + self.simulation_result["Total Simulation Time"] = simulation_result + return pd.DataFrame(self.simulation_result) + + def simulation_metrics(self) -> pd.DataFrame: + """Here you can find the average time per process and standard deviation + + Returns: + pd.DataFrame: Average and Standard deviation + """ + # Use simulation results + df = pd.DataFrame(self.simulation_result) + + if len(df) == 0: + raise ValueError("You need to run the simulation first") + + # Calculate all metrics + metrics_dict_1 = {f"Avg. {column}": df[column].mean() for column in df.columns} + metrics_dict_2 = { + f"Std. Dev. {column}": df[column].std() for column in df.columns + } + metrics_dict = metrics_dict_1 | metrics_dict_2 + + # Create result dataframe + metrics = pd.DataFrame.from_dict(metrics_dict, orient="index").rename( + columns={0: "Value"} + ) + + metrics.index.name = "Metrics" + + return metrics.reset_index() def run_confidence_interval( self, @@ -175,22 +341,42 @@ def run_confidence_interval( Returns: tuple[float]: Returns the lower bound, average, upper bound and standard deviation of the confidence interval """ - # Simulate with replications - average_results_simulations = [ - np.mean(self.run(number_of_simulations)) for _ in range(replications) - ] + # Initializa variables + tot_metrics = pd.DataFrame() + for _ in range(replications): + # Run simulation + self.run(number_of_simulations) + # Save metrics and probabilities + metrics_summary = self.simulation_metrics() + # Concat previous results with current results + tot_metrics = pd.concat([tot_metrics, metrics_summary]) + # Confidence Interval - ## Sample standard deviation - standar_deviation = np.std(average_results_simulations, ddof=1) - standard_error = standar_deviation / math.sqrt(replications) - average = np.mean(average_results_simulations) + std__2 = tot_metrics.groupby(["Metrics"]).std() + mean__2 = tot_metrics.groupby(["Metrics"]).mean() + + standard_error = std__2 / math.sqrt(replications) normal_standard = phitter.continuous.NORMAL({"mu": 0, "sigma": 1}) z = normal_standard.ppf((1 + confidence_level) / 2) ## Confidence Interval - upper_bound = average + (z * standard_error) - lower_bound = average - (z * standard_error) + avg__2 = mean__2.copy() + lower_bound = ( + (mean__2 - (z * standard_error)) + .copy() + .rename(columns={"Value": "LB - Value"}) + ) + upper_bound = ( + (mean__2 + (z * standard_error)) + .copy() + .rename(columns={"Value": "UB - Value"}) + ) + avg__2 = avg__2.rename(columns={"Value": "AVG - Value"}) + tot_metrics_interval = pd.concat([lower_bound, avg__2, upper_bound], axis=1) + tot_metrics_interval = tot_metrics_interval[ + ["LB - Value", "AVG - Value", "UB - Value"] + ] # Return confidence interval - return lower_bound, average, upper_bound, standar_deviation + return tot_metrics_interval.reset_index() def process_graph( self, graph_direction: str = "LR", save_graph_pdf: bool = False @@ -206,30 +392,37 @@ def process_graph( # Add all nodes for node in set(self.order.keys()): - print(node) # Identify if this is a "New branch" if isinstance(self.order[node], int): graph.node( - node, node, shape="circle", style="filled", fillcolor="lightgreen" + f"{node} - {self.servers[node]} server(s)", + f"{node} - {self.servers[node]} server(s)", + shape="circle", + style="filled", + fillcolor="lightgreen", ) elif self.next_process[node] == 0: graph.node( - node, - node, + f"{node} - {self.servers[node]} server(s)", + f"{node} - {self.servers[node]} server(s)", shape="doublecircle", style="filled", fillcolor="lightblue", ) else: - graph.node(node, node, shape="box") + graph.node( + f"{node} - {self.servers[node]} server(s)", + f"{node} - {self.servers[node]} server(s)", + shape="box", + ) for node in set(self.order.keys()): # Identify if this is a "Previous id" if isinstance(self.order[node], list): for previous_node in self.order[node]: graph.edge( - previous_node, - node, + f"{previous_node} - {self.servers[previous_node]} server(s)", + f"{node} - {self.servers[node]} server(s)", label=str(self.number_of_products[previous_node]), fontsize="10", ) diff --git a/tests/phitter_local/simulation/test_process_simulations.ipynb b/tests/phitter_local/simulation/test_process_simulations.ipynb index 944066b..54f367f 100644 --- a/tests/phitter_local/simulation/test_process_simulations.ipynb +++ b/tests/phitter_local/simulation/test_process_simulations.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -21,7 +21,7 @@ "simulation.add_process(\"normal\", {\"mu\": 5, \"sigma\": 2}, \"first\", new_branch=True, number_of_products=10)\n", "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"second\", previous_ids=[\"first\"])\n", "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"ni_idea\", previous_ids=[\"first\"])\n", - "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"ni_idea_2\", previous_ids=[\"first\"])\n", + "simulation.add_process(\"exponential\", {\"lambda\": 4}, \"ni_idea_2\", previous_ids=[\"first\"], number_of_products=15, number_of_servers=4)\n", "\n", "simulation.add_process(\"gamma\", {\"alpha\": 15, \"beta\": 3}, \"third\", new_branch=True)\n", "# simulation.add_process(\"exponential\", {\"lambda\": 4.3}, \"nn\", previous_ids=[\"third\"])\n", @@ -36,25 +36,215 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "

\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
firstsecondni_ideani_idea_2thirdfourthnnfifthsixthseventhTotal Simulation Time
038.9402360.0054570.4098370.88175237.2212954.7291230.2449340.1771850.0073552.00467145.686842
140.1086260.2545080.2447871.01752736.8420884.7338040.1099510.0517000.0808002.37254347.550281
254.1916400.1214820.2179740.87830435.3486665.2532790.4249210.0271060.3771542.13667062.080225
352.3033900.3174690.2470501.08035748.1629155.1263640.1974270.0066640.0884002.62293360.458556
446.1886700.0021170.0606071.01550139.6030284.6179610.0865230.0931260.0453342.56568853.419770
551.8371200.1706010.0551251.08739733.3303725.5829020.1803460.7192440.0813372.20260059.874560
661.7039430.2125830.1773220.93372870.5128434.8880820.0252790.0271050.1135472.81202178.326494
744.9253360.0768591.0214731.59138640.6286484.9911970.1714350.2175370.2366572.99710553.227153
858.7467270.2694410.7876580.64344336.0199884.7651280.1054620.0819440.0723792.68208766.535762
945.0649700.2074420.4556591.30404958.5441074.6075610.1266000.0930140.0170912.59293465.761693
\n", + "
" + ], "text/plain": [ - "[60.910389060638735,\n", - " 71.7036413860762,\n", - " 63.180822548765725,\n", - " 61.67380804299192,\n", - " 57.95645127570466,\n", - " 58.428061132122146,\n", - " 63.30357172070581,\n", - " 70.73770263716327,\n", - " 60.48492019029255,\n", - " 59.26121489392577]" + " first second ni_idea ni_idea_2 third fourth nn \n", + "0 38.940236 0.005457 0.409837 0.881752 37.221295 4.729123 0.244934 \\\n", + "1 40.108626 0.254508 0.244787 1.017527 36.842088 4.733804 0.109951 \n", + "2 54.191640 0.121482 0.217974 0.878304 35.348666 5.253279 0.424921 \n", + "3 52.303390 0.317469 0.247050 1.080357 48.162915 5.126364 0.197427 \n", + "4 46.188670 0.002117 0.060607 1.015501 39.603028 4.617961 0.086523 \n", + "5 51.837120 0.170601 0.055125 1.087397 33.330372 5.582902 0.180346 \n", + "6 61.703943 0.212583 0.177322 0.933728 70.512843 4.888082 0.025279 \n", + "7 44.925336 0.076859 1.021473 1.591386 40.628648 4.991197 0.171435 \n", + "8 58.746727 0.269441 0.787658 0.643443 36.019988 4.765128 0.105462 \n", + "9 45.064970 0.207442 0.455659 1.304049 58.544107 4.607561 0.126600 \n", + "\n", + " fifth sixth seventh Total Simulation Time \n", + "0 0.177185 0.007355 2.004671 45.686842 \n", + "1 0.051700 0.080800 2.372543 47.550281 \n", + "2 0.027106 0.377154 2.136670 62.080225 \n", + "3 0.006664 0.088400 2.622933 60.458556 \n", + "4 0.093126 0.045334 2.565688 53.419770 \n", + "5 0.719244 0.081337 2.202600 59.874560 \n", + "6 0.027105 0.113547 2.812021 78.326494 \n", + "7 0.217537 0.236657 2.997105 53.227153 \n", + "8 0.081944 0.072379 2.682087 66.535762 \n", + "9 0.093014 0.017091 2.592934 65.761693 " ] }, - "execution_count": 3, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -65,16 +255,402 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MetricsValue
0Avg. first49.401066
1Avg. second0.163796
2Avg. ni_idea0.367749
3Avg. ni_idea_21.043344
4Avg. third43.621395
5Avg. fourth4.929540
6Avg. nn0.167288
7Avg. fifth0.149462
8Avg. sixth0.112005
9Avg. seventh2.498925
10Avg. Total Simulation Time59.292134
11Std. Dev. first7.608047
12Std. Dev. second0.109492
13Std. Dev. ni_idea0.315107
14Std. Dev. ni_idea_20.257845
15Std. Dev. third12.067456
16Std. Dev. fourth0.313087
17Std. Dev. nn0.110028
18Std. Dev. fifth0.211046
19Std. Dev. sixth0.112690
20Std. Dev. seventh0.314065
21Std. Dev. Total Simulation Time9.777236
\n", + "
" + ], "text/plain": [ - "(60.88028267603926, 61.12153218365918, 61.362781691279096, 0.6741848235617004)" + " Metrics Value\n", + "0 Avg. first 49.401066\n", + "1 Avg. second 0.163796\n", + "2 Avg. ni_idea 0.367749\n", + "3 Avg. ni_idea_2 1.043344\n", + "4 Avg. third 43.621395\n", + "5 Avg. fourth 4.929540\n", + "6 Avg. nn 0.167288\n", + "7 Avg. fifth 0.149462\n", + "8 Avg. sixth 0.112005\n", + "9 Avg. seventh 2.498925\n", + "10 Avg. Total Simulation Time 59.292134\n", + "11 Std. Dev. first 7.608047\n", + "12 Std. Dev. second 0.109492\n", + "13 Std. Dev. ni_idea 0.315107\n", + "14 Std. Dev. ni_idea_2 0.257845\n", + "15 Std. Dev. third 12.067456\n", + "16 Std. Dev. fourth 0.313087\n", + "17 Std. Dev. nn 0.110028\n", + "18 Std. Dev. fifth 0.211046\n", + "19 Std. Dev. sixth 0.112690\n", + "20 Std. Dev. seventh 0.314065\n", + "21 Std. Dev. Total Simulation Time 9.777236" ] }, - "execution_count": 4, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simulation.simulation_metrics()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MetricsLB - ValueAVG - ValueUB - Value
0Avg. Total Simulation Time61.03274661.27620761.519668
1Avg. fifth0.2234560.2321760.240896
2Avg. first49.78238649.99742350.212460
3Avg. fourth4.9846364.9997965.014956
4Avg. ni_idea0.2383210.2470860.255852
5Avg. ni_idea_21.1974741.2125751.227676
6Avg. nn0.2232420.2323170.241392
7Avg. second0.2383350.2482360.258137
8Avg. seventh2.4908502.5013882.511926
9Avg. sixth0.2256190.2337630.241906
10Avg. third44.98246145.40142345.820385
11Std. Dev. Total Simulation Time7.2151037.5066107.798116
12Std. Dev. fifth0.2177320.2290860.240439
13Std. Dev. first6.1350096.2703886.405767
14Std. Dev. fourth0.4011180.4091440.417170
15Std. Dev. ni_idea0.2314090.2428410.254273
16Std. Dev. ni_idea_20.3612980.3743570.387416
17Std. Dev. nn0.2193720.2314090.243446
18Std. Dev. second0.2266420.2406320.254623
19Std. Dev. seventh0.2864660.2907160.294966
20Std. Dev. sixth0.2139270.2230500.232172
21Std. Dev. third11.20455511.55510311.905651
\n", + "
" + ], + "text/plain": [ + " Metrics LB - Value AVG - Value UB - Value\n", + "0 Avg. Total Simulation Time 61.032746 61.276207 61.519668\n", + "1 Avg. fifth 0.223456 0.232176 0.240896\n", + "2 Avg. first 49.782386 49.997423 50.212460\n", + "3 Avg. fourth 4.984636 4.999796 5.014956\n", + "4 Avg. ni_idea 0.238321 0.247086 0.255852\n", + "5 Avg. ni_idea_2 1.197474 1.212575 1.227676\n", + "6 Avg. nn 0.223242 0.232317 0.241392\n", + "7 Avg. second 0.238335 0.248236 0.258137\n", + "8 Avg. seventh 2.490850 2.501388 2.511926\n", + "9 Avg. sixth 0.225619 0.233763 0.241906\n", + "10 Avg. third 44.982461 45.401423 45.820385\n", + "11 Std. Dev. Total Simulation Time 7.215103 7.506610 7.798116\n", + "12 Std. Dev. fifth 0.217732 0.229086 0.240439\n", + "13 Std. Dev. first 6.135009 6.270388 6.405767\n", + "14 Std. Dev. fourth 0.401118 0.409144 0.417170\n", + "15 Std. Dev. ni_idea 0.231409 0.242841 0.254273\n", + "16 Std. Dev. ni_idea_2 0.361298 0.374357 0.387416\n", + "17 Std. Dev. nn 0.219372 0.231409 0.243446\n", + "18 Std. Dev. second 0.226642 0.240632 0.254623\n", + "19 Std. Dev. seventh 0.286466 0.290716 0.294966\n", + "20 Std. Dev. sixth 0.213927 0.223050 0.232172\n", + "21 Std. Dev. third 11.204555 11.555103 11.905651" + ] + }, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } From 667bd1f25eca24b6c6d1458ca7039070043115bc Mon Sep 17 00:00:00 2001 From: sebastianherreramonterrosa Date: Mon, 28 Oct 2024 22:22:13 -0500 Subject: [PATCH 11/11] Write readme module fit and simulation --- README.md | 258 +++++++++++++++++- SIMULATION.md | 197 ++++++------- .../process_simulation/process_simulation.py | 128 ++------- 3 files changed, 365 insertions(+), 218 deletions(-) diff --git a/README.md b/README.md index de2e60e..d5e8d82 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,12 @@

Phitter analyzes datasets and determines the best analytical probability distributions that represent them. Phitter studies over 80 probability distributions, both continuous and discrete, 3 goodness-of-fit tests, and interactive visualizations. For each selected probability distribution, a standard modeling guide is provided along with spreadsheets that detail the methodology for using the chosen distribution in data science, operations research, and artificial intelligence. +

+

In addition, Phitter offers the capability to perform process simulations, allowing users to graph and observe minimum times for specific observations. It also supports queue simulations with flexibility to configure various parameters, such as the number of servers, maximum population size, system capacity, and different queue disciplines, including First-In-First-Out (FIFO), Last-In-First-Out (LIFO), and priority-based service (PBS). -

+

This repository contains the implementation of the python library and the kernel of Phitter Web

@@ -45,9 +47,11 @@ python: >=3.9 pip install phitter ``` + + ## Usage -### Notebook's Tutorials +### ***1. Fit Notebook's Tutorials*** | Tutorial | Notebooks | | :------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | @@ -57,7 +61,28 @@ pip install phitter | **Fit Specific Disribution** | Open In Colab | | **Working Distribution** | Open In Colab | -### General +### ***2. Simulation Notebook's Tutorials*** +pending + +## Documentation + + + + + + + + + + + +
+ +Documentation Fit Module + + + +### General Fit ```python import phitter @@ -466,6 +491,233 @@ distribution.mode # -> 733.3333333333333 | poisson | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | | uniform | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | +
+ + + + + + + + + +
+Documentation Simulation Module + + + +## Process Simulation + +This will help you to understand your processes. To use it, run the following line + +```python +from phitter import simulation + +# Create a simulation process instance +simulation = simulation.ProcessSimulation() + +``` + +### Add processes to your simulation instance + +There are two ways to add processes to your simulation instance: + +- Adding a **process _without_ preceding process (new branch)** +- Adding a **process _with_ preceding process (with previous ids)** + +#### Process _without_ preceding process (new branch) + +```python +# Add a new process without preceding process +simulation.add_process( + prob_distribution="normal", + parameters={"mu": 5, "sigma": 2}, + process_id="first_process", + number_of_products=10, + number_of_servers=3, + new_branch=True, +) + +``` + +#### Process _with_ preceding process (with previous ids) + +```python +# Add a new process with preceding process +simulation.add_process( + prob_distribution="exponential", + parameters={"lambda": 4}, + process_id="second_process", + previous_ids=["first_process"], +) + +``` + +#### All together and adding some new process + +The order in which you add each process **_matters_**. You can add as many processes as you need. + +```python +# Add a new process without preceding process +simulation.add_process( + prob_distribution="normal", + parameters={"mu": 5, "sigma": 2}, + process_id="first_process", + number_of_products=10, + number_of_servers=3, + new_branch=True, +) + +# Add a new process with preceding process +simulation.add_process( + prob_distribution="exponential", + parameters={"lambda": 4}, + process_id="second_process", + previous_ids=["first_process"], +) + +# Add a new process with preceding process +simulation.add_process( + prob_distribution="gamma", + parameters={"alpha": 15, "beta": 3}, + process_id="third_process", + previous_ids=["first_process"], +) + +# Add a new process without preceding process +simulation.add_process( + prob_distribution="exponential", + parameters={"lambda": 4.3}, + process_id="fourth_process", + new_branch=True, +) + + +# Add a new process with preceding process +simulation.add_process( + prob_distribution="beta", + parameters={"alpha": 1, "beta": 1, "A": 2, "B": 3}, + process_id="fifth_process", + previous_ids=["second_process", "fourth_process"], +) + +# Add a new process with preceding process +simulation.add_process( + prob_distribution="normal", + parameters={"mu": 15, "sigma": 2}, + process_id="sixth_process", + previous_ids=["third_process", "fifth_process"], +) +``` + +### Visualize your processes + +You can visualize your processes to see if what you're trying to simulate is your actual process. + +```python +# Graph your process +simulation.process_graph() +``` + +![Simulation](./multimedia/simulation_process_graph.png) + +### Start Simulation + +You can simulate and have different simulation time values or you can create a confidence interval for your process + +#### Run Simulation + +Simulate several scenarios of your complete process + +```python +# Run Simulation +simulation.run(number_of_simulations=100) + +# After run +simulation: pandas.Dataframe +``` + +### Review Simulation Metrics by Stage + +If you want to review average time and standard deviation by stage run this line of code + +```python +# Review simulation metrics +simulation.simulation_metrics() -> pandas.Dataframe +``` + +#### Run confidence interval + +If you want to have a confidence interval for the simulation metrics, run the following line of code + +```python +# Confidence interval for Simulation metrics +simulation.run_confidence_interval( + confidence_level=0.99, + number_of_simulations=100, + replications=10, +) -> pandas.Dataframe +``` + +## Queue Simulation + +If you need to simulate queues run the following code: + +```python +from phitter import simulation + +# Create a simulation process instance +simulation = simulation.QueueingSimulation( + a="exponential", + a_paramters={"lambda": 5}, + s="exponential", + s_parameters={"lambda": 20}, + c=3, +) +``` + +In this case we are going to simulate **a** (arrivals) with _exponential distribution_ and **s** (service) as _exponential distribution_ with **c** equals to 3 different servers. + +By default Maximum Capacity **k** is _infinity_, total population **n** is _infinity_ and the queue discipline **d** is _FIFO_. As we are not selecting **d** equals to "PBS" we don't have any information to add for **pbs_distribution** nor **pbs_parameters** + +### Run the simulation + +If you want to have the simulation results + +```python +# Run simulation +simulation = simulation.run(simulation_time = 2000) +simulation: pandas.Dataframe +``` + +If you want to see some metrics and probabilities from this simulation you should use:: + +```python +# Calculate metrics +simulation.metrics_summary() -> pandas.Dataframe + +# Calculate probabilities +number_probability_summary() -> pandas.Dataframe +``` + +### Run Confidence Interval for metrics and probabilities + +If you want to have a confidence interval for your metrics and probabilities you should run the following line + +```python +# Calculate confidence interval for metrics and probabilities +probabilities, metrics = simulation.confidence_interval_metrics( + simulation_time=2000, + confidence_level=0.99, + replications=10, +) + +probabilities -> pandas.Dataframe +metrics -> pandas.Dataframe +``` + +
+ ## Contribution If you would like to contribute to the Phitter project, please create a pull request with your proposed changes or enhancements. All contributions are welcome! diff --git a/SIMULATION.md b/SIMULATION.md index 8504199..b403c91 100644 --- a/SIMULATION.md +++ b/SIMULATION.md @@ -1,50 +1,3 @@ -

- - - - phitter-dark-logo - -

- -

- - Downloads - - - License - - - Supported Python versions - - - Tests - -

- -

- Phitter analyzes datasets and determines the best analytical probability distributions that represent them. Phitter studies over 80 probability distributions, both continuous and discrete, 3 goodness-of-fit tests, and interactive visualizations. For each selected probability distribution, a standard modeling guide is provided along with spreadsheets that detail the methodology for using the chosen distribution in data science, operations research, and artificial intelligence. - - In addition, Phitter offers the capability to perform process simulations, allowing users to graph and observe minimum times for specific observations. It also supports queue simulations with flexibility to configure various parameters, such as the number of servers, maximum population size, system capacity, and different queue disciplines, including First-In-First-Out (FIFO), Last-In-First-Out (LIFO), and priority-based service (PBS). - -

-

- This repository contains the implementation of the python library and the kernel of Phitter Web -

- -## Installation - -### Requirements - -```console -python: >=3.9 -``` - -### PyPI - -```console -pip install phitter -``` - # Simulation ## Process Simulation @@ -63,19 +16,21 @@ simulation = simulation.ProcessSimulation() There are two ways to add processes to your simulation instance: -- Adding a **process _without_ preceding process (new branch)** -- Adding a **process _with_ preceding process (with previous ids)** +- Adding a **process _without_ preceding process (new branch)** +- Adding a **process _with_ preceding process (with previous ids)** #### Process _without_ preceding process (new branch) ```python # Add a new process without preceding process -simulation.add_process(prob_distribution = "normal", # Probability Distribution - parameters = {"mu": 5, "sigma": 2}, # Parameters - process_id = "first_process", # Process name - number_of_products = 10, # Number of products to be simulated in this stage - number_of_servers = 3, # Number of servers in that process - new_branch=True) # New branch +simulation.add_process( + prob_distribution="normal", + parameters={"mu": 5, "sigma": 2}, + process_id="first_process", + number_of_products=10, + number_of_servers=3, + new_branch=True, +) ``` @@ -83,10 +38,12 @@ simulation.add_process(prob_distribution = "normal", # Probability Distribution ```python # Add a new process with preceding process -simulation.add_process(prob_distribution = "exponential", # Probability Distribution - parameters = {"lambda": 4}, # Parameters - process_id = "second_process", # Process name - previous_ids = ["first_process"]) # Previous Process +simulation.add_process( + prob_distribution="exponential", + parameters={"lambda": 4}, + process_id="second_process", + previous_ids=["first_process"], +) ``` @@ -96,44 +53,55 @@ The order in which you add each process **_matters_**. You can add as many proce ```python # Add a new process without preceding process -simulation.add_process(prob_distribution = "normal", # Probability Distribution - parameters = {"mu": 5, "sigma": 2}, # Parameters - process_id = "first_process", # Process name - number_of_products = 10, # Number of products to be simulated in this stage - number_of_servers = 3, # Number of servers in that process - new_branch=True) # New branch +simulation.add_process( + prob_distribution="normal", + parameters={"mu": 5, "sigma": 2}, + process_id="first_process", + number_of_products=10, + number_of_servers=3, + new_branch=True, +) # Add a new process with preceding process -simulation.add_process(prob_distribution = "exponential", # Probability Distribution - parameters = {"lambda": 4}, # Parameters - process_id = "second_process", # Process name - previous_ids = ["first_process"]) # Previous Process +simulation.add_process( + prob_distribution="exponential", + parameters={"lambda": 4}, + process_id="second_process", + previous_ids=["first_process"], +) # Add a new process with preceding process -simulation.add_process(prob_distribution = "gamma", # Probability Distribution - parameters = {"alpha": 15, "beta": 3}, # Parameters - process_id = "third_process", # Process name - previous_ids = ["first_process"]) # Previous Process +simulation.add_process( + prob_distribution="gamma", + parameters={"alpha": 15, "beta": 3}, + process_id="third_process", + previous_ids=["first_process"], +) # Add a new process without preceding process -simulation.add_process(prob_distribution = "exponential", # Probability Distribution - parameters = {"lambda": 4.3}, # Parameters - process_id = "fourth_process", # Process name - new_branch=True) # New branch +simulation.add_process( + prob_distribution="exponential", + parameters={"lambda": 4.3}, + process_id="fourth_process", + new_branch=True, +) # Add a new process with preceding process -simulation.add_process(prob_distribution = "beta", # Probability Distribution - parameters = {"alpha": 1, "beta": 1, "A": 2, "B": 3}, # Parameters - process_id = "fifth_process", # Process name - previous_ids = ["second_process", "fourth_process"]) # Previous Process - You can add several previous processes +simulation.add_process( + prob_distribution="beta", + parameters={"alpha": 1, "beta": 1, "A": 2, "B": 3}, + process_id="fifth_process", + previous_ids=["second_process", "fourth_process"], +) # Add a new process with preceding process -simulation.add_process(prob_distribution = "normal", # Probability Distribution - parameters = {"mu": 15, "sigma": 2}, # Parameters - process_id = "sixth_process", # Process name - previous_ids = ["third_process", "fifth_process"]) # Previous Process - You can add several previous processes - +simulation.add_process( + prob_distribution="normal", + parameters={"mu": 15, "sigma": 2}, + process_id="sixth_process", + previous_ids=["third_process", "fifth_process"], +) ``` ### Visualize your processes @@ -157,10 +125,10 @@ Simulate several scenarios of your complete process ```python # Run Simulation -simulation.run(number_of_simulations = 100) -simulation +simulation.run(number_of_simulations=100) -# -> df +# After run +simulation: pandas.Dataframe ``` ### Review Simulation Metrics by Stage @@ -169,9 +137,7 @@ If you want to review average time and standard deviation by stage run this line ```python # Review simulation metrics -simulation.simulation_metrics() - -# -> df +simulation.simulation_metrics() -> pandas.Dataframe ``` #### Run confidence interval @@ -180,10 +146,11 @@ If you want to have a confidence interval for the simulation metrics, run the fo ```python # Confidence interval for Simulation metrics -simulation.run_confidence_interval(confidence_level = 0.99, - number_of_simulations = 100, - replications = 10) -# -> df +simulation.run_confidence_interval( + confidence_level=0.99, + number_of_simulations=100, + replications=10, +) -> pandas.Dataframe ``` ## Queue Simulation @@ -194,12 +161,13 @@ If you need to simulate queues run the following code: from phitter import simulation # Create a simulation process instance -simulation = simulation.QueueingSimulation(a = "exponential", - a_paramters = {"lambda": 5}, - s = "exponential", - s_parameters = {"lambda": 20}, - c = 3) - +simulation = simulation.QueueingSimulation( + a="exponential", + a_paramters={"lambda": 5}, + s="exponential", + s_parameters={"lambda": 20}, + c=3, +) ``` In this case we are going to simulate **a** (arrivals) with _exponential distribution_ and **s** (service) as _exponential distribution_ with **c** equals to 3 different servers. @@ -213,20 +181,17 @@ If you want to have the simulation results ```python # Run simulation simulation = simulation.run(simulation_time = 2000) -simulation -# -> df result +simulation: pandas.Dataframe ``` If you want to see some metrics and probabilities from this simulation you should use:: ```python # Calculate metrics -simulation.metrics_summary() -# -> df result +simulation.metrics_summary() -> pandas.Dataframe # Calculate probabilities -number_probability_summary() -# -> df result +number_probability_summary() -> pandas.Dataframe ``` ### Run Confidence Interval for metrics and probabilities @@ -235,12 +200,12 @@ If you want to have a confidence interval for your metrics and probabilities you ```python # Calculate confidence interval for metrics and probabilities -probabilities, metrics = simulation.confidence_interval_metrics(simulation_time = 2000, - confidence_level = 0.99, - replications = 10) -probabilities -# -> df result - -metrics -# -> df result +probabilities, metrics = simulation.confidence_interval_metrics( + simulation_time=2000, + confidence_level=0.99, + replications=10, +) + +probabilities -> pandas.Dataframe +metrics -> pandas.Dataframe ``` diff --git a/phitter/simulation/process_simulation/process_simulation.py b/phitter/simulation/process_simulation/process_simulation.py index 6f0de41..9b3639c 100644 --- a/phitter/simulation/process_simulation/process_simulation.py +++ b/phitter/simulation/process_simulation/process_simulation.py @@ -1,7 +1,6 @@ import math import random -import numpy as np import pandas as pd from graphviz import Digraph from IPython.display import display @@ -19,10 +18,7 @@ def __init__(self) -> None: self.number_of_products = dict() self.process_positions = dict() self.next_process = dict() - self.probability_distribution = ( - phitter.continuous.CONTINUOUS_DISTRIBUTIONS - | phitter.discrete.DISCRETE_DISTRIBUTIONS - ) + self.probability_distribution = phitter.continuous.CONTINUOUS_DISTRIBUTIONS | phitter.discrete.DISCRETE_DISTRIBUTIONS self.servers = dict() self.simulation_result = dict() @@ -72,9 +68,7 @@ def add_process( """ # Verify if the probability is created in phitter if prob_distribution not in self.probability_distribution.keys(): - raise ValueError( - f"""Please select one of the following probability distributions: '{"', '".join(self.probability_distribution.keys())}'.""" - ) + raise ValueError(f"""Please select one of the following probability distributions: '{"', '".join(self.probability_distribution.keys())}'.""") else: # Verify unique id name for each process if process_id not in self.order.keys(): @@ -84,9 +78,7 @@ def add_process( if number_of_servers >= 1: # Verify that if you create a new branch, it's impossible to have a previous id (or preceding process). One of those is incorrect if new_branch == True and previous_ids != None: - raise ValueError( - f"""You cannot select 'new_branch' is equals to True if 'previous_id' is not empty. OR you cannot add 'previous_ids' if 'new_branch' is equals to True.""" - ) + raise ValueError(f"""You cannot select 'new_branch' is equals to True if 'previous_id' is not empty. OR you cannot add 'previous_ids' if 'new_branch' is equals to True.""") else: # If it is a new branch then initialize all the needed paramters if new_branch == True: @@ -95,27 +87,17 @@ def add_process( self.order[process_id] = branch_id self.number_of_products[process_id] = number_of_products self.servers[process_id] = number_of_servers - self.process_prob_distr[process_id] = ( - self.probability_distribution[prob_distribution]( - parameters - ) - ) + self.process_prob_distr[process_id] = self.probability_distribution[prob_distribution](parameters) self.next_process[process_id] = 0 # Create id of that process in the simulation result self.simulation_result[process_id] = [] # If it is NOT a new branch then initialize all the needed paramters - elif previous_ids != None and all( - id in self.order.keys() for id in previous_ids - ): + elif previous_ids != None and all(id in self.order.keys() for id in previous_ids): self.order[process_id] = previous_ids self.number_of_products[process_id] = number_of_products self.servers[process_id] = number_of_servers - self.process_prob_distr[process_id] = ( - self.probability_distribution[prob_distribution]( - parameters - ) - ) + self.process_prob_distr[process_id] = self.probability_distribution[prob_distribution](parameters) self.next_process[process_id] = 0 # Create id of that process in the simulation result self.simulation_result[process_id] = [] @@ -127,17 +109,11 @@ def add_process( f"""Please create a new_brach == True if you need a new process or specify the previous process/processes (previous_ids) that are before this one. Processes that have been added: '{"', '".join(self.order.keys())}'.""" ) else: - raise ValueError( - f"""You must add number_of_servers grater or equals than 1.""" - ) + raise ValueError(f"""You must add number_of_servers grater or equals than 1.""") else: - raise ValueError( - f"""You must add number_of_products grater or equals than 1.""" - ) + raise ValueError(f"""You must add number_of_products grater or equals than 1.""") else: - raise ValueError( - f"""You need to create diferent process_id for each process, '{process_id}' already exists.""" - ) + raise ValueError(f"""You need to create diferent process_id for each process, '{process_id}' already exists.""") def run(self, number_of_simulations: int = 1) -> list[float]: """Simulation of the described process @@ -166,38 +142,25 @@ def run(self, number_of_simulations: int = 1) -> list[float]: if self.servers[self.branches[key]] == 1: # Simulate the time it took to create each product needed for _ in range(self.number_of_products[self.branches[key]]): - partial_result += self.process_prob_distr[ - self.branches[key] - ].ppf(random.random()) + partial_result += self.process_prob_distr[self.branches[key]].ppf(random.random()) # Add all simulation time according to the time it took to create all products in that stage simulation_partial_result[self.branches[key]] = partial_result # Add this partial result to see the average time of this specific process - self.simulation_result[self.branches[key]].append( - simulation_partial_result[self.branches[key]] - ) + self.simulation_result[self.branches[key]].append(simulation_partial_result[self.branches[key]]) # Because we are simulating the "new branch" or first processes, accumulative it's the same as partial result - simulation_accumulative_result[self.branches[key]] = ( - simulation_partial_result[self.branches[key]] - ) + simulation_accumulative_result[self.branches[key]] = simulation_partial_result[self.branches[key]] # If there are more than one servers in that process else: # Simulate the time it took to create each product needed - products_times = [ - self.process_prob_distr[self.branches[key]].ppf(random.random()) - for _ in range(self.number_of_products[self.branches[key]]) - ] + products_times = [self.process_prob_distr[self.branches[key]].ppf(random.random()) for _ in range(self.number_of_products[self.branches[key]])] # Initialize dictionary - servers_dictionary = { - server: 0 for server in range(self.servers[self.branches[key]]) - } + servers_dictionary = {server: 0 for server in range(self.servers[self.branches[key]])} # Organize times according to the number of machines you have for product in products_times: # Identify server with the shortest time of all - min_server_time = min( - servers_dictionary, key=servers_dictionary.get - ) + min_server_time = min(servers_dictionary, key=servers_dictionary.get) # Add product time to that server servers_dictionary[min_server_time] += product @@ -207,13 +170,9 @@ def run(self, number_of_simulations: int = 1) -> list[float]: # Add all simulation time according to the time it took to create all products in that stage simulation_partial_result[self.branches[key]] = partial_result # Add this partial result to see the average time of this specific process - self.simulation_result[self.branches[key]].append( - simulation_partial_result[self.branches[key]] - ) + self.simulation_result[self.branches[key]].append(simulation_partial_result[self.branches[key]]) # Because we are simulating the "new branch" or first processes, accumulative it's the same as partial result - simulation_accumulative_result[self.branches[key]] = ( - simulation_partial_result[self.branches[key]] - ) + simulation_accumulative_result[self.branches[key]] = simulation_partial_result[self.branches[key]] # For every process for key in self.process_prob_distr.keys(): @@ -224,15 +183,11 @@ def run(self, number_of_simulations: int = 1) -> list[float]: if self.servers[key] == 1: # Simulate all products time for _ in range(self.number_of_products[key]): - partial_result += self.process_prob_distr[key].ppf( - random.random() - ) + partial_result += self.process_prob_distr[key].ppf(random.random()) # Save partial result simulation_partial_result[key] = partial_result # Add this partial result to see the average time of this specific process - self.simulation_result[key].append( - simulation_partial_result[key] - ) + self.simulation_result[key].append(simulation_partial_result[key]) # Accumulate this partial result plus the previous processes of this process simulation_accumulative_result[key] = ( simulation_partial_result[key] @@ -246,22 +201,15 @@ def run(self, number_of_simulations: int = 1) -> list[float]: # If there are more than one servers in that process else: # Simulate the time it took to create each product needed - products_times = [ - self.process_prob_distr[key].ppf(random.random()) - for _ in range(self.number_of_products[key]) - ] + products_times = [self.process_prob_distr[key].ppf(random.random()) for _ in range(self.number_of_products[key])] # Initialize dictionary - servers_dictionary = { - server: 0 for server in range(self.servers[key]) - } + servers_dictionary = {server: 0 for server in range(self.servers[key])} # Organize times according to the number of machines you have for product in products_times: # Identify server with the shortest time of all - min_server_time = min( - servers_dictionary, key=servers_dictionary.get - ) + min_server_time = min(servers_dictionary, key=servers_dictionary.get) # Add product time to that server servers_dictionary[min_server_time] += product @@ -271,9 +219,7 @@ def run(self, number_of_simulations: int = 1) -> list[float]: # Save partial result simulation_partial_result[key] = partial_result # Add this partial result to see the average time of this specific process - self.simulation_result[key].append( - simulation_partial_result[key] - ) + self.simulation_result[key].append(simulation_partial_result[key]) # Accumulate this partial result plus the previous processes of this process simulation_accumulative_result[key] = ( simulation_partial_result[key] @@ -311,15 +257,11 @@ def simulation_metrics(self) -> pd.DataFrame: # Calculate all metrics metrics_dict_1 = {f"Avg. {column}": df[column].mean() for column in df.columns} - metrics_dict_2 = { - f"Std. Dev. {column}": df[column].std() for column in df.columns - } + metrics_dict_2 = {f"Std. Dev. {column}": df[column].std() for column in df.columns} metrics_dict = metrics_dict_1 | metrics_dict_2 # Create result dataframe - metrics = pd.DataFrame.from_dict(metrics_dict, orient="index").rename( - columns={0: "Value"} - ) + metrics = pd.DataFrame.from_dict(metrics_dict, orient="index").rename(columns={0: "Value"}) metrics.index.name = "Metrics" @@ -360,27 +302,15 @@ def run_confidence_interval( z = normal_standard.ppf((1 + confidence_level) / 2) ## Confidence Interval avg__2 = mean__2.copy() - lower_bound = ( - (mean__2 - (z * standard_error)) - .copy() - .rename(columns={"Value": "LB - Value"}) - ) - upper_bound = ( - (mean__2 + (z * standard_error)) - .copy() - .rename(columns={"Value": "UB - Value"}) - ) + lower_bound = (mean__2 - (z * standard_error)).copy().rename(columns={"Value": "LB - Value"}) + upper_bound = (mean__2 + (z * standard_error)).copy().rename(columns={"Value": "UB - Value"}) avg__2 = avg__2.rename(columns={"Value": "AVG - Value"}) tot_metrics_interval = pd.concat([lower_bound, avg__2, upper_bound], axis=1) - tot_metrics_interval = tot_metrics_interval[ - ["LB - Value", "AVG - Value", "UB - Value"] - ] + tot_metrics_interval = tot_metrics_interval[["LB - Value", "AVG - Value", "UB - Value"]] # Return confidence interval return tot_metrics_interval.reset_index() - def process_graph( - self, graph_direction: str = "LR", save_graph_pdf: bool = False - ) -> None: + def process_graph(self, graph_direction: str = "LR", save_graph_pdf: bool = False) -> None: """Generates the graph of the process Args: