diff --git a/docs/index.md b/docs/index.md index c105f53..aa450c4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,11 +36,20 @@ If you use our work in your research, please consider citing us with ``` ## Authors +`LineFlow` was originally developed at the [University of Applied Sciences +Kempten](http://hs-kempten.de/ims) by: + +- [Kai Müller](https://mueller-kai.github.io) +- [Tobias Windisch](https://www.tobias-windisch.de) +- [Martin Wenzel](https://www.hs-kempten.de/personen/martin-wenzel) + +`LineFlow` gratefully acknowledges contributions from: + +- Kilian Führer +- [Andreas Fritz](https://www.hs-kempten.de/personen/andreas-fritz) +- Marie Kraus -- [Kai Müller](https://mueller-kai.github.io) (University of Applied Sciences, Kempten) -- [Tobias Windisch](https://www.tobias-windisch.de) (University of Applied Sciences, Kempten) -- Martin Wenzel (University of Applied Sciences, Kempten) ## Funding -The research behind LineFlow is funded by the Bavarian state ministry of research. Learn more [here](https://kefis.fza.hs-kempten.de/de/forschungsprojekt/599-lineflow). +The research behind `LineFlow` is funded by the Bavarian state ministry of research. Learn more [here](https://kefis.fza.hs-kempten.de/de/forschungsprojekt/599-lineflow). diff --git a/lineflow/examples/multi_sink.py b/lineflow/examples/multi_sink.py index bb2111c..564f0e8 100644 --- a/lineflow/examples/multi_sink.py +++ b/lineflow/examples/multi_sink.py @@ -62,5 +62,5 @@ def build(self): if __name__ == '__main__': line = MultiSink(realtime=False, n_sinks=5, alternate=False) agent = make_greedy_policy(5) - line.run(simulation_end=200, agent=agent, visualize=True, capture_screen=True) + line.run(simulation_end=200, agent=agent, visualize=True, capture_screen=False) print('Produced parts: ', line.get_n_parts_produced()) diff --git a/lineflow/learning/helpers.py b/lineflow/learning/helpers.py index 30d5560..8a6d899 100644 --- a/lineflow/learning/helpers.py +++ b/lineflow/learning/helpers.py @@ -9,26 +9,19 @@ def make_stacked_vec_env(line, simulation_end, reward="uptime", n_envs=10, n_stack=5): - if n_envs > 1: - env = make_vec_env( - env_id=LineSimulation, - n_envs=n_envs, - vec_env_cls=SubprocVecEnv, - vec_env_kwargs={ - 'start_method': 'fork', - }, - env_kwargs={ - "line": line, - "simulation_end": simulation_end, - "reward": reward, - } - ) - else: - env = LineSimulation( - line=line, - simulation_end=simulation_end, - reward=reward, - ) + env = make_vec_env( + env_id=LineSimulation, + n_envs=n_envs, + vec_env_cls=SubprocVecEnv, + vec_env_kwargs={ + 'start_method': 'fork', + }, + env_kwargs={ + "line": line, + "simulation_end": simulation_end, + "reward": reward, + } + ) if n_stack > 1: diff --git a/lineflow/simulation/__init__.py b/lineflow/simulation/__init__.py index 962a7d9..2051a71 100644 --- a/lineflow/simulation/__init__.py +++ b/lineflow/simulation/__init__.py @@ -5,6 +5,7 @@ from lineflow.simulation.stations import Sink #noqa from lineflow.simulation.stations import Switch #noqa from lineflow.simulation.stations import Process #noqa +from lineflow.simulation.stations import SequentialProcess #noqa from lineflow.simulation.stations import Magazine #noqa from lineflow.simulation.stations import WorkerPool #noqa from lineflow.simulation.line import Line #noqa diff --git a/lineflow/simulation/movable_objects.py b/lineflow/simulation/movable_objects.py index 3ab7d48..eff7e0d 100644 --- a/lineflow/simulation/movable_objects.py +++ b/lineflow/simulation/movable_objects.py @@ -57,6 +57,9 @@ class Worker(object): def __init__(self, name, transition_time=5, skill_levels=None): self.name = name self.transition_time = transition_time + + if skill_levels is None: + skill_levels = {} self.skill_levels = skill_levels def register(self, env): @@ -118,8 +121,9 @@ def work(self): class Part(MovableObject): - def __init__(self, env, name, specs=None, color='Orange'): + def __init__(self, env, name, specs=None, color='Orange', nok_probability=0.0): super(Part, self).__init__(env, name, specs=specs) + self.nok_probability = nok_probability self._color = color def is_valid_for_assembly(self, station_name): @@ -142,22 +146,27 @@ def _draw(self, screen, x, y, width, height): _part_rect = pygame.Rect(x, y, width, height) pygame.draw.rect(screen, self._color, _part_rect, border_radius=1) + def get_processing_time(self, station): + return self.specs.get(station, {}).get("extra_processing_time", 0) + + def get_error_probability(self, station): + return self.specs.get(station, {}).get("error_probability", 0.0) + + def get_error_time(self, station): + return self.specs.get(station, {}).get("error_time", 0) + class Carrier(MovableObject): - def __init__(self, env, name, color='Black', width=30, height=10, capacity=np.inf, part_specs=None): + def __init__(self, env, name, color='Black', width=30, height=10, capacity=np.inf): super(Carrier, self).__init__(env, name, specs=None) self.capacity = capacity self._color = color self._width = width self._height = height - if part_specs is None: - part_specs = {} - self.part_specs = part_specs.copy() - self._width_part = 0.8*self._width - if capacity < 15: + if capacity < np.inf: self._width_part = self._width_part / self.capacity self._height_part = 0.7*self._height @@ -216,6 +225,9 @@ def __iter__(self): for part in self.parts.values(): yield part + def get_parts_for_station(self, station): + return [p for p in self if station in p.specs] + def get_additional_processing_time(self, station): total_time = 0 @@ -224,6 +236,3 @@ def get_additional_processing_time(self, station): total_time += processing_time return total_time - - - diff --git a/lineflow/simulation/stations.py b/lineflow/simulation/stations.py index 3f1a3c6..4c160f7 100644 --- a/lineflow/simulation/stations.py +++ b/lineflow/simulation/stations.py @@ -193,6 +193,12 @@ def _draw(self, screen): def _draw_info(self, screen): pass + def _is_nok_part(self, component): + return self.random.choice( + [True, False], + p=[component.nok_probability, 1 - component.nok_probability], + ) + def _draw_n_workers(self, screen): if not self.is_automatic: font = pygame.font.SysFont(None, 14) @@ -356,9 +362,9 @@ def __init__( buffer_in=None, buffer_out=None, buffer_component=None, + buffer_return=None, processing_time=5, position=None, - buffer_return=None, processing_std=None, NOK_part_error_time=2, worker_pool=None, @@ -379,8 +385,15 @@ def __init__( if buffer_out is not None: self._connect_to_output(buffer_out) + self.buffer_component = [] + if buffer_component is not None: - self.buffer_component = buffer_component.connect_to_output(self) + + if isinstance(buffer_component, Buffer): + buffer_component = [buffer_component] + + for buffer in buffer_component: + self._connect_to_component_input(buffer) if buffer_return is not None: self.buffer_return = buffer_return.connect_to_input(self) @@ -405,7 +418,7 @@ def init_state(self): def connect_to_component_input(self, station, *args, **kwargs): buffer = Buffer(name=f"Buffer_{station.name}_to_{self.name}", *args, **kwargs) - self.buffer_component = buffer.connect_to_output(self) + self._connect_to_component_input(buffer) station._connect_to_output(buffer) def connect_to_component_return(self, station, *args, **kwargs): @@ -413,14 +426,22 @@ def connect_to_component_return(self, station, *args, **kwargs): self.buffer_return = buffer.connect_to_input(self) station._connect_to_input(buffer) + def _connect_to_component_input(self, buffer): + self.buffer_component.append(buffer.connect_to_output(self)) + def _has_invalid_components_on_carrier(self, carrier): """ Checks if any of the components on the carrier is not valid for assembly. In this case, `True` is returned. Otherwise, `False` is returned. """ for component in carrier: + # Check assembly condition if not component.is_valid_for_assembly(self.name): return True + + # Check if part is self._is_nok_part(component): + if self._is_nok_part(component): + return True return False def _draw_info(self, screen): @@ -440,41 +461,50 @@ def run(self): # Update current_carrier and count parts of carrier self.state['carrier'].update(carrier.name) - # Run until carrier with components each having a valid assembly condition is - # received - while True: - # Wait to get component - carrier_component = yield self.env.process(self.buffer_component()) - self.state['carrier_component'].update(carrier_component.name) - - # Check component - if self._has_invalid_components_on_carrier(carrier_component): - yield self.env.process(self.set_to_error()) - yield self.env.timeout(self.NOK_part_error_time) - self.state['n_scrap_parts'].increment() - - # send carrier back - if hasattr(self, 'buffer_return'): - carrier_component.parts.clear() - yield self.env.process(self.buffer_return(carrier_component)) - yield self.env.process(self.set_to_waiting()) - continue - else: - # All components are valid, proceed with assembly - break + carrier_components = [] + for buffer_component in self.buffer_component: + # Run until carrier with components each having a valid assembly condition is + # received + while True: + # Wait to get component + carrier_component = yield self.env.process(buffer_component()) + self.state['carrier_component'].update(carrier_component.name) + + # Check component + if self._has_invalid_components_on_carrier(carrier_component): + yield self.env.process(self.set_to_error()) + yield self.env.timeout(self.NOK_part_error_time) + self.state['n_scrap_parts'].increment() + + # send carrier back + if hasattr(self, 'buffer_return'): + carrier_component.parts.clear() + yield self.env.process(self.buffer_return(carrier_component)) + yield self.env.process(self.set_to_waiting()) + continue + + else: + # All components are valid, proceed with assembly + carrier_components.append(carrier_component) + break # Process components yield self.env.process(self.set_to_work()) + + add_time = sum( + [c.get_additional_processing_time(self.name) for c in carrier_components] + ) processing_time = self._sample_exp_time( - time=self.processing_time + carrier.get_additional_processing_time(self.name), + time=self.processing_time + add_time, scale=self.processing_std, ) yield self.env.timeout(processing_time) self.state['processing_time'].update(processing_time) - for component in carrier_component: - carrier.assemble(component) + for carrier_component in carrier_components: + for component in carrier_component: + carrier.assemble(component) # Release workers self.release_workers() @@ -513,7 +543,6 @@ def __init__( processing_std=None, rework_probability=0, worker_pool=None, - ): super().__init__( @@ -582,6 +611,98 @@ def run(self): yield self.turn_off() +class SequentialProcess(Process): + """ + SequentialProcess stations take a carrier as input, process each part on the carrier + """ + def __init__( + self, + name, + buffer_in=None, + buffer_out=None, + position=None, + worker_pool=None, + processing_time=0, + processing_std=0.1, + error_std=0, + ): + + super().__init__( + name=name, + buffer_in=buffer_in, + buffer_out=buffer_out, + processing_time=processing_time, + position=position, + processing_std=processing_std, + rework_probability=0, + worker_pool=worker_pool, + ) + + self.processing_std = processing_std + self.error_std = error_std + assert self.processing_std >= 0 and self.processing_std <= 1 + assert self.error_std >= 0 and self.error_std <= 1 + + def run(self): + + while True: + if self.is_on(): + # Wait to get part from buffer_in + yield self.env.process(self.set_to_waiting()) + carrier = yield self.env.process(self.buffer_in()) + self.state['carrier'].update(carrier.name) + + yield self.env.timeout(2) + + total_processing_time = 0 + + for part in carrier.get_parts_for_station(self.name): + + yield self.env.process(self.request_workers()) + self.state['n_workers'].update(self.n_workers) + + error_proba = part.get_error_probability(self.name) + + if self.random.uniform(0, 1) < error_proba: + yield self.env.process(self.set_to_error()) + # Release workers + self.release_workers() + self.state['n_workers'].update(self.n_workers) + error_time = self._sample_exp_time( + time=part.get_error_time(self.name), + scale=self.error_std, + rework_probability=self.rework_probability, + ) + + yield self.env.timeout(error_time) + total_processing_time += error_time + yield self.env.process(self.set_to_waiting()) + yield self.env.process(self.request_workers()) + self.state['n_workers'].update(self.n_workers) + + yield self.env.process(self.set_to_work()) + + processing_time = self._sample_exp_time( + time=self.processing_time+part.get_processing_time(self.name), + scale=self.processing_std, + rework_probability=self.rework_probability, + ) + yield self.env.timeout(processing_time) + total_processing_time += processing_time + # Release workers + self.release_workers() + yield self.env.process(self.set_to_waiting()) + + self.state['processing_time'].update(total_processing_time) + + # Wait to place carrier to buffer_out + yield self.env.process(self.buffer_out(carrier)) + self.state['carrier'].update(None) + + else: + yield self.turn_off() + + class Source(Station): ''' Source station generating parts on carriers. @@ -608,6 +729,8 @@ class Source(Station): unlimited_carriers (bool): If source has the ability to create unlimited carriers carrier_capacity (int): Defines how many parts can be assembled on a carrier. If set to default (infinity) or > 15, carrier will be visualized with one part. + nok_probability (float): Between 0 and 1, probability of a part on the carrier being labled + as nok carrier_min_creation (int): Minimum number of carriers of same spec created subsequentially carrier_max_creation (int): Maximum number of carriers of same spec created subsequentially @@ -628,6 +751,7 @@ def __init__( carrier_capacity=np.inf, carrier_specs=None, carrier_min_creation=1, + nok_probability=0.0, carrier_max_creation=None, ): super().__init__( @@ -664,6 +788,8 @@ def __init__( self._carrier_counter = 0 self.init_waiting_time = waiting_time + self.nok_probability = nok_probability + assert 0 <= self.nok_probability <= 1, "nok_probability must be between 0 and 1" def init_state(self): @@ -703,14 +829,6 @@ def _assert_init_args(self, unlimited_carriers, carrier_capacity, buffer_in): def create_carrier(self): - if self._carrier_counter == 0: - carrier_spec = self.random.choice(list(self.carrier_specs.keys())) - self.state['carrier_spec'].update(carrier_spec) - self._carrier_counter = self.random.randint( - self.carrier_min_creation, - self.carrier_max_creation + 1, - ) - carrier_spec = self.state['carrier_spec'].to_str() name = f'{self.name}_{carrier_spec}_{self.carrier_id}' @@ -718,28 +836,42 @@ def create_carrier(self): self.env, name=name, capacity=self.carrier_capacity, - part_specs=self.carrier_specs[carrier_spec], ) self.carrier_id += 1 - self._carrier_counter -= 1 return carrier - def create_parts(self, carrier): + def get_current_carrier_spec(self): + if self._carrier_counter == 0: + carrier_spec = self.random.choice(list(self.carrier_specs.keys())) + self.state['carrier_spec'].update(carrier_spec) + self._carrier_counter = self.random.randint( + self.carrier_min_creation, + self.carrier_max_creation + 1, + ) + self.carrier_id += 1 + self._carrier_counter -= 1 + + return self.carrier_specs[self.state['carrier_spec'].to_str()].copy() + + + def create_parts(self, carrier, carrier_spec): """ Creates the parts based on the part_specs attribute For each dict in the part_specs list one part is created """ parts = [] - for part_id, (part_name, part_spec) in enumerate(carrier.part_specs.items()): + for part_id, (part_name, part_spec) in enumerate(carrier_spec.items()): part = Part( env=self.env, name=f"{carrier.name}_{part_name}_{part_id}", specs=part_spec, + nok_probability=self.nok_probability, ) part.create(self.position) parts.append(part) + return parts def assemble_parts_on_carrier(self, carrier, parts): @@ -749,9 +881,9 @@ def assemble_parts_on_carrier(self, carrier, parts): for part in parts: carrier.assemble(part) - def assemble_carrier(self, carrier): + def assemble_carrier(self, carrier, carrier_spec): - parts = self.create_parts(carrier) + parts = self.create_parts(carrier, carrier_spec) self.state['part'].update(parts[0].name) processing_time = self._sample_exp_time( @@ -785,8 +917,10 @@ def run(self): else: carrier = yield self.env.process(self.buffer_in()) + carrier_spec = self.get_current_carrier_spec() + yield self.env.process(self.set_to_work()) - carrier = yield self.env.process(self.assemble_carrier(carrier)) + carrier = yield self.env.process(self.assemble_carrier(carrier, carrier_spec)) yield self.env.process(self.set_to_waiting()) yield self.env.process(self.buffer_out(carrier)) @@ -1067,7 +1201,6 @@ def __init__( actionable_magazine=True, carrier_getting_time=2, carriers_in_magazine=0, - carrier_specs=None, carrier_min_creation=1, carrier_max_creation=None, ): @@ -1086,10 +1219,6 @@ def __init__( self.init_carriers_in_magazine = carriers_in_magazine self.carrier_getting_time = carrier_getting_time - if carrier_specs is None: - carrier_specs = {"carrier": {"part": {}}} - self.carrier_specs = carrier_specs - self.unlimited_carriers = unlimited_carriers self.carrier_capacity = carrier_capacity self.carrier_id = 1 @@ -1131,18 +1260,16 @@ def _assert_init_args(self, buffer_in, unlimited_carriers, carriers_in_magazine, def create_carrier(self): if self._carrier_counter == 0: - self._current_carrier_spec = self.random.choice(list(self.carrier_specs.keys())) self._carrier_counter = self.random.randint( self.carrier_min_creation, self.carrier_max_creation + 1, ) - name = f'{self.name}_{self._current_carrier_spec}_{self.carrier_id}' + name = f'{self.name}_{self.carrier_id}' carrier = Carrier( self.env, name=name, capacity=self.carrier_capacity, - part_specs=self.carrier_specs[self._current_carrier_spec], ) self.carrier_id += 1 self._carrier_counter -= 1 diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 4d808dd..29d3e06 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -122,6 +122,7 @@ def build(self): ) + class SimpleLineWithSendingBack(Line): def build(self): @@ -222,8 +223,8 @@ def test_transition_time(self): # 5 (processing) + 1 (put) + 1 (get next one) self.assertEqual( self.df_valid[ - (self.df_valid['carrier'] == 'Magazine Source_carrier_2') & - (self.df_valid['carrier_component'] == 'Magazine Component Source_carrier_2') + (self.df_valid['carrier'] == 'Magazine Source_2') & + (self.df_valid['carrier_component'] == 'Magazine Component Source_2') ].iloc[0]["T_start"], 26 ) @@ -235,3 +236,75 @@ def test_assembly_condition(self): # Should have multiple carrier_components self.assertGreater(len(self.df["carrier_component"].unique()), 2) + + +class TestMultilpleComponentBuffers(unittest.TestCase): + + class AssemblyWithMultipleComponentBuffers(Line): + + def build(self): + + source_main = Source( + 'Source', + processing_time=2, + processing_std=0, + unlimited_carriers=True, + carrier_capacity=1+3, + position=(100, 200), + ) + + assembly = Assembly( + 'A', + position=(300, 200), + ) + + + sc_1 = Source('SC1', + unlimited_carriers=True, + position=(220, 400), + processing_time=7, + nok_probability=0.2, + carrier_specs={ + 'T1': {'Part1': {'A': {"extra_processing_time": 1}}}, + 'T2': {'Part1': {'A': {"extra_processing_time": 4}}} + }, + ) + + sc_2 = Source('SC2', + unlimited_carriers=True, + position=(300, 400), + processing_time=2, + nok_probability=0.1, + carrier_specs={ + 'T1': {'Part2': {'A': {"extra_processing_time": 4}}}, + 'T2': {'Part2': {'A': {"extra_processing_time": 2}}} + }, + ) + + sc_3 = Source('SC3', + position=(380, 400), + processing_time=5, + unlimited_carriers=True, + carrier_specs={ + 'T1': {'Part3': {'A': {"extra_processing_time": 3}}}, + }, + ) + + assembly.connect_to_component_input(sc_1, capacity=4) + assembly.connect_to_component_input(sc_2, capacity=4) + assembly.connect_to_component_input(sc_3, capacity=4) + + sink = Sink( + 'Sink', + processing_time=1, + processing_std=0, + position=(600, 200), + ) + assembly.connect_to_input(source_main) + assembly.connect_to_output(sink) + + def test_run(self): + line = TestMultilpleComponentBuffers.AssemblyWithMultipleComponentBuffers() + line.run(400, visualize=False) + df = line.get_observations('A') + self.assertGreaterEqual(df.n_scrap_parts.max(), 1) diff --git a/tests/test_connectors.py b/tests/test_connectors.py index 9c50040..cd02743 100644 --- a/tests/test_connectors.py +++ b/tests/test_connectors.py @@ -95,7 +95,7 @@ def test_with_fast_removal(self): line.run(60) df = line.get_observations('Sink') - index = df[df['carrier'] == 'Magazine_carrier_1'].index + index = df[df['carrier'] == 'Magazine_1'].index t_end = df.loc[index[0] + 1, 'T_end'] # First part should be removed after # 1 (getting time) + 1 (put) + 15 (transition)+ 1 (put) + 1 (process) + 1 (put) + 15 (transition) + 1 (get) + 1 (remove) @@ -104,7 +104,7 @@ def test_with_fast_removal(self): t_end, 1 + 1 + 15 + 1 + 1 + 1 + 15 + 1 + 1) - index = df[df['carrier'] == 'Magazine_carrier_2'].index + index = df[df['carrier'] == 'Magazine_2'].index t_end = df.loc[index[0] + 1, 'T_end'] # Second part should be removed after # 37 (time first carrier needs with removal) + 15 / 3 (time second diff --git a/tests/test_line.py b/tests/test_line.py index c622290..b7a46e1 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -27,14 +27,16 @@ def build(self): m1 = Magazine( name='M1', unlimited_carriers=True, - carrier_specs={ - 'A': {'Part': {'C2': {"assembly_condition": 20}}} - } ) m2 = Magazine('M2', unlimited_carriers=True) - a1 = Source('A1') + a1 = Source( + name='A1', + carrier_specs={ + 'A': {'Part': {'C2': {"assembly_condition": 20}}} + } + ) c1 = Source('C1') diff --git a/tests/test_magazine.py b/tests/test_magazine.py index 4e7b343..713743e 100644 --- a/tests/test_magazine.py +++ b/tests/test_magazine.py @@ -174,7 +174,7 @@ def policy(line_state, env): # Afterwards no more carriers self.assertListEqual(df.iloc[6:].carrier.unique().tolist(), [None]) - def test_carrier_inseration_by_policy(self): + def test_carrier_insertion_by_policy(self): self.removal_line = SimplestLine() self.removal_line.build() @@ -195,9 +195,9 @@ def policy(line_state, env): self.removal_line.run(simulation_end=50, agent=policy) df = self.removal_line.get_observations("Magazine") # Magazine produces carrier 'Magazine_cr_1' - self.assertEqual(df.iloc[2].carrier, 'Magazine_carrier_1') + self.assertEqual(df.iloc[2].carrier, 'Magazine_1') # Afterwards all carriers are removed exept one (the tenth) - self.assertEqual(df.iloc[5].carrier, 'Magazine_carrier_10') + self.assertEqual(df.iloc[5].carrier, 'Magazine_10') def test_carrier_specs(self): source = Source( @@ -231,7 +231,7 @@ def test_carrier_specs(self): self.assertEqual(carrier.name, 'TestSource_Carrier1_1') - parts = source.create_parts(carrier) + parts = source.create_parts(carrier, source.carrier_specs['Carrier1']) self.assertEqual(len(parts), 2) self.assertDictEqual( diff --git a/tests/test_part_dependence.py b/tests/test_part_dependence.py new file mode 100644 index 0000000..e80f598 --- /dev/null +++ b/tests/test_part_dependence.py @@ -0,0 +1,126 @@ +import unittest +from lineflow.simulation.movable_objects import ( + Carrier, + Part, +) +from lineflow.simulation import ( + WorkerPool, + Source, + Sink, + Line, + SequentialProcess, +) + + +carrier_specs = { + 'Type_A': { + 'Part1': { + 'Process1': { + 'extra_processing_time': 20, + 'error_probability': 0.0, + 'error_time': 0, + }, + 'Process2': { + 'extra_processing_time': 0, + 'error_probability': 0.0, + 'error_time': 0, + } + }, + 'Part2': { + 'Process2': { + 'extra_processing_time': 10, + 'error_probability': 0.5, + 'error_time': 10, + } + } + }, + 'Type_B': { + 'Part1': { + 'Process1': { + 'extra_processing_time': 0, + 'error_probability': 0.0, + 'error_time': 0, + + }, + 'Process2': { + 'extra_processing_time': 20, + 'error_probability': 0.5, + 'error_time': 10, + } + }, + 'Part2': { + 'Process1': { + 'extra_processing_time': 10, + 'error_probability': 0.0, + 'error_time': 0, + }, + 'Process2': { + 'extra_processing_time': 0, + 'error_probability': 0.0, + 'error_time': 0, + } + }, + }, +} + +class SequentialProcessLine(Line): + def build(self): + source = Source( + name='Source', + processing_time=10, + unlimited_carriers=True, + carrier_capacity=2, + carrier_min_creation=10, + carrier_specs=carrier_specs, + ) + pool = WorkerPool(name='Pool', n_workers=4) + + p1 = SequentialProcess('Process1', worker_pool=pool) + p2 = SequentialProcess('Process2', worker_pool=pool) + sink = Sink('Sink') + + p1.connect_to_input(source, capacity=15) + p2.connect_to_input(p1, capacity=15) + sink.connect_to_input(p2) + + + +class TestCarrier(unittest.TestCase): + + class EnvMock(object): + + def __init__(self): + self.time = 0 + + @property + def now(self): + self.time = self.time + 1 + return self.time + + def test_get_parts(self): + + carrier = Carrier( + env=self.EnvMock(), + name='Carrier_1', + capacity=2, + ) + + for part_name, part_spec in carrier_specs['Type_A'].items(): + part = Part( + env=self.EnvMock(), + name=part_name, + specs=part_spec, + ) + carrier.assemble(part) + + self.assertEqual(len(carrier.get_parts_for_station('Process1')), 1) + self.assertEqual(len(carrier.get_parts_for_station('Process2')), 2) + +class TestPartDependentProcessLine(unittest.TestCase): + + def setUp(self): + self.line = SequentialProcessLine() + + def test_get_carrier(self): + self.line.run(simulation_end=1000, visualize=False) + self.assertTrue(self.line.get_n_parts_produced() > 10) diff --git a/tests/test_parts.py b/tests/test_parts.py index 07d3386..7a1f718 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -14,6 +14,11 @@ class TestParts(unittest.TestCase): def setUp(self): self.env = simpy.Environment() + self.specs = { + "A1": {'assembly_condition': 5}, + "P1": {'extra_processing_time': 10, 'state': 'nok'}, + } + def test_init(self): part = Part(env=self.env, name='C1-1') @@ -30,7 +35,7 @@ def test_execption_on_position(self): with self.assertRaises(ValueError): part.create(position=(1, 2)) - def test_specs(self): + def test_dict_like_access(self): specs = {"assembly_condition": 5} part = Part(env=self.env, name="env", specs=specs) self.assertEqual(part["assembly_condition"], 5) @@ -39,6 +44,15 @@ def test_specs_with_problematic_values(self): specs = {"create": 5} part = Part(env=self.env, name="env", specs=specs) + def test_is_valid_for_assembly(self): + part = Part(env=self.env, name="env", specs=self.specs) + + self.assertTrue(part.is_valid_for_assembly("A1")) + + def test_is_nok(self): + part = Part(env=self.env, name="env", specs=self.specs) + self.assertEqual(part.nok_probability, 0.0) + class TestCarrier(unittest.TestCase): @@ -95,4 +109,3 @@ def test_draw(self): part.create(position=pygame.Vector2(200, 100)) carrier = Carrier(env=self.env, name='WPC1') carrier.assemble(part) - diff --git a/tests/test_switches.py b/tests/test_switches.py index 8c37094..2ae8cbb 100644 --- a/tests/test_switches.py +++ b/tests/test_switches.py @@ -104,7 +104,7 @@ def test_without_alternate(self): # All parts should visit P1, none vists P2 and P3 p1, p2, p3 = self.get_parts_at_processes(line) - self.assertListEqual(p1, [f'M1_carrier_{i}' for i in range(1, 7)]) + self.assertListEqual(p1, [f'M1_{i}' for i in range(1, 7)]) self.assertListEqual(p2, []) self.assertListEqual(p3, []) @@ -137,15 +137,15 @@ def test_alternate(self): self.assertListEqual( p1, - ['M1_carrier_1', 'M2_carrier_2', 'M1_carrier_4', 'M2_carrier_5'] + ['M1_1', 'M2_2', 'M1_4', 'M2_5'] ) self.assertListEqual( p2, - ['M2_carrier_1', 'M1_carrier_3', 'M2_carrier_4', 'M1_carrier_6'] + ['M2_1', 'M1_3', 'M2_4', 'M1_6'] ) self.assertListEqual( p3, - ['M1_carrier_2', 'M2_carrier_3', 'M1_carrier_5'] + ['M1_2', 'M2_3', 'M1_5'] ) def test_with_policy_on_fixed_index(self): @@ -166,5 +166,5 @@ def policy(*args): line.run(100, agent=policy) p1, p2, p3 = self.get_parts_at_processes(line) self.assertListEqual(p1, []) - self.assertListEqual(p2, [f'M2_carrier_{i}' for i in range(1, 7)]) + self.assertListEqual(p2, [f'M2_{i}' for i in range(1, 7)]) self.assertListEqual(p3, [])