Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.2.0
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)