diff --git a/LICENSE.txt b/LICENSE.txt index 6b59a08..03a8730 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,10 +1,6 @@ MIT License -<<<<<<< HEAD -Copyright (c) 2019 Michael Hoffman -======= Copyright (c) 2020 Michael Hoffman ->>>>>>> dev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index afe4349..7e6ec11 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,6 @@ # Simantha -<<<<<<< HEAD -`maintsim` can be used to model a discrete manufacturing system where components degrade over time and receive maintenance. Users can define the configuration and parameters of the system, as well as the maintenance policy to be carried out. It is built on the `SimPy` discrete-event simulation package. - -## Installing maintsim - -`pip install maintsim` -======= -Simantha uses discrete event simulation to model the behavior of discrete manufacturing systems, in particular the production and maintenance functions of a system. ->>>>>>> dev +Simantha uses discrete event simulation to model the behavior of discrete manufacturing systems, in particular the production and maintenance functions mass production systems. ## Using this package @@ -16,6 +8,14 @@ Simantha uses discrete event simulation to model the behavior of discrete manufa Simantha requires Python ≥ 3.6 and [SciPy](https://www.scipy.org/) ≥ 1.5.2 for running tests. +### Installation + +Install from PyPi using + +``` +pip install simantha +``` + ### Setting up a manufacturing system A system consists of various assets that are linked together by a specified routing. The available assets are described below. @@ -37,9 +37,7 @@ Parameters While a machine operates it will continuously take parts from an upstream container (if the container is not empty), process the part, and place the part in a downstream container (if the container is not full). Machines can be subject to periodic degradation and failure, at which point they will require maintenance action before they can be restored. -Currently, machines may only follow a Markovian degradation process[^1]. Under this degradation mode, a degradation transition matrix is specified as a parameter of a machine. State transitions of this Markov process represent changes in the degradation level of the machine. In general, it is assumed that there is one absorbing state that represents machine failure. - -[^1]: Chan, G. K., & Asgarpoor, S. (2006). Optimum maintenance policy with Markov processes. Electric power systems research, 76(6-7), 452-456. https://doi.org/10.1016/j.epsr.2005.09.010 +Currently, machines may only follow a Markovian degradation process.[1](#chan) Under this degradation mode, a degradation transition matrix is specified as a parameter of a machine. State transitions of this Markov process represent changes in the degradation level of the machine. In general, it is assumed that there is one absorbing state that represents machine failure. Parameters - `cycle_time` - the duration of time a machine must process a part before it can be placed at the next station. @@ -128,9 +126,7 @@ Simulation finished in 0.00s Parts produced: 100 ``` -As a slightly more complex example, the code below constructs a two-machine one-buffer line where each machine is subject to degradation. The degradation transition matrix will represent Bernoulli reliability with p = 0.01.[^2] Additionally, the maintainer capacity is set to 1, indicating that only one machine may be repaired at a time. - -[^2]: Li, J., & Meerkov, S. M. (2008). Production systems engineering. Springer Science & Business Media. https://doi.org/10.1007/978-0-387-75579-3 +As a slightly more complex example, the code below constructs a two-machine one-buffer line where each machine is subject to degradation. The degradation transition matrix will represent Bernoulli reliability with p = 0.01.[2](#li) Additionally, the maintainer capacity is set to 1, indicating that only one machine may be repaired at a time. ```python >>> from simantha import Source, Machine, Buffer, Sink, System, Maintainer @@ -192,10 +188,10 @@ Parts produced: 200 The elements of these examples can be used to more complex configurations of arbitrary structure. Additionally, the `Machine` class may represent other operations in a manufacturing system, such as an inspection station or a material handling process. -## Planned features +Additional examples are located in `simantha/examples/`. + +## References -Key planned features include +1. Chan, G. K., & Asgarpoor, S. (2006). Optimum maintenance policy with Markov processes. Electric power systems research, 76(6-7), 452-456. https://doi.org/10.1016/j.epsr.2005.09.010 -- Parallelization of simulation iterations -- Improved efficiency for iterating a simulation -- Exporting system model for reuse +2. Li, J., & Meerkov, S. M. (2008). Production systems engineering. Springer Science & Business Media. https://doi.org/10.1007/978-0-387-75579-3 \ No newline at end of file diff --git a/setup.py b/setup.py index c8b10af..cad65b6 100644 --- a/setup.py +++ b/setup.py @@ -4,18 +4,20 @@ long_description = f.read() setuptools.setup( - name='maintsim', - version='0.1.3', + name='simantha', + version='0.0.1', author='Michael Hoffman', - author_email='MichaelHoffman@psu.edu', - description='Simulation of maintenance for manufacturing', + author_email='hoffman@psu.edu', + description='Simulation of Manufacturing Systems', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/m-hoff/maintsim', packages=setuptools.find_packages(), classifiers=[ 'Programming Language :: Python :: 3', + 'Intended Audience :: Manufacturing', + 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent' ] -) \ No newline at end of file +) diff --git a/simantha/Evaluator.py b/simantha/Evaluator.py deleted file mode 100644 index 810af11..0000000 --- a/simantha/Evaluator.py +++ /dev/null @@ -1,78 +0,0 @@ -import time - -from joblib import Parallel, delayed -import numpy as np -from pathos.multiprocessing import ProcessingPool as Pool - -#from .System import * - -class Evaluator: - def __init__(self, system, warm_up_time, simulation_time): - self.system = system - self.warm_up_time = warm_up_time - self.simulation_time = simulation_time - - def evaluate_policy(self, policy, replications ,objective='production', - parallelize=True, verbose=True): - # evalaute system under a given policy - start_time = time.time() - - self.system.maintenance_policy = policy - - if parallelize: - def simulate_in_parallel(seed): - np.random.seed(seed) - #return np.random.randint(min(seed,10),20) - result = self.system.iterate_simulation(1, self.warm_up_time, - self.simulation_time, verbose=False) - - return result[0] - - with Pool(min(24, replications)) as p: - result = p.map(self.simulate_in_parallel, list(range(replications))) - #result = Parallel(n_jobs=20)(delayed(self.simulate_in_parallel)(seed) - # for seed in range(replications)) - - else: - result = self.system.iterate_simulation(replications, self.warm_up_time, - self.simulation_time, verbose=False) - - stop_time = time.time() - if verbose: - print(f'Finished in {stop_time-start_time:.4f}s,', - f'{(stop_time-start_time)/replications:.4f}s/replication') - - return result - - def parallel_simulation_proxy(self, seed): - np.random.seed(seed) - - result = self.system.iterate_simulation(1, self.warm_up_time, - self.simulation_time, verbose=False) - - return result - - def simulate_in_parallel(self, seed): - #result = self.system.iterate_simulation(1, self.warm_up_time, - # self.simulation_time, verbose=False) - - result = list(self.parallel_simulation_proxy(seed)) - - return result - - - -def firstn(n): - k = 0 - while k < n: - yield k - k += 1 - -def wrapped(n): - return list(firstn(n)) - -def parallel_test(): - with Pool(10) as p: - result = p.map(wrapped, list(range(10))) - - return result \ No newline at end of file diff --git a/simantha/Machine.py b/simantha/Machine.py index e6d78ea..66a4616 100644 --- a/simantha/Machine.py +++ b/simantha/Machine.py @@ -160,7 +160,6 @@ def get_part(self): self.target_giver = None def request_space(self): - #request_space_start = time.time() self.has_finished_part = True candidate_receivers = [obj for obj in self.downstream if obj.can_receive()] if len(candidate_receivers) > 0: @@ -188,7 +187,7 @@ def put_part(self): source = f'{self.name}.put_part at {self.env.now}' self.env.schedule_event(self.env.now, self, self.request_part, source) - # check if this event fed another machine + # Check if this event fed another machine for asset in self.target_receiver.downstream: if self.target_receiver.can_give() and asset.can_receive() and not asset.has_content_request(): source = f'{self.name}.put_part at {self.env.now}' @@ -384,7 +383,7 @@ def can_give(self): ) def has_content_request(self): - # check if a machine has an existing request for a part + # Check if a machine has an existing request for a part for event in self.env.events: if ( ((event.location is self) and (event.action.__name__ == 'request_part')) @@ -400,14 +399,14 @@ def has_vacancy_request(self): return False def cancel_all_events(self): - # cancel all events scheduled on this machine + # Cancel all events scheduled on this machine for event in self.env.events: if event.location == self: event.canceled = True def get_candidate_givers(self, only_free=False, blocked=False): if blocked: - # get only candidate givers that can give a part + # Get only candidate givers that can give a part return [obj for obj in self.get_candidate_givers() if obj.blocked] else: return [obj for obj in self.upstream if obj.can_give()] @@ -416,5 +415,5 @@ def get_candidate_receivers(self, only_free=False, starved=False): if starved: return [obj for obj in self.get_candidate_receivers() if obj.starved] else: - # get only candidate receivers that can accept a part + # Get only candidate receivers that can accept a part return [obj for obj in self.downstream if obj.can_receive()] diff --git a/simantha/Repairman.py b/simantha/Repairman.py deleted file mode 100644 index 2d7f973..0000000 --- a/simantha/Repairman.py +++ /dev/null @@ -1,105 +0,0 @@ -import copy - -import mcts -import numpy as np - - -class Repairman: - def __init__(self, env, system, capacity, scheduling_policy): - self.env = env - self.system = system - - self.capacity = capacity - self.utilization = 0 - - self.scheduling_policy = scheduling_policy - - # queue data in the form of [time, queue level] - self.queue_data = np.zeros((0, 2)) - - - def get_queue(self): - queue = [] - #print(f't={self.env.now}') - for machine in self.system.machines: - #print(f'M{machine.index}', machine.__dict__, '\n\n') - if ( - (machine.in_queue) - and (machine.health > 0) - #or ((not machine.under_repair) and (machine.get_health(self.env.now) >= machine.maintenance_threshold)) - # TODO: fix this behavior - # currently machine health data is updated before it is placed - # in the queue, so if it fails at the same time as MCTS is - # formulated it will not be considered in the schedule, but will - # still fail - ): - queue.append(machine) - - return queue - - - def schedule_maintenance(self): - queue = self.get_queue() - - if self.system.debug: - if self.system.mcts_system: - print('MCTS: ', end='') - print(f'Queue at t={self.env.now}: {[(machine.index, machine.get_health(self.env.now)) for machine in queue]}') - - if (len(queue) == 0) or (self.utilization == self.capacity): - self.update_queue_data() - return - elif len(queue) == 1: - if self.system.debug: - if self.system.mcts_system: - print('MCTS: ', end='') - print(f'Queue length 1, repairman starting maintenance on M{queue[0].index} at t={self.env.now}') - #self.utilization += 1 - next_machine = queue[0] - #self.env.process(queue[0].repair()) - #return - #elif type(self.scheduling_policy) == list: - # # schedule according to list, [first, second, third, ...] - # if self.system.debug: - # if self.system.mcts_system: - # print('MCTS: ', end='') - # print(f'Repairman\'s current schedule: {self.scheduling_policy}') - # for machine in queue: - # #try: # TODO: fix this block - # if machine.index == self.scheduling_policy[0]: - # next_machine = machine - # del(self.scheduling_policy[0]) - # break - # #except: - # # print('ERROR HERE') - # # print(f't={self.env.now}', self.scheduling_policy, [m.index for m in queue]) - # # print([machine.allow_new_failures for machine in self.system.machines]) - # #self.env.process(next_machine.repair()) - else: # len(queue) > 1 - next_machine = self.resolve_simultaneous_repairs() - - self.utilization += 1 - self.env.process(next_machine.repair()) - - - def resolve_simultaneous_repairs(self): - queue = self.get_queue() - - # FIFO policy - next_machine = min(queue, key=lambda m: m.time_entered_queue) - - if self.system.debug: - if self.system.mcts_system: - print('MCTS: ', end='') - print(f'Repairman selecting M{next_machine.index} for repair at t={self.env.now}') - - #self.utilization += 1 - #self.env.process(next_machine.repair()) - return next_machine - - - def update_queue_data(self): - queue_length = len(self.get_queue()) - self.queue_data = np.append( - self.queue_data, [[self.env.now, queue_length]], axis=0 - ) diff --git a/simantha/System.py b/simantha/System.py index 5f4d038..3f31385 100644 --- a/simantha/System.py +++ b/simantha/System.py @@ -1,255 +1,4 @@ import copy -<<<<<<< HEAD -import time - -from joblib import Parallel, delayed -import numpy as np -import pandas as pd -from scipy import stats -import simpy - -from .Machine import * -from .Repairman import Repairman - - -class System: - def __init__( - self, - cycle_times, - buffer_capacity=1, - degradation_matrices=None, - maintenance_policy=None, # CBM policy - repairman=None, - maintenance_capacity=1, - scheduling_policy='FIFO', - pm_distribution=None, - cm_distribution=None, - - initial_health_states=None, - initial_remaining_process=None, - initial_buffer_level=None, - - allow_new_failures=True, - initial_time=None, - - mcts_system=False - ): - self.n = len(cycle_times) - self.cycle_times = cycle_times - self.bottleneck = np.argmax(cycle_times) - self.buffer_capacity = buffer_capacity - - self.degradation_matrices = degradation_matrices - - self.repairman = repairman - self.maintenance_policy = maintenance_policy - self.maintenance_capacity = maintenance_capacity - self.scheduling_policy = scheduling_policy - - self.pm_distribution = pm_distribution - self.cm_distribution = cm_distribution - - if initial_health_states: - self.initial_health_states = initial_health_states - else: - self.initial_health_states = [0] * self.n - - if initial_remaining_process: - self.initial_remaining_process = initial_remaining_process - else: - self.initial_remaining_process = [None] * self.n - - if initial_buffer_level: - self.initial_buffer_level = initial_buffer_level - else: - self.initial_buffer_level = [0] * self.n - - self.allow_new_failures = allow_new_failures - self.initial_time = initial_time - - self.initialize() - - self.debug = False - self.mcts_system = mcts_system - - - def initialize(self): - # intialize simulation objects - self.env = simpy.Environment() - - if self.initial_time: - self.env.run(until=self.initial_time) - - self.buffers = [] - for i in range(self.n): - self.buffers.append( - simpy.Container( - self.env, - capacity=self.buffer_capacity, - init=self.initial_buffer_level[i] - ) - ) - self.buffers[i].buffer_data = np.array([[0,0]]) - - self.machines = [] - for i, cycle_time in enumerate(self.cycle_times): - if self.degradation_matrices: - degradation_matrix = self.degradation_matrices[i] - else: - degradation_matrix = Machine.degradation_matrix - - if self.maintenance_policy: - maintenance_threshold = self.maintenance_policy[i] - else: - maintenance_threshold = degradation_matrix.shape[0] - - if self.n == 1: # single-machine line - self.machines.append( - Machine( - self.env, - self, - i, - cycle_time, - degradation_matrix=degradation_matrix, - maintenance_threshold=maintenance_threshold, - initial_health=self.initial_health_states[i], - initial_remaining_process=self.initial_remaining_process[i], - allow_new_failures=self.allow_new_failures - ) - ) - elif i == 0: # first machine - self.machines.append( - Machine( - self.env, - self, - i, - cycle_time, - out_buffer=self.buffers[i], - degradation_matrix=degradation_matrix, - maintenance_threshold=maintenance_threshold, - initial_health=self.initial_health_states[i], - allow_new_failures=self.allow_new_failures - ) - ) - elif 0 < i < self.n - 1: - self.machines.append( - Machine( - self.env, - self, - i, - cycle_time, - in_buffer=self.buffers[i-1], out_buffer=self.buffers[i], - degradation_matrix=degradation_matrix, - maintenance_threshold=maintenance_threshold, - initial_health=self.initial_health_states[i], - initial_remaining_process=self.initial_remaining_process[i], - allow_new_failures=self.allow_new_failures - ) - ) - else: # i == len(cycle_times) - 1, last machine - self.machines.append( - Machine( - self.env, - self, - i, - cycle_time, - in_buffer=self.buffers[i-1], - degradation_matrix=degradation_matrix, - maintenance_threshold=maintenance_threshold, - initial_health=self.initial_health_states[i], - initial_remaining_process=self.initial_remaining_process[i], - allow_new_failures=self.allow_new_failures - ) - ) - - if not self.repairman: - # default FIFO scheduler - self.repairman = Repairman( - self.env, - system=self, - capacity=self.maintenance_capacity, - scheduling_policy='FIFO' - ) - else: - # currently only works for MctsRepairman.MctsRepairman object - self.repairman.__init__( - env=self.env, - system=self, - capacity=self.maintenance_capacity, - scheduling_policy=self.scheduling_policy, - #time_limit=self.repairman.time_limit, - #iteration_limit=self.repairman.iteration_limit - ) - - # MCTS limits are hardcoded here - if self.scheduling_policy == 'MCTS': - self.repairman.limit = {'timeLimit': 1000} - - - self.queue_data = None - - - def simulate( - self, - warm_up_time=0, - simulation_time=0, - verbose=True, - debug=False - ): - self.initialize() # reinitialize system - - # main simulation function - self.warm_up_time = warm_up_time - self.simulation_time = simulation_time - - self.debug = debug - - # simulate the machines for the specified time - self.total_time = warm_up_time + simulation_time - - start_time = time.time() - self.env.run(until=self.total_time) - stop_time = time.time() - - if verbose: - print(f'Finished in {stop_time-start_time:.4f}s') - print(f'Units produced: {self.machines[-1].parts_made}') - - # clean up data - - # queue data - self.queue_data = pd.DataFrame( - self.repairman.queue_data, - columns=['time', 'queue level'] - ).set_index('time') - - self.queue_data = self.queue_data[ - ~self.queue_data.index.duplicated(keep='last') - ] - - self.queue_data = self.queue_data.reindex( - index=range(self.warm_up_time+self.simulation_time+1) - ) - - self.queue_data.loc[0, 'queue level'] = 0 - self.queue_data.ffill(inplace=True) - - # health data - - - # production data - - - - def continue_simulation(self, simulation_time, debug=False): - # simulate from current state without reinitializing - self.debug = debug - - self.repairman.schedule_maintenance() - - self.env.run(until=self.env.now+simulation_time) - -======= import multiprocessing import random import time @@ -346,84 +95,11 @@ def simulate( if verbose: print(f'Simulation finished in {stop-start:.2f}s') print(f'Parts produced: {sum([sink.level for sink in self.sinks])}') ->>>>>>> dev def iterate_simulation( self, replications, warm_up_time=0, -<<<<<<< HEAD - simulation_time=100, - objective='production', - verbose=True - ): - results = [] - - start_time = time.time() - - for _ in range(replications): - self.simulate(warm_up_time, simulation_time, verbose=False) - if objective == 'production': - results.append(self.machines[-1].parts_made) - - stop_time = time.time() - - if verbose: - print(f'Finished in {stop_time-start_time:.4f}s,', - f'{(stop_time-start_time)/replications:.4f}s/replication') - print(f'Average objective value: {np.mean(results):.2f} units') - - return results - - - def get_queue(self): - # returns a list of machines in the current maintenance queue - queue = [machine for machine in self.machines if machine.in_queue] - return queue - - - def tidy_data(self): - # cleans up data after simulation - return - - -# def get_system_copy(system): -# # returns a deep copy of a System instance -# system_copy = System([1]) - -# env_copy = copy.deepcopy(system.env) - -# system_copy.__dict__ = system.__dict__ -# system_copy.env = env_copy - -# for i, buffer in enumerate(system_copy.buffers): -# system_copy.buffers[i] = simpy.Container( -# env_copy, buffer.capacity, buffer.level -# ) - -# for j, machine in enumerate(system_copy.machines): -# machine.env = env_copy - - -def main(): - np.random.seed(1) - system = System( - cycle_times=[3, 5, 4], - buffer_capacity=5, - pm_distribution=stats.randint(10,20), - cm_distribution=stats.randint(20,40) - ) - - print('Simulating system...') - system.simulate(warm_up_time=100, simulation_time=1000) - - print('\nIterating simulation...') - _ = system.iterate_simulation(10, warm_up_time=10, simulation_time=1000) - print() - -if __name__ == '__main__': - main() -======= simulation_time=0, store_system_state=False, verbose=True, @@ -491,4 +167,3 @@ def simulate_in_parallel( availability, system_state ) ->>>>>>> dev diff --git a/simantha/__init__.py b/simantha/__init__.py index 6a1b4a8..ea23543 100644 --- a/simantha/__init__.py +++ b/simantha/__init__.py @@ -1,9 +1,3 @@ -<<<<<<< HEAD -from .Evaluator import * -from .Machine import * -from .Repairman import * -from .System import * -======= from .System import System from .Source import * from .Machine import * @@ -19,4 +13,3 @@ #__title__ = 'simantha' #__author__ = 'Michael Hoffman ' ->>>>>>> dev diff --git a/simantha/simulation.py b/simantha/simulation.py index fae6a69..402449c 100644 --- a/simantha/simulation.py +++ b/simantha/simulation.py @@ -60,13 +60,11 @@ def execute(self): def __lt__(self, other): return ( self.time, - #self.action_priority[self.action.__name__], self.get_action_priority(), self.priority, self.tiebreak ) < ( other.time, - #other.event_priority[other.action.__name__], other.get_action_priority(), other.priority, other.tiebreak diff --git a/simantha/utils.py b/simantha/utils.py index 3ecf9b6..050f0db 100644 --- a/simantha/utils.py +++ b/simantha/utils.py @@ -1,14 +1,3 @@ -<<<<<<< HEAD -import numpy as np - -def generate_degradation_matrix(q, dim=10): - degradation_matrix = np.eye(dim) - for i in range(len(degradation_matrix - 1)): - degradation_matrix[i, i] = 1 - q - degradation_matrix[i, i+1] = q - - return degradation_matrix -======= def generate_degradation_matrix(p, h_max): # Returns an upper bidiagonal degradation matrix with probability p of degrading at # each time step. @@ -26,4 +15,3 @@ def generate_degradation_matrix(p, h_max): WEEK = 7 * DAY MONTH = 30 * DAY YEAR = 365 * DAY ->>>>>>> dev