diff --git a/docs/index.md b/docs/index.md index c105f53..0724c97 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,15 +24,20 @@ If you use our work in your research, please consider citing us with ``` -@misc{müller2025lineflowframeworklearnactive, - title={LineFlow: A Framework to Learn Active Control of Production Lines}, - author={Kai Müller and Martin Wenzel and Tobias Windisch}, - year={2025}, - eprint={2505.06744}, - archivePrefix={arXiv}, - primaryClass={cs.LG}, - url={https://arxiv.org/abs/2505.06744}, +@InProceedings{pmlr-v267-muller25c, + title = {{L}ine{F}low: A Framework to Learn Active Control of Production Lines}, + author = {M\"{u}ller, Kai and Wenzel, Martin and Windisch, Tobias}, + booktitle = {Proceedings of the 42nd International Conference on Machine Learning}, + pages = {45212--45235}, + year = {2025}, + editor = {Singh, Aarti and Fazel, Maryam and Hsu, Daniel and Lacoste-Julien, Simon and Berkenkamp, Felix and Maharaj, Tegan and Wagstaff, Kiri and Zhu, Jerry}, + volume = {267}, + series = {Proceedings of Machine Learning Research}, + month = {13--19 Jul}, + publisher = {PMLR}, + url = {https://proceedings.mlr.press/v267/muller25c.html}, } + ``` ## Authors @@ -40,6 +45,7 @@ If you use our work in your research, please consider citing us with - [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) +- Marie Kraus (University of Applied Sciences, Kempten) ## Funding diff --git a/docs/userguide/visualization.md b/docs/userguide/visualization.md index 4591d83..40201e5 100644 --- a/docs/userguide/visualization.md +++ b/docs/userguide/visualization.md @@ -75,3 +75,8 @@ This will: - Run the simulation in real-time (`realtime=True`). - Display the visualization (`visualize=True`) at normal speed (`factor=1.0`). + +## Zooming and Panning + +Using `+` and `-` keys, you can zoom in and out of the visualization. Moreover, using the arrow +keys, you can pan around the visualization area to explore different parts of the line. diff --git a/lineflow/examples/multi_process.py b/lineflow/examples/multi_process.py index b6bdf7d..0753683 100644 --- a/lineflow/examples/multi_process.py +++ b/lineflow/examples/multi_process.py @@ -95,7 +95,7 @@ def build(self): if __name__ == "__main__": - n_processes = 5 + n_processes = 10 line = MultiProcess(n_processes=n_processes, realtime=True, factor=0.1, alternate=False) agent = make_greedy_policy(n_processes) line.run(simulation_end=3000, agent=agent, visualize=True) diff --git a/lineflow/examples/worker_assignment.py b/lineflow/examples/worker_assignment.py index 56ef5a7..b62313a 100644 --- a/lineflow/examples/worker_assignment.py +++ b/lineflow/examples/worker_assignment.py @@ -113,5 +113,4 @@ def build(self): line = WorkerAssignment(with_rework=True, realtime=False, n_assemblies=7, step_size=2) agent = make_random_agent(7) - line.run(simulation_end=1000, agent=agent, visualize=True, capture_screen=True) - print(line.get_n_parts_produced()) + line.run(simulation_end=1000, agent=agent, visualize=True, capture_screen=False) diff --git a/lineflow/simulation/line.py b/lineflow/simulation/line.py index 94990f1..914c1ea 100644 --- a/lineflow/simulation/line.py +++ b/lineflow/simulation/line.py @@ -12,6 +12,7 @@ Station, Sink, ) +from lineflow.simulation.visualization import Viewpoint logger = logging.getLogger(__name__) @@ -174,14 +175,26 @@ def _register_objects_at_env(self): for o in self._objects.values(): o.register(self.env) - def _draw(self, screen, actions=None): + def _draw(self, actions=None): - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self.teardown_draw() - sys.exit() + self.viewpoint.check_user_input() - screen.fill('white') + self.viewpoint.clear() + + # Draw objects, first connectors, then stations + self._draw_connectors() + self._draw_stations() + + self.viewpoint._draw() + + if actions is not None: + self._draw_actions(actions) + + self._draw_info() + + pygame.display.flip() + + def _draw_info(self): font = pygame.font.SysFont(None, 20) @@ -189,47 +202,59 @@ def _draw(self, screen, actions=None): n_parts = font.render( f'#Parts={self.get_n_parts_produced()}', True, 'black' ) + self.viewpoint.screen.blit(time, time.get_rect(center=(30, 30))) + self.viewpoint.screen.blit(n_parts, n_parts.get_rect(center=(30, 50))) - screen.blit(time, time.get_rect(center=(30, 30))) - screen.blit(n_parts, n_parts.get_rect(center=(30, 50))) - - # Draw objects, first connectors, then stations - self._draw_connectors(screen) - self._draw_stations(screen) - if actions: - self._draw_actions(screen, actions) - pygame.display.flip() - - def _draw_actions(self, screen, actions): + def _draw_actions(self, actions): font = pygame.font.SysFont(None, 20) actions = font.render(f'{actions}', True, 'black') - screen.blit(actions, actions.get_rect(center=(500, 30))) + self.viewpoint.screen.blit(actions, actions.get_rect(center=(500, 30))) - def _draw_stations(self, screen): - self._draw_objects_of_type(screen, Station) + def _draw_stations(self): + self._draw_objects_of_type(Station) - def _draw_connectors(self, screen): - self._draw_objects_of_type(screen, Connector) + def _draw_connectors(self): + self._draw_objects_of_type(Connector) - def _draw_objects_of_type(self, screen, object_type): - for name, obj in self._objects.items(): + def _draw_objects_of_type(self, object_type): + for _, obj in self._objects.items(): if isinstance(obj, object_type): - obj._draw(screen) + obj._draw(self.viewpoint.paper) - def setup_draw(self): - pygame.init() + def _get_object_positions(self): x = [] y = [] for o in self._objects.values(): - o.setup_draw() if hasattr(o, "position"): x.append(o.position[0]) y.append(o.position[1]) + return x, y + + def _adjust_positions(self): + x, y = self._get_object_positions() - return pygame.display.set_mode((max(x) + 100, max(y) + 100)) + if min(x) < 100: + delta_x = 100 - min(x) + for o in self._objects.values(): + if hasattr(o, "position"): + o.position[0] += delta_x + if min(y) < 100: + delta_y = 100 - min(y) + for o in self._objects.values(): + if hasattr(o, "position"): + o.position[1] += delta_y - def teardown_draw(self): - pygame.quit() + x, y = self._get_object_positions() + return max(x), max(y) + + def setup_draw(self): + pygame.init() + + max_x, max_y = self._adjust_positions() + for o in self._objects.values(): + o.setup_draw() + + self.viewpoint = Viewpoint(size=(max_x+100, max_y+100)) def apply(self, values): for object_name in values.keys(): @@ -288,8 +313,8 @@ def run( """ if visualize: - # Stations first, then connectors - screen = self.setup_draw() + self.setup_draw() + # Register objects when simulation is initially started if len(self.env._queue) == 0: @@ -317,16 +342,13 @@ def run( self.apply(actions) if visualize: - if actions is not None: - self._draw(screen, actions) - else: - self._draw(screen) + self._draw(actions) if capture_screen and visualize: - pygame.image.save(screen, f"{self.name}.png") + pygame.image.save(self.viewpoint.screen, f"{self.name}.png") if visualize: - self.teardown_draw() + self.viewpoint.teardown() def get_observations(self, object_name=None): """ diff --git a/lineflow/simulation/visualization.py b/lineflow/simulation/visualization.py new file mode 100644 index 0000000..b5de182 --- /dev/null +++ b/lineflow/simulation/visualization.py @@ -0,0 +1,68 @@ +import pygame +import sys + +class Viewpoint: + """ + A class to manage the viewpoint for rendering a 2D surface with zoom and pan capabilities. + """ + + def __init__( + self, + size=None, + position=None, + zoom=1, + ): + + + if size is None: + size = (1410, 1000) + self.paper = pygame.Surface(size) + + + self.screen = pygame.display.set_mode((1280, 720)) + + if position is None: + position = (0, 0) + + self._view = pygame.Vector3(position[0], position[1], zoom) + + def check_user_input(self): + + if pygame.key.get_pressed()[pygame.K_PLUS]: + self._view.z += 0.1 + + if pygame.key.get_pressed()[pygame.K_MINUS]: + self._view.z -=0.1 + + if pygame.key.get_pressed()[pygame.K_UP]: + self._view.y += 10 + + if pygame.key.get_pressed()[pygame.K_DOWN]: + self._view.y -= 10 + + if pygame.key.get_pressed()[pygame.K_LEFT]: + self._view.x += 10 + + if pygame.key.get_pressed()[pygame.K_RIGHT]: + self._view.x -= 10 + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.teardown() + + self._view.z = max(self._view.z, 0.1) + self._view.z = min(self._view.z, 5) + + def clear(self): + self.screen.fill('white') + self.paper.fill('white') + + def _draw(self): + self.screen.blit( + pygame.transform.smoothscale_by(self.paper, self._view.z), + (self._view.x,self._view.y), + ) + + def teardown(self): + pygame.quit() + sys.exit() diff --git a/tests/test_line.py b/tests/test_line.py index c622290..1c56ea8 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -151,20 +151,3 @@ def test_step_function(self): state, terminated = self.line.step() # Check if another step_size is passed self.assertTrue(state.df()["T_end"].iloc[-1] == self.line.step_size * 2) - - def test_setup_draw(self): - self.line = ShowCase() - self.line.build() # Make sure the line is built before testing - - # Run the setup_draw method - screen = self.line.setup_draw() - - # Validate screen dimensions - x_positions = [o.position[0] for o in self.line._objects.values() if isinstance(o, Station)] - y_positions = [o.position[1] for o in self.line._objects.values() if isinstance(o, Station)] - - expected_width = max(x_positions) + 100 - expected_height = max(y_positions) + 100 - - self.assertEqual(screen.get_width(), expected_width) - self.assertEqual(screen.get_height(), expected_height)