Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,28 @@ 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

- [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

Expand Down
5 changes: 5 additions & 0 deletions docs/userguide/visualization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 1 addition & 2 deletions lineflow/examples/worker_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
74 changes: 37 additions & 37 deletions lineflow/simulation/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Station,
Sink,
)
from lineflow.simulation.visualization import Viewpoint

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -174,47 +175,51 @@ 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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved more stuff into the Viewpoint class. Maybe - after the refactoring - all pygame specific things can happen inside Viewpoint?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't put everything in the Viewpoint class. But the visualization.py is supposed to contain every pygame specific thing in the future.


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)

time = font.render('T={:.2f}'.format(self.env.now), True, 'black')
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()
Expand All @@ -225,11 +230,9 @@ def setup_draw(self):
if hasattr(o, "position"):
x.append(o.position[0])
y.append(o.position[1])

self.viewpoint = Viewpoint(size=(max(x)+100,max(y)+100))

return pygame.display.set_mode((max(x) + 100, max(y) + 100))

def teardown_draw(self):
pygame.quit()

def apply(self, values):
for object_name in values.keys():
Expand Down Expand Up @@ -288,8 +291,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:
Expand Down Expand Up @@ -317,16 +320,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):
"""
Expand Down
68 changes: 68 additions & 0 deletions lineflow/simulation/visualization.py
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Contributor

@windisch windisch Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the screen an instance variable of the viewpoint. That way, a lot of things happen inside the viewpoint solely.


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():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HSMarieK: I moved that piece to here as well - I think we could collect all sorts of user input here - what do you think?

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()
17 changes: 0 additions & 17 deletions tests/test_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)