diff --git a/.docs/module_class_overview.png b/.docs/module_class_overview.png
new file mode 100644
index 0000000..921628f
Binary files /dev/null and b/.docs/module_class_overview.png differ
diff --git a/.docs/module_class_overview.vsdx b/.docs/module_class_overview.vsdx
new file mode 100644
index 0000000..03ab17c
Binary files /dev/null and b/.docs/module_class_overview.vsdx differ
diff --git a/.docs/~$$module_class_overview.~vsdx b/.docs/~$$module_class_overview.~vsdx
new file mode 100644
index 0000000..f515c9e
Binary files /dev/null and b/.docs/~$$module_class_overview.~vsdx differ
diff --git a/.gitignore b/.gitignore
index 07f43b8..d53d2f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,165 @@
-data/*
\ No newline at end of file
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+*_log.json
+*.csv
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e9e6a80
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "python.testing.unittestArgs": [
+ "-v",
+ "-s",
+ "./tests",
+ "-p",
+ "test_*.py"
+ ],
+ "python.testing.pytestEnabled": false,
+ "python.testing.unittestEnabled": true
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 85a6dcd..b034000 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,604 @@
-# cellular-automatic
+# Pedestrian simulation written in python
-## Testing
-to run unittests execute: python -m unittest discover -s tests -p "*"
\ No newline at end of file
+## Table of Contents
+
+[1. Overview](#section/overview) \
+[1.1 Getting Started](#overview/getting-started)
+
+[2. Configuration](#section/configuration) \
+[2.1 Configuration Types](#configuration/types) \
+[2.1.1 Simulation Type](#configuration/types) \
+[2.1.2 Grid Type](#configuration/types/grid-type) \
+[2.1.3 Target Type](#configuration/types/target-type) \
+[2.1.4 Spawner Type](#configuration/types/spawner-type) \
+[2.1.5 Obstacle Type](#configuration/types/obstacle-type) \
+[2.1.6 Social Distancing Type](#configuration/types/social-distancing-type) \
+[2.1.7 Distancing Type](#configuration/types/distancing-type) \
+[2.1.8 Cell Type](#configuration/types/cell-type) \
+[2.2 Algorithm Implementations](#configuration/implementations) \
+[2.2.1 Heatmap Generator Type](#configuration/implementations/heatmap-generator-type) \
+[2.2.2 Neighbourhood Type](#configuration/implementations/neighbourhood-type) \
+[2.2.3 Distancing Algorithm Type](#configuration/implementations/distancing-algorithm-type) \
+[2.3 Configuration Enums](#configuration/enums) \
+[2.3.1 Cell State](#configuration/enums/cell-state) \
+[2.3.2 TargetingStrategy Type](#configuration/enums/targeting-type)
+
+[3. Visualisation](#section/visualisation) \
+[3.1 Visualisation Features](#visualisation/features) \
+[3.2 Keyboard Shortcuts](#visualisation/keyboard-shortcuts) \
+[3.3 Mouse Interaction](#visualisation/mouse-interaction)
+
+[4. Architecture](#architecture) \
+[4.1 Modules](#architecture/modules) \
+[4.1.1 Overview](#architecture/overview) \
+[4.1.2 Exceptions Module](#architecture/exceptions) \
+[4.1.3 Serialization Module](#architecture/serialization) \
+[4.1.3.1 Heatmap Serialization](#architecture/serialization/heatmap) \
+[4.1.4 Simulation Core Module](#architecture/simulation/core) \
+[4.1.4.1 Pedestrian](#architecture/simulation/core/pedestrian) \
+[4.1.4.2 Spawner](#architecture/simulation/core/spawner) \
+[4.1.4.3 Target](#architecture/simulation/core/target) \
+[4.1.4.4 Position](#architecture/simulation/core/position) \
+[4.1.4.5 Waypoint](#architecture/simulation/core/waypoint)
+
+[5. Testing](#section/testing) \
+[5.1 Testing Coverage](#testing/coverage)
+
+
+
Overview
+
+The main file of this project is run.py the first passed argument is the path to a json [
+`simulation configuration`](#section/configuration) file.
+
+The visualisation will start automatically with the simulation. See the [`manual`](#section/visualisation) for a basic
+overview of the visualisation.
+
+Getting Started
+
+Make sure that you are on python 3.13 and have installed the required dependencies found in `requirements.txt`.
+
+
+On Linux:
+```shell
+python -m venv ./.venv
+source ./.venv/bin/activate
+pip install -r requirements.txt
+```
+
+On Windows using powershell
+```powershell
+python --version # make sure python 3.13 is returned
+python -m venv ./.venv
+& ./.venv/Scripts/activate.ps1
+pip install -r requirements.txt
+```
+
+On Windows using batch
+```bat
+python --version # make sure python 3.13 is returned
+python -m venv ./.venv
+.\.venv\Scripts\activate.bat
+pip install -r requirements.txt
+```
+
+To run the simulation call `run.py` and pass the path to a simulation configuration file as the first argument.
+For example to run the `Chicken Test` :
+
+```shell
+python run.py ./simulation_config/chicken_test.json
+```
+
+ Configuration
+
+The configuration is split into the following sections
+
+| Field | Type | Required | Default | Description |
+|---------------------|--------------------------------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
+| `grid` | [`Grid`](#configuration/types/grid-type) | Yes | - | Configuration of the simulation grid |
+| `neighbourhood` | [`NeighbourhoodType`](#configuration/implementations/neighbourhood-type) | Yes | - | Which algorithm to use to get neighbouring cells |
+| `obstacles` | [`Obstacle[]`](#configuration/types/obstacle-type) | No | null | An array of obstacles |
+| `spawners` | [`Spawner[]`](#configuration/types/spawner-type) | Yes | - | An array of spawners |
+| `targets` | [`Target[]`](#configuration/types/target-type) | Yes | - | An array of target cells |
+| `social_distancing` | [`SocialDistancing`](#configuration/types/social-distancing-type) | Yes | - | Configuration of social distancing rules |
+| `distancing` | [`Distancing`](#configuration/types/distancing-type) | Yes | - | Configuration of social distancing rules |
+| `simulation` | [`Simulation`](#configuration/types/simulation-type) | Yes | - | Configuration of the simulation |
+| `log_file` | `string\|null` | No | null | The path to a file to log the simulation data step-wise. `{0}` will be formated with the current date. If set to `null` logging is disabled |
+
+Example Configuration:
+
+```json
+{
+ "grid": {
+ "width": 30,
+ "height": 10,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "neighbourhood": "MooreNeighbourhood",
+ "obstacles": [
+ {
+ "name": "obstacle1",
+ "rect": [0, 0, 2, 2]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "S_left",
+ "rect": [0, 0, 0, 9],
+ "targets": ["T_right"],
+ "total_spawns": 10,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0,
+ "targeting": "RANDOM"
+ }
+ ],
+ "targets": [
+ {
+ "name": "T_right",
+ "rect": [29, 0, 29, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": 1.0,
+ "waypoint_threshold": 1.0,
+ "waypoint_distance": 5,
+ "waypoint_heatmap_generator": "DijkstraHeatmapGenerator"
+ }
+}
+```
+
+Configuration Types
+
+Simulation Type
+
+| Field | Type | Required | Default | Description |
+|------------------------------|---------------------------------------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `time_resolution` | `float` | Yes | - | The minimum amount of time between simulation steps in seconds |
+| `occupation_bias_modifier` | `float\|null` | No | 1.0 | A factor which is multiplied with the occupation bias of a cell higher values mean the cell is less favourable as a next target. If set to `null` pedestrians cannot target cells which are `OCCUPIED` |
+| `retargeting_threshold` | `float\|null` | No | -1.0 | The threshold in virtual moved meters at which a pedestrian will retarget to another cell which is not occupied |
+| `waypoint_threshold` | `float\|null` | No | - | The threshold in virtual moved meters at which a pedestrian will retarget to a waypoint. Waypoints are a cheaper alternative to recalculating the target heatmap each simulation tick. Set to `null` to disable Waypoints |
+| `waypoint_distance` | `int` | No* | - | The depth for pathfinding to select a waypoint |
+| `waypoint_heatmap_generator` | [`HeatmapGeneratorType`](#configuration/implementations/heatmap-generator-type) | No* | - | The type of heatmap generator to use for pathfinding to a waypoint, cellstate will be set to `[OBSTACLE]` |
+
+*Only if `waypoint_threshold` is set
+
+Example Simulation Configuration:
+
+```json
+{
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": 1.0,
+ "waypoint_threshold": 1.0,
+ "waypoint_distance": 5,
+ "waypoint_heatmap_generator": "DijkstraHeatmapGenerator"
+}
+```
+
+Grid Type
+
+| Field | Type | Required | Default | Description |
+|-----------------|--------------------------------------------------------------------------|----------|---------|--------------------------------------------------|
+| `width` | `int` | Yes | - | The width of the simulation grid |
+| `height` | `int` | Yes | - | The height of the simulation grid |
+| `neighbourhood` | [`NeighbourhoodType`](#configuration/implementations/neighbourhood-type) | Yes | - | Which algorithm to use to get neighbouring cells |
+
+Target Type
+
+| Field | Type | Required | Default | Description |
+|---------------------|---------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `name` | `string` | Yes | - | A unique name of the target |
+| `rect` | `int[]` | No* | - | An array with exactly 4 entries `[x1, y1, x2, y2]` which spans an rectangle betwen `(x1, y1)` and `(x2, y2)` |
+| `cells` | [`Cell[]`](#configuration/types/cell-type) | No* | - | An array of cells that are part of the obstacle. Each cell is represented by a single integer. |
+| `cellstate` | [`CellState[]`](#configuration/enums/cell-state) | No | `[OBSTACLE]` | A list of [`CellState`](#configuration/enums/cell-state) which are considered blocked by the pathfinding algorithm. Pathfinding only gets updated each simulation step if any of the [`CellState`](#configuration/enums/cell-state) is not static |
+| `heatmap_generator` | [`HeatmapGeneratorType`](#configuration/implementations/heatmap-generator-type) | Yes | - | The type of heatmap generator to use for pathfinding for this target |
+
+*either `rect` or `cells` must be present
+
+Example Target Configuration with `rect`:
+
+```json
+{
+ "name": "T_right",
+ "rect": [29, 0, 29, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+}
+```
+
+Spawner Type
+
+| Field | Type | Required | Default | Description |
+|-----------------|--------------------------------------------------------|----------|----------|--------------------------------------------------------------------------------------------------------------|
+| `name` | `string` | Yes | - | A unique name of the spawner |
+| `rect` | `int[]` | No* | - | An array with exactly 4 entries `[x1, y1, x2, y2]` which spans an rectangle betwen `(x1, y1)` and `(x2, y2)` |
+| `cells` | [`Cell[]`](#configuration/types/cell-type) | No* | - | An array of cells that are part of the obstacle. Each cell is represented by a single integer. |
+| `targets` | `str[]` | Yes | - | An array of target names that the pedestrians spawned by this spawner can walk to |
+| `total_spawns` | `int\|null` | Yes | - | The total number of pedestrians to spawn. Use `null` for unlimited spawning |
+| `batch_size` | `int` | Yes | - | The maximum amount of pedestrians to spawn in each spawn attempt |
+| `spawn_delay` | `float` | Yes | - | The delay in seconds between each spawn attempt |
+| `initial_delay` | `float` | Yes | - | The delay in seconds before the first spawn attempt after the simulation started |
+| `targeting` | [`TargetingType`](#configuration/enums/targeting-type) | No | `RANDOM` | How to pick a target for a newly spawned pedestrian |
+
+*either `rect` or `cells` must be present
+
+Example Spawner Configuration with `rect`:
+
+```json
+{
+ "name": "S_left",
+ "rect": [0, 0, 0, 9],
+ "targets": ["T_right"],
+ "total_spawns": 10,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0,
+ "targeting": "RANDOM"
+}
+```
+
+Obstacle Type
+
+| Field | Type | Required | Default | Description |
+|---------|--------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------|
+| `name` | `string` | Yes | - | A unique name of the obstacle |
+| `rect` | `int[]` | No* | - | An array with exactly 4 entries `[x1, y1, x2, y2]` which spans an rectangle betwen `(x1, y1)` and `(x2, y2)` |
+| `cells` | [`Cell[]`](#configuration/types/cell-type) | No* | - | An array of cells that are part of the obstacle. Each cell is represented by a single integer. |
+
+*either `rect` or `cells` must be present
+
+Example Obstacle Configuration with `rect`:
+
+```json
+{
+ "name": "obstacle1",
+ "rect": [0, 0, 2, 2]
+}
+```
+
+Example Obstacle Configuration with `cells`:
+
+```json
+{
+ "name": "obstacle1",
+ "cells": [[0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2]]
+}
+```
+
+Social Distancing Type
+
+| Field | Type | Required | Default | Description |
+|----------|-------|----------|---------|------------------------------------------|
+| `width` | `int` | Yes | - | The width for social distancing formula |
+| `height` | `int` | Yes | - | The height for social distancing formula |
+
+Example Social Distancing Configuration:
+
+```json
+{
+ "width": 3,
+ "height": 3
+}
+```
+
+The social distancing formula is defined as follows:
+$$
+\begin{cases}
+\text{repulsion} = height * \exp{\frac{1}{(\text{distance}/{width})^2 - 1}} & \text{if |distance|} < width \\
+\text{repulsion} = 0 & \text{otherwise}
+\end{cases}
+$$
+
+Distancing Type
+
+| Field | Type | Required | Default | Description |
+|---------|------------------------------------------------------------------------------|----------|---------|----------------------------------------------|
+| `type` | [`DistancingType`](#configuration/implementations/distancing-algorithm-type) | Yes | - | The type of social distancing to apply |
+| `scale` | `float` | Yes | - | The distance in meters of two adjacent cells |
+
+Example Distancing Configuration:
+
+```json
+{
+ "type": "EuclideanDistance",
+ "scale": 1.0
+}
+```
+
+Cell Type
+A cell is an `int[]` with exactly 2 entries `[x, y]` which represent the zero based x and y coordinates of the cell on
+the grid. `[0, 0]` is the top left corner of the grid.
+
+| Index | Type | Required | Default | Description |
+|-------|-------|----------|---------|--------------------------------|
+| `[0]` | `int` | Yes | - | The `x` coordinate of the cell |
+| `[1]` | `int` | Yes | - | The `y` coordinate of the cell |
+
+Algorithm Implementations
+
+Different implementations of algorithms needed in the simulation can be selected by the following names:
+
+Heatmap Generator Type
+
+| Name | Description |
+|--------------------------------|----------------------------------------------------------|
+| `DijkstraHeatmapGenerator` | Use Dijkstra's algorithm to generate the heatmap. |
+| `FastMarchingHeatmapGenerator` | Use the Fast Marching algorithm to generate the heatmap. |
+
+Neighbourhood Type
+
+| Name | Description |
+|------------------------|-------------------------------------------------------|
+| `NeumannNeighbourhood` | The four cells directly adjacent to the current cell |
+| `MooreNeighbourhood` | The eight cells directly adjacent to the current cell |
+
+Distancing Algorithm Type
+
+| Name | Description |
+|---------------------|------------------------------------------|
+| `EuclideanDistance` | The Euclidean distance between two cells |
+| `TaxiDistance` | The Manhattan distance between two cells |
+
+### Configuration Enums
+
+Configuration Enums
+
+Different enums are used throughout the configuration to select from a predefined set of values.
+
+Cell State
+
+| Name | Is Static | Description |
+|------------|-----------|-------------------------------------------------|
+| `FREE` | Yes | The cell is free to walk on. |
+| `OBSTACLE` | Yes | The cell is blocked by an obstacle. |
+| `OCCUPIED` | No | The cell is currently occupied by a pedestrian. |
+
+TargetingStrategy Type
+
+| Name | Description |
+|------------|------------------------------------------------------------------------------------------------------------------------|
+| `RANDOM` | Pick a random target from the `targets` array |
+| `CLOSEST` | Pick the target that is closest to the spawner. If multiple targets are equally close, pick the first one in the list |
+| `FURTHEST` | Pick the target that is furthest from the spawner. If multiple targets are equally far, pick the first one in the list |
+
+ Visualisation
+
+The visualisation runs in realtime and consists of different toggable layers called [
+`features`](#visualisation/features). Additionally to the buttons on the top of the window, the following keyboard
+shortcuts are available as well:
+
+Visualisation Features
+
+| Name | Z-Index* | Description |
+|-----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `TargetHeatmap` | 0 | Renders the heatmap of the selected target and social distancing heatmap |
+| `Spawner` | 1 | Renders the spawners and their spawn cells |
+| `Target` | 2 | Renders the target cells and their target cells |
+| `Grid` | 3 | Renders the base grid, obstacles and grid lines |
+| `Path` | 4 | Renders the path of the selected pedestrian |
+| `Waypoint` | 5 | Renders all waypoints and a line connecting them to their associated pedestrian |
+| `Pedestrian` | 6 | Renders all pedestrians, their headed direction, their target cell and information. Inner color is the same as its spawners color and outline has the same color as it's target |
+| `Info` | 7 | Renders the current simulation time, step count, current fps and the amount of pedestrians in the grid |
+
+*Order of the rendering, lower layers are rendered first meaning they can get covered by higher layers
+
+Keyboard Shortcuts
+
+| Combination | Description |
+|---------------|------------------------------------------------------------------------------------------------------|
+| `h` | Toggle the heatmap visualisation feature |
+| `s` | Toggle the spawner visualisation feature |
+| `t` | Toggle the target visualisation feature |
+| `g` | Toggle the grid visualisation feature |
+| `p` | Toggle the pedestrian visualisation feature |
+| `i` | Toggle the info visualisation feature |
+| `d` | Toggle social distancing rendering in heatmap visualisation feature |
+| `r` | Toggle the path visualisation feature |
+| `l` | Toggle rendering of the grid lines in the grid visualisation feature |
+| `w` | Toggle rendering of the waypoints visualisation feature |
+| `n` | Toggle rendering of object names in all visualisation feature |
+| `p` + `SHIFT` | Toggle rendering of pedestrian detail text in pedestrian visualisation feature |
+| `p` + `CTRL` | Toggle rendering of line pointing to the pedestrians target cell in pedestrian visualisation feature |
+| `w` + `SHIFT` | Toggle rendering of line between pedestrian and it's waypoint in pedestrian visualisation feature |
+| `Space` | Toggle running of the simulation |
+| `Arrow Up` | Select previous pedestrian path in pedestrian visualisation feature |
+| `Arrow Down` | Select next pedestrian path in pedestrian visualisation feature |
+| `Arrow Left` | Select previous target in heatmap visualisation feature |
+| `Arrow Right` | Select next target in heatmap visualisation feature |
+
+Mouse Interaction
+
+| Action | Target | Description |
+|--------------|------------|----------------------------------------------------------------------------------|
+| `Left Click` | Pedestrian | Select the pedestrian and show it's path in the pedestrian visualisation feature |
+| `Left Click` | Target | Select the target and show it's heatmap in the heatmap visualisation feature |
+
+## Architecture
+
+The architecture is divided into different [`modules`](#architecture/modules), each module is responsible for a specific
+aspect of the
+simulation.
+
+Modules
+
+| Path | Description |
+|-----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
+| [`exceptions`](#architecture/exceptions) | Contains custom exceptions for the simulation |
+| [`serialization`](#architecture/serialization) | Contains the interfaces and logic for serializing and deserializing the states of simulation objects for logging |
+| [ `simulation/core`](#architecture/simulation/core) | Contains the core simulation logic, such as the simulation loop, pedestrian movement and pathfinding as well as the core components |
+| `simulation/heatmaps` | Contains the heatmap generation logic, such as Dijkstra's algorithm and the Fast Marching algorithm |
+| `simulation/heatmaps/distancing` | Contains different algorithms for distance calculation between two cells |
+| `simulation/neighbourhood` | Contains different algorithms for getting neighbouring cells |
+| `simulation_config` | Contains the logic for parsing the simulation configuration from a json file |
+| `visualisation` | Contains the logic for the visualisation of the simulation |
+| `visualisation/features` | Contains the different visualisation features and their logic |
+| `utils` | Contains utility functions used throughout the simulation |
+
+Overview over all classes and their inheritance
+
+
+
+Exceptions Module
+
+This module contains `SimulationError` and an enum of applicable error codes.
+
+| File | Description |
+|----------------------------|--------------------------------------------------------------|
+| `simulation_error.py` | Contains the `SimulationError` class |
+| `simulation_error_code.py` | Contains the `SimulationErrorCode` enum with all error codes |
+
+The `SimulationError` class is a custom exception class that takes an error code and a dictionary as context. The error
+message is derived from the error code and the context is used to enrich the error message with additional information.
+
+Serialization Module
+
+| File | Description |
+|-------------------|---------------------------------------------------------------------------------------------------|
+| `serializer.py` | Contains the logic for serializing and deserializing the states of simulation objects for logging |
+| `serializable.py` | Contains the abstract base class for objects that can be serialized and deserialized |
+
+Serialization is done by inheriting the `Serializable` base class and implementing the `get_serialization_data` and
+`get_identifier` methods. The `Serializer` class can then be used to serialize and deserialize the objects.
+`get_serialization_data` should return a dictionary with string key and value which is either a `primitive`, an `array`
+or a `Serializable` with the data that should be serialized and `get_identifier` should return a unique identifier for
+the object.
+This dictionary is then traversed recursively and all `Serializable` objects are replaced with the result of their
+`get_serialization_data` method.
+
+Heatmap Serialization
+
+Heatmaps are serialized as binary data and afterward encoded in base64 to save space in the log file.
+The 2D heatmap is indexed as a 1D array, the index of the 1D array is calculated as `y * width + x`.
+
+| Field | Offset | Size | Type | Endianess | Description |
+|--------|--------|---------------------------|-----------|-----------|---------------------------|
+| width | 0x00 | 0x04 | uint32 | little | The width of the heatmap |
+| height | 0x04 | 0x04 | uint32 | little | The height of the heatmap |
+| data | 0x08 | 0x08 * `width` * `height` | float64[] | little | The heatmap data |
+
+Simulation Core Module
+
+This module contains the core simulation logic, such as the simulation loop and core classes
+
+| File | Description |
+|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `cell.py` | Contains the `Cell` class which represents a cell on the grid |
+| `cell_state.py` | Contains the `CellState` enum which represents the state of a cell on the grid |
+| `grid_base.py` | Contains the `GridBase[T]` class which is an abstract base class for all 2D grids. It contains logic to index the internal 1D array and to check if something is in bounds |
+| [`pedestrian.py`](#architecture/simulation/core/pedestrian) | Contains the `Pedestrian` class which contains fields and logic to handle pedestrians |
+| `position.py` | Contains the abstract `Position` class which is inherited by anything which has a position on the simulation grid. Also offers methods to compare different implementations of `Position` only by `x` and `y` coordinate |
+| `simulation.py` | Contains the `Simulation` class which represents the complete simulation and core loop of the simulation |
+| `spawner.py` | Contains the `Spawner` class which handles the logic for [`Pedestrian`](#architecture/simulation/core/pedestrian) creation |
+| `target.py` | Contains the `Target` class which represents a target a [`Pedestrian`](#architecture/simulation/core/pedestrian) can walk to and exit the simulation. This class contains all cells which belong to the target and handles the generation of it's navigation heatmap |
+| `targeting_strategy` | Contains the [`TargetingStrategy`](#configuration/enums/targeting-type) enum |
+| `waypoint.py` | Contains the `Waypoint` class which represents a waypoint a [`Pedestrian`](#architecture/simulation/core/pedestrian) can walk to and exit the simulation. This class contains the cell where the waypoint is located and handles the generation of it's navigation heatmap |
+
+Pedestrian
+
+The [`Pedestrian`](#architecture/simulation/core/pedestrian) class handles the tracking of a single pedestrians.
+Pedestrians have an `update` method which should be called every simulation tick. The `can_move` method checks if the
+pedestrian can move to it's current target cell including bookkeeping of the moved distance to handle different walking
+speeds. Once a pedestrian `can_move` returns `True` the `move` method to update the pedestrians state, the cell has to
+be updated by the simulation itself since the pedestrian only modifies it's own state. Bookkeeping of movement speed
+works by calculating the distance to it's target cell and storing it in `_current_distance`. Every update the
+`time_delta` is multiplied by the pedestrians `_current_speed` and subtracted from `_current_distance`. Once
+`_current_distance` reaches 0 the pedestrian can move upon it's target cell if the cell is free. `_current_distance` can
+become negative to signal for how long a pedestrian has been stuck. Pedestrians are updated in order of their
+`_current_distance` where the ones with the lowest value get updated first
+
+Spawner
+
+The [`Spawner`](#architecture/simulation/core/spawner) class handles the spawning of pedestrians. The `update` method
+should be called every simulation tick. It yields a list of newly created pedestrians which have to be added to the
+simulation. It also chooses a target for the pedestrian and it's optimal walking speed by sampling from
+a [normal distribution](#architecture/utils/clipped_normal_distribution) with mean 1.34 m/s and standard deviation 0.26
+m/s. It's value is taken absolutely (to prevent negative speeds) and clipped to a range between 0.69 and 2.45 as
+suggested
+by [Similar Normal Distribution of Pedestrian Speeds at Signalized Intersection Crosswalks](https://ieeexplore.ieee.org/document/6977742).
+Spawning happens in batches where the size is the minimum of `batch_size` and the count of free spawn cells.
+
+Target
+
+The [`Target`](#architecture/simulation/core/target) class handles the generation of the navigation heatmap for the
+target. The heatmap is generated by the selected [
+`HeatmapGenerator`](#configuration/implementations/heatmap-generator-type) and is
+updated either at the beginning of the simulation or every simulation tick if it also considers [
+`OCCUPIED`](#configuration/enums/cell-state) as blocked. The heatmap is used by the pedestrians to find the shortest
+path to
+the target. The target also contains the cells and a method `is_inside_target` to check if a [
+`Position`](#architecture/simulation/core/position) is inside of one of the target's cells.
+
+Position
+
+The [`Position`](#architecture/simulation/core/position) class is an abstract base class which is inherited by anything
+which has a position on the simulation grid. It contains the `x` and `y` coordinates and a `pos_equals` method to
+compare different implementations of `Position` only by `x` and `y` coordinate.
+
+Waypoint
+
+The [`Waypoint`](#architecture/simulation/core/waypoint) class handles the generation of the navigation heatmap for the
+waypoint. The heatmap is generated by the selected [
+`HeatmapGenerator`](#configuration/implementations/heatmap-generator-type) and is
+updated either at the beginning of the simulation or every simulation tick if it also considers [
+`OCCUPIED`](#configuration/enums/cell-state) as blocked. The waypoint is used by the pedestrians to find the shortest
+path to
+the waypoint. The waypoint also contains the cell and a method `is_inside_waypoint` to check if a [
+`Position`](#architecture/simulation/core/position) is inside of the waypoint's cell. A waypoint is only created when
+the simulations `waypoint_threshold` is set and the pedestrian couldn't move for longer than the threshold. A [
+`Waypoint`](#architecture/simulation/core/waypoint) is a cheaper alternative to updating the navigation heatmaps of each
+target each ticks in respect of all pedestrians. It works by generating a Heatmap with djisktras algorithm with respect
+to all pedestrians. It then pathfinds for a given depth (`waypoint_distance`) starting at the pedestrians current
+location towards it's target. Once a pedestrian has a set [`Waypoint`](#architecture/simulation/core/waypoint) it will
+first walk to it's waypoint before continuing to it's original target. Waypoints mainly should prevent pedestrians
+getting stuck in narrow corridors.
+
+Testing
+
+The project is tested with `pytest`. The tests are located in the `tests` directory and can be run with the following
+Currently the tests only cover the core components of the simulations and parts of it's logic due to time constraints.
+
+```shell
+pytest tests
+```
+
+Testing Coverage
+
+All no mentioned modules aren't covered at all.
+
+| Module | File | Implemented |
+|-------------------------------------|------------------------------------------|-------------|
+| `simulation/core` | `cell.py` | Yes |
+| `simulation/core` | `cell_state.py` | Yes |
+| `simulation/core` | `grid_base.py` | Yes |
+| `simulation/core` | `pedestrian.py` | Yes |
+| `simulation/core` | `position.py` | Yes |
+| `simulation/core` | `simulation.py` | No |
+| `simulation/core` | `simulation_grid.py` | Yes |
+| `simulation/core` | `spawner.py` | Yes |
+| `simulation/core` | `target.py` | Yes |
+| `simulation/core` | `waypoint.py` | No |
+| `simulation/core` | `targeting_strategy.py` | No |
+| `simulation/heatmaps` | `dijkstra_heatmap_generator.py` | Yes |
+| `simulation/heatmaps` | `fast_marching_heatmap_generator.py` | Yes |
+| `simulation/heatmaps` | `social_distancing_heatmap_generator.py` | Yes |
+| `simulation/heatmaps` | `pathfinding_queue.py` | No |
+| `simulation/heatmaps` | `heatmap.py` | No |
+| `simulation/heatmaps` | `heatmap_generator_base.py` | No |
+| `simulation/heatmaps/neighbourhood` | `neumann_neighbourhood.py` | Yes |
+| `simulation/heatmaps/neighbourhood` | `moore_neighbourhood.py` | Yes |
+| `simulation/heatmaps/neighbourhood` | `base_neighbourhood.py` | No |
+| `simulation/heatmaps/distancing` | `euclidean_distance.py` | Yes |
+| `simulation/heatmaps/distancing` | `taxi_distance.py` | Yes |
+| `simulation/heatmaps/distancing` | `base_distance` | No |
diff --git a/src/__init__.py b/exceptions/__init__.py
similarity index 100%
rename from src/__init__.py
rename to exceptions/__init__.py
diff --git a/exceptions/simulation_error.py b/exceptions/simulation_error.py
new file mode 100644
index 0000000..b6dbdb3
--- /dev/null
+++ b/exceptions/simulation_error.py
@@ -0,0 +1,26 @@
+import json
+from typing import Dict, Any
+
+from exceptions.simulation_error_codes import SimulationErrorCode
+
+class SimulationError(Exception):
+ def __init__(self, error_code: SimulationErrorCode, context: Dict[str, Any] = None) -> None:
+ self._error_code: SimulationErrorCode = error_code
+ self._context: Dict[str, Any]|None = context
+
+ def get_message(self) -> str:
+ return self._error_code.value[1]
+
+ def get_code(self) -> str:
+ return self._error_code.value[0]
+
+ def __str__(self):
+ return repr(self)
+
+ def __repr__(self):
+ msg = f"[{self.get_code()}]: {self.get_message()}"
+ if self._context is not None:
+ context_msg = ", ".join([f"{key}={repr(value)}" for key, value in self._context.items()])
+ msg += f"\nContext {{{context_msg}}}"
+
+ return msg
\ No newline at end of file
diff --git a/exceptions/simulation_error_codes.py b/exceptions/simulation_error_codes.py
new file mode 100644
index 0000000..a7b6b22
--- /dev/null
+++ b/exceptions/simulation_error_codes.py
@@ -0,0 +1,20 @@
+from enum import Enum
+
+class ErrorCodeDataMixin:
+ error_code: int
+ message: str
+
+class SimulationErrorCode(ErrorCodeDataMixin, Enum):
+ NOT_IMPLEMENTED_IN_SIMULATION = 1, "A Function in the simulation is not implemented"
+ LENGTH_OF_GRID_INVALID = 2, "The length of the grid has to be more than 0 and of type int"
+ INVALID_COORDINATES = 3, "The accessed coordinates are outside of the grid"
+ PEDESTRIAN_HEAT_MAP_CALCULATION_FAILED = 4, "The calculation of the pedestrian heat map failed"
+ PEDESTRIAN_HEAT_MAP_NOT_FOUND = 5, "The Pedestrian Heat Map could not be found."
+ CELL_OCCUPIED = 6, "The cell is already occupied with another pedestrian"
+ CELL_BLOCKED = 7, "This cell is blocked by an obstacle and cannot be entered"
+ CELL_NOT_OCCUPIED = 8, "Tried to unoccupy a cell that is not occupied"
+ VALUE_NOT_INITIALIZED = 9, "The value has not been initialized yet, this mostly occurs when the first simulation step has not been executed"
+ ALREADY_IN_CELL = 10, "The pedestrian is already in the cell and can't target it"
+ CANNOT_MOVE = 11, "Move was called but the pedestrian can't move or has already reached it's target"
+ NO_FIXED_NEIGHBOURS = 12, "No fixed neighbours found for fast marching algorithm"
+ TELEPORTER_FULL = 13, "Trying to spawn a pedestrian on an full teleporter."
diff --git a/plotFlowChart.py b/plotFlowChart.py
new file mode 100644
index 0000000..8049048
--- /dev/null
+++ b/plotFlowChart.py
@@ -0,0 +1,30 @@
+import pandas as pd
+import matplotlib.pyplot as plt
+
+def main():
+ user_input = input("Enter the names of the CSV files separated by commas: ").split(',')
+ file_list = [file.strip() for file in user_input]
+ data = read_and_combine_csv(file_list)
+ plotData(data)
+
+def read_and_combine_csv(file_list):
+ dataframes = []
+ for file in file_list:
+ try:
+ df = pd.read_csv(file)
+ dataframes.append(df)
+ except FileNotFoundError:
+ print(f"Error: File '{file}' not found.")
+ return pd.concat(dataframes, ignore_index=True)
+
+def plotData(data):
+ plt.figure(figsize=(10, 6))
+ plt.plot(data['pedestrianDensity'], data['flowRate'], color='blue')
+ plt.title('Flow Rate')
+ plt.xlabel('Pedestrian Density')
+ plt.ylabel('Flow Rate')
+ plt.legend()
+ plt.show()
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 485eb9e..e11777a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,5 @@
+numpy
+scipy
pygame
-numpy
\ No newline at end of file
+pandas
+matplotlib
\ No newline at end of file
diff --git a/run.py b/run.py
index e20d950..3093b44 100644
--- a/run.py
+++ b/run.py
@@ -1,7 +1,109 @@
-from src.userSimulation import exampleSimulation
+import sys
+from typing import Generator
+
+from simulation.core.cell import Cell
+from simulation.core.teleporter import Teleporter
+from simulation.core.simulation import Simulation
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.core.spawner import Spawner
+from simulation.core.target import Target
+from simulation.core.targeting_stratey import TargetingStrategy
+from simulation.heatmaps.social_distancing_heatmap_generator import SocialDistancingHeatmapGenerator
+from visualisation.flow_meter import FlowMeter
+from visualisation.visualisation import Visualisation
+from simulation_config.config_loader import SimulationConfigLoader
+
+
+
+def get_cells(config, grid) -> Generator[Cell]:
+ if "cells" in config:
+ yield from [grid.get_cell(x, y) for (x, y) in config["cells"]]
+ elif "rect" in config:
+ x1, y1, x2, y2 = config["rect"]
+ for x in range(x1, x2 + 1):
+ for y in range(y1, y2 + 1):
+ yield grid.get_cell(x, y)
+
+def main(config_path):
+ simulation_config = SimulationConfigLoader.load_config(config_path)
+
+ neighbourhood_class = SimulationConfigLoader.get_neighbourhood_class(simulation_config["grid"]["neighbourhood"])
+ grid = SimulationGrid(simulation_config["grid"]["width"], simulation_config["grid"]["height"], neighbourhood_class)
+
+ if "obstacles" in simulation_config:
+ for obstacle in simulation_config["obstacles"]:
+ for cell in get_cells(obstacle, grid):
+ cell.set_osbtacle()
+
+ distance_class = SimulationConfigLoader.get_distance_class(simulation_config["distancing"]["type"])
+ distancing = distance_class(simulation_config["distancing"]["scale"])
+
+ target_mapping = {}
+ for target_config in simulation_config["targets"]:
+ target_cells = list(get_cells(target_config, grid))
+ blocked_states = SimulationConfigLoader.get_cell_states(target_config.get("cellstate", None))
+ heatmap_generator = SimulationConfigLoader.create_heatmap_generator(target_config["heatmap_generator"], distancing, blocked_states)
+ target_obj = Target(target_config["name"], target_cells, grid, heatmap_generator)
+ target_mapping[target_config["name"]] = target_obj
+
+
+ spawners = []
+ for spawner_config in simulation_config["spawners"]:
+ spawner_cells = list(get_cells(spawner_config, grid))
+ spawner_targets = [target_mapping[name] for name in spawner_config["targets"]]
+ targeting = TargetingStrategy[spawner_config.get("targeting", "RANDOM")]
+ spawners.append(Spawner(
+ spawner_config["name"], distancing, spawner_cells, spawner_targets,
+ spawner_config["total_spawns"], spawner_config["batch_size"],
+ spawner_config["spawn_delay"], spawner_config["initial_delay"],
+ targeting
+ ))
+
+ social_distancing = SocialDistancingHeatmapGenerator(
+ distancing,
+ simulation_config["social_distancing"]["width"],
+ simulation_config["social_distancing"]["height"]
+ )
+
+ teleporter = None
+ if "teleports" in simulation_config:
+ teleporter_cells = list(get_cells(simulation_config["teleports"], grid))
+ teleporter = Teleporter(teleporter_cells)
+
+ flow_meters = None
+
+ if "flow_meters" in simulation_config:
+ flow_meters = []
+ for flow_meter_config in simulation_config["flow_meters"]:
+ name = flow_meter_config["name"]
+ time_span = flow_meter_config.get("time_span")
+ logfile = flow_meter_config.get("logfile")
+ log_interval = flow_meter_config.get("log_interval")
+ cells = list(get_cells(flow_meter_config, grid))
+ flow_meters.append(FlowMeter(name, time_span, cells, logfile, log_interval))
+
+ sim = Simulation(
+ simulation_config["simulation"]["time_resolution"],
+ grid,
+ distancing,
+ social_distancing,
+ list(target_mapping.values()),
+ spawners,
+ simulation_config["simulation"].get("occupation_bias_modifier", 1.0),
+ simulation_config["simulation"].get("retargeting_threshold", -1.0),
+ simulation_config["simulation"].get("waypoint_threshold", None),
+ simulation_config["simulation"].get("waypoint_distance", None),
+ SimulationConfigLoader.create_heatmap_generator(simulation_config["simulation"].get("waypoint_heatmap_generator", None), distancing, None),
+ teleporter=teleporter,
+ flow_meter=flow_meters
+ )
+
+
+
+ log_file = simulation_config.get("log_file", None)
+ vis = Visualisation(sim, None, 30.0, log_file, flow_meters)
+ vis.run()
-def main():
- exampleSimulation.run()
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main(sys.argv[1])
\ No newline at end of file
diff --git a/src/exceptions/__init__.py b/serialization/__init__.py
similarity index 100%
rename from src/exceptions/__init__.py
rename to serialization/__init__.py
diff --git a/serialization/serializable.py b/serialization/serializable.py
new file mode 100644
index 0000000..9962d40
--- /dev/null
+++ b/serialization/serializable.py
@@ -0,0 +1,10 @@
+from abc import ABC, abstractmethod
+
+class Serializable(ABC):
+ @abstractmethod
+ def get_serialization_data(self) -> dict[str, any]:
+ pass
+
+ @abstractmethod
+ def get_identifier(self) -> str:
+ pass
\ No newline at end of file
diff --git a/serialization/serializer.py b/serialization/serializer.py
new file mode 100644
index 0000000..e1b8b8a
--- /dev/null
+++ b/serialization/serializer.py
@@ -0,0 +1,91 @@
+import json
+from asyncio import gather
+
+from serialization.serializable import Serializable
+from simulation.core.simulation import Simulation
+from utils import utils
+
+
+class Serializer:
+ def __init__(self, simulation: Simulation, file: str):
+ self._simulation = simulation
+ self._file = open(file, "w")
+ self._initialize()
+ self._is_first = True
+ pass
+
+ def _initialize(self):
+ grid = self.handle_serializable(self._simulation.get_grid())
+ grid["neighbourhood"] = self._simulation.get_grid()._neighbourhood.__class__.__name__
+
+ targets = [{
+ "id": target.get_identifier(),
+ "cells": [cell.get_identifier() for cell in target.get_cells()],
+ "heatmap": utils.heatmap_to_base64(target.get_heatmap()),
+ } for target in self._simulation.get_targets()]
+
+ spawners = [{
+ "id": spawner.get_identifier(),
+ "cells": [cell.get_identifier() for cell in spawner.get_cells()],
+ "spawn_delay": spawner._spawn_delay,
+ "initial_delay": spawner._current_delay,
+ "total_spawns": spawner._total_spawns,
+ "batch_size": spawner._batch_size,
+ "targeting_strategy": int(spawner._targeting_strategy),
+ "targets": [target.get_identifier() for target in spawner._targets],
+ } for spawner in self._simulation.get_spawners()]
+
+ distancing = {
+ "scale": self._simulation._distancing.get_scale(),
+ "type": self._simulation._distancing.__class__.__name__,
+ }
+
+ simulation = {
+ "time_resolution": self._simulation.get_time_resolution(),
+ "grid": grid,
+ "distancing": distancing,
+ "targets": targets,
+ "spawners": spawners,
+ "social_distancing": {
+ "width": self._simulation._social_distancing_generator._width,
+ "height": self._simulation._social_distancing_generator._height,
+ }
+ }
+
+
+ self._file.write(f'{{"setup": {json.dumps(simulation, indent=4)},\n"steps": [')
+
+ def write_current_state(self):
+ if self._file is None:
+ return
+
+ if self._is_first:
+ self._is_first = False
+ else:
+ self._file.write(",\n")
+
+ self._file.write(json.dumps(self.handle_serializable(self._simulation)))
+ if self._simulation.is_done():
+ self.close()
+
+ def close(self):
+ if self._file is not None:
+ self._file.write("]}")
+ self._file.close()
+ self._file = None
+
+ def handle_dict(self, data: dict[str, any]) -> dict[str, any]:
+ for key, value in data.items():
+ if isinstance(value, Serializable):
+ data[key] = self.handle_serializable(value)
+ elif isinstance(value, list):
+ data[key] = [self.handle_serializable(item) if isinstance(item, Serializable) else item for item in value]
+
+ return data
+
+ def handle_serializable(self, serializable: Serializable) -> dict[str, any]:
+ data = serializable.get_serialization_data()
+ return self.handle_dict(data)
+
+ def serialize (self, data: Serializable) -> str:
+ return json.dumps(self.handle_serializable(data), indent=4)
\ No newline at end of file
diff --git a/tests/config/__init__.py b/simulation/__init__.py
similarity index 100%
rename from tests/config/__init__.py
rename to simulation/__init__.py
diff --git a/tests/exceptions/__init__.py b/simulation/core/__init__.py
similarity index 100%
rename from tests/exceptions/__init__.py
rename to simulation/core/__init__.py
diff --git a/simulation/core/cell.py b/simulation/core/cell.py
new file mode 100644
index 0000000..fcc55a7
--- /dev/null
+++ b/simulation/core/cell.py
@@ -0,0 +1,69 @@
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from serialization.serializable import Serializable
+from simulation.core.cell_state import CellState
+from simulation.core.position import Position
+
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from simulation.core.pedestrian import Pedestrian
+
+class Cell(Position, Serializable):
+ def __init__(self, x: int, y: int, initial_state: CellState = CellState.FREE):
+ super().__init__(x, y)
+ self._state: CellState = initial_state
+ self._pedestrian: 'Pedestrian' | None = None
+
+ def __eq__(self, other):
+ return self._x == other._x and self._y == other._y
+
+ def __hash__(self):
+ return hash((self._x, self._y))
+
+ def get_state(self) -> CellState:
+ return self._state
+
+ def set_osbtacle(self) -> None:
+ self._state = CellState.OBSTACLE
+
+ def set_pedestrian(self, pedestrian: 'Pedestrian') -> None:
+ if self._state == CellState.OBSTACLE:
+ raise SimulationError(SimulationErrorCode.CELL_BLOCKED)
+
+ if self._state == CellState.OCCUPIED:
+ raise SimulationError(SimulationErrorCode.CELL_OCCUPIED, {"pedestrian": pedestrian})
+
+ self._pedestrian = pedestrian
+ self._state = CellState.OCCUPIED
+
+ def remove_pedestrian(self) -> None:
+ if self._state != CellState.OCCUPIED:
+ raise SimulationError(SimulationErrorCode.CELL_NOT_OCCUPIED, {"x": self._x, "y": self._y})
+
+ self._pedestrian = None
+ self._state = CellState.FREE
+
+ def is_free(self):
+ return self._state == CellState.FREE
+
+ def get_pedestrian(self) -> 'Pedestrian':
+ return self._pedestrian
+
+ def is_occupied(self):
+ return self._state == CellState.OCCUPIED
+
+ def get_identifier(self) -> str:
+ return f"{self._x}_{self._y}"
+
+ def get_serialization_data(self) -> dict[str, any]:
+ data = {
+ "id": self.get_identifier(),
+ "state": int(self._state),
+ "x": self._x,
+ "y": self._y
+ }
+
+ if self._pedestrian is not None:
+ data["pedestrian"] = self._pedestrian.get_identifier()
+
+ return data
\ No newline at end of file
diff --git a/simulation/core/cell_state.py b/simulation/core/cell_state.py
new file mode 100644
index 0000000..aad70bf
--- /dev/null
+++ b/simulation/core/cell_state.py
@@ -0,0 +1,7 @@
+from enum import Enum, IntEnum
+
+
+class CellState(IntEnum):
+ FREE = 0,
+ OCCUPIED = 1,
+ OBSTACLE = 2
\ No newline at end of file
diff --git a/simulation/core/grid_base.py b/simulation/core/grid_base.py
new file mode 100644
index 0000000..aa4e894
--- /dev/null
+++ b/simulation/core/grid_base.py
@@ -0,0 +1,65 @@
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import Sequence, Generator
+
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.position import Position
+from utils.immutable_list import ImmutableList
+
+
+class GridBase[T](ABC):
+ def __init__(self, width: int, height: int):
+ self._width: int = width
+ self._height: int = height
+ self.cells: list[T] = [self._init_cell(*self._get_cell_coordinate(i)) for i in range(width * height)]
+
+ @abstractmethod
+ def _init_cell(self, x: int, y: int) -> T:
+ pass
+
+ def get_width(self) -> int:
+ return self._width
+
+ def get_height(self) -> int:
+ return self._height
+
+ def get_cells(self) -> ImmutableList[T]:
+ return ImmutableList(self.cells)
+
+ def _get_cell_index(self, x: int, y: int) -> int:
+ return y * self._width + x
+
+ def _get_cell_coordinate(self, index: int) -> tuple[int, int]:
+ return index % self._width, index // self._width
+
+
+ def is_in_bounds(self, x: int, y: int) -> bool:
+ return 0 <= x < self._width and 0 <= y < self._height
+
+
+ def check_bounds(self, x: int, y: int) -> None:
+ """
+ Raises a SimulationError if the given coordinates are out of bounds.
+ """
+ if not self.is_in_bounds(x, y):
+ raise SimulationError(SimulationErrorCode.INVALID_COORDINATES, {"x": x, "y": y})
+
+ def get_cell(self, x: int, y: int) -> T:
+ self.check_bounds(x, y)
+ return self.cells[self._get_cell_index(x, y)]
+
+ def get_cell_at_pos(self, pos: Position):
+ return self.get_cell(pos.get_x(), pos.get_y())
+
+ def __contains__(self, item):
+ if isinstance(item, Cell):
+ return item in self.cells
+ elif isinstance(item, Position):
+ return self.is_in_bounds(item.get_x(), item.get_y())
+ elif isinstance(item, tuple) and len(item) == 2:
+ return self.is_in_bounds(*item)
+ else:
+ return False
\ No newline at end of file
diff --git a/simulation/core/pedestrian.py b/simulation/core/pedestrian.py
new file mode 100644
index 0000000..458581e
--- /dev/null
+++ b/simulation/core/pedestrian.py
@@ -0,0 +1,164 @@
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from serialization.serializable import Serializable
+from simulation.core.cell_state import CellState
+from simulation.core.position import Position
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from typing import TYPE_CHECKING
+
+from simulation.core.waypoint import Waypoint
+
+if TYPE_CHECKING:
+ from simulation.core.cell import Cell
+ from simulation.core.target import Target
+ from simulation.core.spawner import Spawner
+ from simulation.heatmaps.heatmap import Heatmap
+ from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+class Pedestrian(Position, Serializable):
+ NEGATIVE_INFINITY = float('-inf')
+ INFINITY = float('inf')
+ ID_COUNTER = 0
+
+ @staticmethod
+ def get_next_id() -> int:
+ Pedestrian.ID_COUNTER += 1
+ return Pedestrian.ID_COUNTER
+
+ def __init__(self, x: int, y: int, speed: float, spawner: 'Spawner', target: 'Target', distancing: 'DistanceBase', time_alive: float = 0):
+ super().__init__(x, y)
+ self._id: int = Pedestrian.get_next_id()
+ self._optimal_speed: float = speed
+ self._current_speed: float = speed
+ self._target: 'Target' = target
+ self._spawner: 'Spawner' = spawner
+ self._current_distance: float = Pedestrian.INFINITY
+ self._distance_to_target: float = Pedestrian.INFINITY
+ self._distancing: 'DistanceBase' = distancing
+ self._target_cell: 'Cell' | None = None
+ self._time_alive: float = time_alive
+ self._total_distance_moved: float = 0
+ self._refund_distance_flag = False
+ self._reached_target = False
+ self._waypoint: Waypoint|None = None
+
+ def set_waypoint(self, cell: Waypoint) -> None:
+ self._waypoint = cell
+
+ def get_waypoint(self) -> Waypoint:
+ return self._waypoint
+
+ def clear_waypoint(self) -> None:
+ self._waypoint = None
+
+ def has_reached_waypoint(self) -> bool:
+ return self._waypoint is not None and self._waypoint.get_cell().pos_equals(self)
+
+ def has_waypoint(self) -> bool:
+ return self._waypoint is not None
+
+ def set_reached_target(self) -> None:
+ self._reached_target = True
+
+ def has_reached_target(self) -> bool:
+ return self._reached_target
+
+ def set_target_cell(self, target_cell: 'Cell') -> None:
+ if target_cell is None:
+ self._target_cell = None
+ self._current_distance = Pedestrian.NEGATIVE_INFINITY
+ else:
+ if target_cell.pos_equals(self):
+ raise SimulationError(SimulationErrorCode.ALREADY_IN_CELL, {"cell": target_cell})
+
+ if self._refund_distance_flag and self._current_distance is not Pedestrian.NEGATIVE_INFINITY and self._current_distance is not Pedestrian.INFINITY:
+ self._current_distance += self._distancing.calculate_distance(self, target_cell)
+ else:
+ self._current_distance = self._distancing.calculate_distance(self, target_cell)
+
+ self._distance_to_target = self._current_distance
+ self._target_cell = target_cell
+ self._refund_distance_flag = True
+
+ def get_spawner(self) -> 'Spawner':
+ return self._spawner
+
+ def get_id(self) -> int:
+ return self._id
+
+ def is_inside_target(self) -> bool:
+ return self._target.is_inside_target(self)
+
+ def get_targeted_cell(self) -> 'Cell':
+ return self._target_cell
+
+ def can_move(self) -> bool:
+ return self._current_distance < 0 and self.has_targeted_cell() and self._target_cell.is_free() and not self.has_reached_target()
+
+ def move(self) -> None:
+ if not self.can_move() or self.has_reached_target():
+ raise SimulationError(SimulationErrorCode.CANNOT_MOVE)
+
+ # pedestrians shouldn't manipulate the themselves
+ # self._target_cell.set_pedestrian(self)
+ self._x = self._target_cell.get_x()
+ self._y = self._target_cell.get_y()
+ self._total_distance_moved += self._distance_to_target
+ # self.set_target_cell(None)
+ self._distance_to_target = Pedestrian.INFINITY
+
+ def get_occupation_bias(self) -> float:
+ if self._target_cell is None:
+ return 1
+ else:
+ return 1.0 - (max(0.0, self._current_distance)/self._distance_to_target)
+
+ def get_average_speed(self) -> float:
+ return self._total_distance_moved / self._time_alive
+
+ def update(self, delta: float):
+ self._time_alive += delta
+ if self._current_distance < 0:
+ self._refund_distance_flag = False
+
+ self._current_distance -= self._current_speed * delta
+
+ def get_current_distance(self) -> float:
+ return self._current_distance
+
+ def get_optimal_speed(self) -> float:
+ return self._optimal_speed
+
+ def get_target(self) -> 'Target':
+ return self._target
+
+ def get_heatmap(self) -> 'Heatmap':
+ return self._target.get_heatmap()
+
+ def has_targeted_cell(self):
+ return self._target_cell is not None
+
+ def get_serialization_data(self) -> dict[str, any]:
+ return {
+ "id": self._id,
+ "average_speed": self.get_average_speed(),
+ "current_distance": self._current_distance,
+ "time_alive": self._time_alive,
+ "total_distance_moved": self._total_distance_moved,
+ "optimal_speed": self._optimal_speed,
+ "target": self._target.get_identifier(),
+ "target_cell": self._target_cell.get_identifier() if self._target_cell is not None else None,
+ "spawner": self._spawner.get_identifier(),
+ "cell_id": f"{self._x}_{self._y}",
+ "x": self._x,
+ "y": self._y,
+ }
+
+ def get_identifier(self) -> str:
+ return str(self._id)
+
+ def get_distancing(self) -> DistanceBase:
+ return self._distancing
+
+ def get_time_alive(self) -> float:
+ return self._time_alive
\ No newline at end of file
diff --git a/simulation/core/position.py b/simulation/core/position.py
new file mode 100644
index 0000000..501d590
--- /dev/null
+++ b/simulation/core/position.py
@@ -0,0 +1,33 @@
+from abc import ABC
+
+
+class Position(ABC):
+ def __init__(self, x: int, y: int):
+ self._x: int = x
+ self._y: int = y
+
+ def get_x(self) -> int:
+ return self._x
+
+ def get_y(self) -> int:
+ return self._y
+
+ def as_tuple(self) -> tuple[int, int]:
+ return self._x, self._y
+
+ def pos_equals(self, other: object) -> bool:
+ return isinstance(other, Position) and self.get_x() == other.get_x() and self.get_y() == other.get_y()
+
+ def __ne__(self, other: object) -> bool:
+ return not self.__eq__(other)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Position):
+ return False
+ return self._x == other.get_x() and self._y == other.get_y()
+
+ def __hash__(self) -> int:
+ return hash((self._x, self._y))
+
+ def __str__(self) -> str:
+ return f"({self._x}, {self._y})"
\ No newline at end of file
diff --git a/simulation/core/simulation.py b/simulation/core/simulation.py
new file mode 100644
index 0000000..1a62f58
--- /dev/null
+++ b/simulation/core/simulation.py
@@ -0,0 +1,299 @@
+from typing import Iterable
+
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from simulation.core.cell_state import CellState
+from simulation.heatmaps.djisktra_heatmap_generator import DijkstraHeatmapGenerator
+from utils.utils import none_check
+
+from typing import Iterable
+
+import utils.utils
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from serialization.serializable import Serializable
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.position import Position
+from simulation.core.spawner import Spawner
+from simulation.core.teleporter import Teleporter
+from simulation.core.target import Target
+from simulation.core.waypoint import Waypoint
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.heatmaps.djisktra_heatmap_generator import DijkstraHeatmapGenerator
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+from simulation.heatmaps.pathfinding_queue import PathfindingQueue
+from simulation.heatmaps.social_distancing_heatmap_generator import SocialDistancingHeatmapGenerator
+from simulation.neighbourhood.base_neighbourhood import NeighbourhoodBase
+from simulation.core.pedestrian import Pedestrian
+from simulation.core.simulation_grid import SimulationGrid
+from utils.immutable_list import ImmutableList
+from utils.utils import none_check
+from visualisation.flow_meter import FlowMeter
+
+class Simulation(Serializable):
+ def __init__(self, time_resolution: float, grid: SimulationGrid, distancing: DistanceBase,
+ social_distancing: SocialDistancingHeatmapGenerator, targets: list[Target], spawners: list[Spawner],
+ occupation_bias_modifier: float | None = 1.0, retargeting_threshold: float | None = -1.0, waypoint_threshold: float | None = None, waypoint_distance: int | None = None, waypoint_heatmap_generator: HeatmapGeneratorBase | None = None, teleporter: Teleporter | None = None, flow_meter: list[FlowMeter] | None = None):
+ self._pedestrians: list[Pedestrian] = list()
+ self._grid: SimulationGrid = grid
+ self._targets: list[Target] = targets
+ self._spawners: list[Spawner] = spawners
+ self._social_distancing_generator: SocialDistancingHeatmapGenerator = social_distancing
+ self._distancing_heatmap: Heatmap = None
+ self._distancing = distancing
+ self._steps: int = 0
+ self._run_time: float = 0
+ self._time_resolution: float = time_resolution
+ self._occupation_bias_modifier: float | None = occupation_bias_modifier
+ self._retargeting_threshold: float | None = retargeting_threshold
+ self._waypoint_threshold: float | None = waypoint_threshold
+ self._waypoint_distance: int | None = waypoint_distance
+ self._waypoint_heatmap_generator: HeatmapGeneratorBase | None = waypoint_heatmap_generator
+ self._waypoints: list[Waypoint] = []
+ self._waypoint_pathfinding_heatmap_generator: DijkstraHeatmapGenerator = DijkstraHeatmapGenerator(distancing, {CellState.OBSTACLE, CellState.OCCUPIED})
+ self._waypoint_heatmap_cache: dict[Target, Heatmap] = {}
+ self._teleporter = teleporter
+ self._number_of_movable_cells = self.get_number_of_movable_cells()
+ self._flow_meters: list[FlowMeter] = flow_meter
+ self._init_flow_meter_logs()
+
+ waypoint_none, none_fields = none_check(waypoint_threshold=waypoint_threshold, waypoint_distance=waypoint_distance, waypoint_heatmap_generator=waypoint_heatmap_generator)
+ if waypoint_none is False:
+ raise SimulationError(SimulationErrorCode.VALUE_NOT_INITIALIZED, {"parameters": none_fields})
+
+ def get_number_of_movable_cells(self) -> int:
+ number_of_movable_cells = 0
+ for cell in self._grid.cells:
+ if not cell.get_state() == CellState.OBSTACLE:
+ number_of_movable_cells += 1
+ return number_of_movable_cells
+
+ def get_waypoints(self) -> ImmutableList[Waypoint]:
+ return ImmutableList(self._waypoints)
+
+ def get_time_resolution(self) -> float:
+ return self._time_resolution
+
+ def get_spawners(self) -> ImmutableList[Spawner]:
+ return ImmutableList(self._spawners)
+
+ def get_targets(self) -> ImmutableList[Target]:
+ return ImmutableList(self._targets)
+
+ def get_grid(self) -> SimulationGrid:
+ return self._grid
+
+ def get_max_grid_distance(self):
+ max_point = Position(self.get_grid().get_width(), self.get_grid().get_height())
+ min_point = Position(0, 0)
+ return self._distancing.calculate_distance(min_point, max_point)
+
+ def get_distancing_heatmap(self) -> Heatmap:
+ return self._distancing_heatmap
+
+ def get_target(self, x: int, y: int) -> Target | None:
+ for target in self._targets:
+ if target.is_coordinate_inside_target(x, y):
+ return target
+ return None
+
+ def get_pedestrians(self) -> ImmutableList[Pedestrian]:
+ return ImmutableList(self._pedestrians)
+
+ def get_steps(self) -> int:
+ return self._steps
+
+ def get_run_time(self) -> float:
+ return self._run_time
+
+ def is_done(self) -> bool:
+ return all(spawner.is_done() for spawner in self._spawners) and len(self._pedestrians) == 0
+
+ def update(self, delta: float = None):
+ delta = delta or self._time_resolution
+ self._update_spawners(delta)
+ self._update_distancing_heatmap()
+ self._update_targets()
+ self._update_waypoints()
+ self._update_pedestrians(delta)
+ self._update_flow_meters(delta)
+ self._steps += 1
+ self._run_time += delta
+
+ def _init_flow_meter_logs(self):
+ if self._flow_meters == None:
+ return
+ for flow_meter in self._flow_meters:
+ flow_meter.create_log()
+
+ def _remove_waypoint(self, waypoint: Waypoint):
+ waypoint.get_pedestrian().clear_waypoint()
+ self._waypoints.remove(waypoint)
+
+ def _create_waypoint(self, cell: Cell, pedestrian: Pedestrian):
+ waypoint = Waypoint(self._waypoint_heatmap_generator, self._grid, cell, pedestrian)
+ self._waypoints.append(waypoint)
+ pedestrian.set_waypoint(waypoint)
+
+ def _remove_pedestrian(self, pedestrian: Pedestrian):
+ if not self._teleporter:
+ self._pedestrians.remove(pedestrian)
+ cell = self._grid.get_cell_at_pos(pedestrian)
+ cell.remove_pedestrian()
+ pedestrian.set_reached_target()
+ if self._teleporter and self._teleporter.has_free_cell():
+ self._respawn_pedestrian_in_cell(pedestrian)
+ self._pedestrians.remove(pedestrian)
+ cell = self._grid.get_cell_at_pos(pedestrian)
+ cell.remove_pedestrian()
+ pedestrian.set_reached_target()
+
+ def _respawn_pedestrian_in_cell(self, pedestrian: Pedestrian) -> None:
+ speed = pedestrian.get_optimal_speed()
+ spawner = pedestrian.get_spawner()
+ target = pedestrian.get_target()
+ distancing = pedestrian.get_distancing()
+ time_alive = pedestrian.get_time_alive()
+ self._add_pedestrian(self._teleporter.spawn(speed, spawner, target, distancing, time_alive))
+
+ def _add_pedestrian(self, pedestrian: Pedestrian):
+ cell = self._grid.get_cell_at_pos(pedestrian)
+ cell.set_pedestrian(pedestrian)
+ self._pedestrians.append(pedestrian)
+
+ def _update_spawners(self, delta: float):
+ for spawner in self._spawners:
+ for pedestrian in spawner.update(delta):
+ self._add_pedestrian(pedestrian)
+
+ def _update_distancing_heatmap(self):
+ pedestrian_cells = [self._grid.get_cell_at_pos(pedestrian) for pedestrian in self._pedestrians]
+ self._distancing_heatmap = self._social_distancing_generator.generate_heatmap(pedestrian_cells,
+ self._grid)
+
+ def _update_targets(self):
+ for target in self._targets:
+ target.update_heatmap()
+
+ def _get_cell_value(self, pos: Position, last_pos: Position, cell: Cell, heatmap: Heatmap) -> float:
+ value = heatmap.get_cell_at_pos(cell)
+ value += min(0, self._distancing_heatmap.get_cell_at_pos(cell) - self._social_distancing_generator.get_bias(last_pos, cell))
+ if self._occupation_bias_modifier is not None:
+ value += (self._occupation_bias_modifier * cell.get_pedestrian().get_occupation_bias()) if cell.is_occupied() else 0
+
+ value += self._distancing.calculate_distance(pos, cell)
+ return value
+
+ def _get_next_target_cell(self, heatmap: Heatmap, pos: Position, last_pos: Position) -> Cell | None:
+ neighbours = self._grid.get_neighbours_at(pos)
+ neigbour_values = [(cell, self._get_cell_value(pos, last_pos, cell, heatmap)) for cell in neighbours]
+ sorted_neighbours = sorted(neigbour_values, key=lambda n: n[1]);
+ for cell, value in sorted_neighbours:
+ if (self._occupation_bias_modifier is not None or cell.is_free()) and value != Heatmap.INFINITY:
+ return cell
+
+ return None
+
+ def _get_next_pedestrian_target(self, pedestrian: Pedestrian, last_pos: Position) -> Cell | None:
+ if pedestrian.is_inside_target():
+ self._remove_pedestrian(pedestrian)
+ return None
+ elif pedestrian.has_waypoint():
+ return pedestrian.get_waypoint().next_cell(pedestrian)
+ else:
+ return self._get_next_target_cell(pedestrian.get_target().get_heatmap(), pedestrian, last_pos)
+
+
+ def _get_waypoint_heatmap(self, target: Target) -> Heatmap:
+ if target not in self._waypoint_heatmap_cache:
+ self._waypoint_heatmap_cache[target] = self._waypoint_heatmap_generator.generate_heatmap(target.get_cells(), self._grid)
+
+ return self._waypoint_heatmap_cache[target]
+
+ def _find_waypoint(self, cell: Cell, target: Target, depth: int = 10) -> Cell | None:
+ heatmap = self._get_waypoint_heatmap(target)
+ queue = PathfindingQueue()
+ queue.push(cell, heatmap.get_cell_at_pos(cell))
+ current: Cell = cell
+ while depth > 0 and not queue.is_empty():
+ current = queue.pop()
+ for neighbour in self._grid.get_neighbours_at(current):
+ if neighbour in queue:
+ continue
+
+ if neighbour.is_free():
+ queue.push(neighbour, heatmap.get_cell_at_pos(neighbour))
+ else:
+ queue.mark_visited(neighbour)
+
+ depth -= 1
+
+ return None if cell.pos_equals(current) else (current if queue.is_empty() else queue.pop())
+
+ def _update_pedestrians(self, delta: float):
+ for pedestrian in sorted(self._pedestrians, key=lambda p: p.get_current_distance()):
+ pedestrian.update(delta)
+
+ if pedestrian.has_reached_waypoint():
+ self._remove_waypoint(pedestrian.get_waypoint())
+
+ if pedestrian.can_move():
+ cell = self._grid.get_cell_at_pos(pedestrian)
+ cell.remove_pedestrian()
+ pedestrian.move()
+ pedestrian.get_targeted_cell().set_pedestrian(pedestrian)
+ new_target_cell = self._get_next_pedestrian_target(pedestrian, cell)
+ pedestrian.set_target_cell(new_target_cell)
+
+ elif pedestrian.has_targeted_cell() is False:
+ new_target_cell = self._get_next_pedestrian_target(pedestrian, pedestrian)
+ pedestrian.set_target_cell(new_target_cell)
+ elif (pedestrian.get_targeted_cell().is_occupied() and
+ self._retargeting_threshold is not None and
+ self._retargeting_threshold > pedestrian.get_current_distance() and
+ (new_target_cell := self._get_next_pedestrian_target(pedestrian, pedestrian)) and
+ new_target_cell is not None and
+ new_target_cell.is_free()):
+
+ pedestrian.set_target_cell(new_target_cell)
+ elif (pedestrian.has_waypoint() is False and
+ self._waypoint_threshold is not None and
+ self._waypoint_threshold > pedestrian.get_current_distance() and
+ (waypoint_target := self._find_waypoint(self._grid.get_cell_at_pos(pedestrian), pedestrian.get_target(), self._waypoint_distance)) is not None):
+
+ self._create_waypoint(waypoint_target, pedestrian)
+ else:
+ pass
+ # pedestrian is stuck, do nothing
+
+
+ def _update_flow_meters(self, delta: float) -> None:
+ if self._flow_meters == None:
+ return
+ pedestrian_densety = self._get_pedestrian_density()
+ for flow_meter in self._flow_meters:
+ flow_meter.log(delta, self._run_time, pedestrian_densety)
+
+ def _get_pedestrian_density(self) -> float:
+ return len(self._pedestrians) / self._number_of_movable_cells
+
+ def _update_waypoints(self):
+ self._waypoint_heatmap_cache.clear()
+ for waypoint in self._waypoints:
+ waypoint.update()
+
+ def get_serialization_data(self) -> dict[str, any]:
+ return {
+ "run_time": self._run_time,
+ "step": self._steps,
+ "pedestrians": self._pedestrians,
+ "targets": self._targets,
+ "spawners": self._spawners,
+ "waypoints": self._waypoints,
+ "social_distancing_heatmap": utils.utils.heatmap_to_base64(self._distancing_heatmap),
+ }
+
+ def get_identifier(self) -> str:
+ return "simulation"
\ No newline at end of file
diff --git a/simulation/core/simulation_grid.py b/simulation/core/simulation_grid.py
new file mode 100644
index 0000000..7fe07b8
--- /dev/null
+++ b/simulation/core/simulation_grid.py
@@ -0,0 +1,37 @@
+from typing import Type, Generator
+
+from scipy.stats import wasserstein_distance
+
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from serialization.serializable import Serializable
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.grid_base import GridBase
+from simulation.core.position import Position
+from simulation.neighbourhood.base_neighbourhood import NeighbourhoodBase
+
+
+class SimulationGrid(GridBase[Cell], Serializable):
+ def __init__(self, width: int, height: int, neighbourhood: Type[NeighbourhoodBase]):
+ super().__init__(width, height)
+ self._neighbourhood: NeighbourhoodBase = neighbourhood(width, height)
+
+ def _init_cell(self, x: int, y: int) -> Cell:
+ return Cell(x, y)
+
+ def get_neighbours(self, x: int, y: int, width: int = 1, height: int = 1) -> Generator[Cell]:
+ for x, y in self._neighbourhood.get_neighbours(x, y, width, height):
+ yield self.get_cell(x, y)
+
+ def get_neighbours_at(self, pos: Position, width: int = 1, height: int = 1) -> Generator[Cell]:
+ return self.get_neighbours(pos.get_x(), pos.get_y(), width, height)
+
+ def get_serialization_data(self) -> dict[str, any]:
+ return {
+ "cells": self.cells
+ }
+
+ def get_identifier(self) -> str:
+ return "grid"
+
diff --git a/simulation/core/spawner.py b/simulation/core/spawner.py
new file mode 100644
index 0000000..abe4094
--- /dev/null
+++ b/simulation/core/spawner.py
@@ -0,0 +1,104 @@
+import random
+from typing import Generator
+
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from serialization.serializable import Serializable
+from simulation.core.pedestrian import Pedestrian
+from simulation.core.targeting_stratey import TargetingStrategy
+
+from utils.clipped_normal_distribution import ClippedNormalDistribution
+
+from typing import TYPE_CHECKING
+
+from utils.immutable_list import ImmutableList
+
+if TYPE_CHECKING:
+ from simulation.core.cell import Cell
+ from simulation.core.target import Target
+ from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+
+class Spawner(Serializable):
+ # Chosen by values from this paper: https://ieeexplore.ieee.org/document/6977742
+ SPEED_DISTRIBUTION = ClippedNormalDistribution(1.34, 0.26, 0.69, 2.45)
+
+ def __init__(self, name: str, distancing: 'DistanceBase', cells: list['Cell'], targets: list['Target'], total_spawns: int | None, batch_size: int | None, spawn_delay: float, initial_delay: float, targeting_strategy: TargetingStrategy = TargetingStrategy.RANDOM):
+ self._name: str = name
+ self._cells: list['Cell'] = cells
+ self._targets: list['Target'] = targets
+ self._total_spawns: int | None = total_spawns
+ self._batch_size: int | None = None if batch_size is None else min(batch_size, len(cells)) # can't spawn more pedestrians than there are cells in one batch
+ self._spawn_delay: float = spawn_delay
+ self._current_delay: float = initial_delay
+ self._distancing: 'DistanceBase' = distancing
+ self._targeting_strategy: TargetingStrategy = targeting_strategy
+ self._spawn_count: int = 0
+
+ def get_name(self) -> str:
+ return self._name
+
+ def get_spawn_count(self) -> int:
+ return self._spawn_count
+
+ def get_cells(self) -> ImmutableList['Cell']:
+ return ImmutableList(self._cells)
+
+ def can_spawn(self) -> bool:
+ return (self._total_spawns is None or self._total_spawns > 0) and self._current_delay <= 0
+
+ def is_done(self) -> bool:
+ return self._total_spawns == 0
+
+ def update(self, delta: float) -> Generator[Pedestrian]:
+ if not self.is_done():
+ self._current_delay -= delta
+ if self.can_spawn():
+ self._current_delay = self._spawn_delay
+ yield from self.spawn()
+
+ def decrement_total_spawns(self) -> bool:
+ if self._total_spawns is not None:
+ if self._total_spawns > 0:
+ self._total_spawns -= 1
+ self._spawn_count += 1
+ return True
+ else:
+ return False
+
+ self._spawn_count += 1
+ return True
+
+ def _get_target(self, cell) -> 'Target':
+ if self._targeting_strategy == TargetingStrategy.RANDOM:
+ return random.choice(self._targets)
+ elif self._targeting_strategy == TargetingStrategy.CLOSEST:
+ return min(self._targets, key=lambda target: min(self._distancing.calculate_distance(cell, x) for x in target.get_cells()))
+ elif self._targeting_strategy == TargetingStrategy.FARTHEST:
+ return max(self._targets, key=lambda target: max(self._distancing.calculate_distance(cell, x) for x in target.get_cells()))
+ else:
+ raise SimulationError(SimulationErrorCode.NOT_IMPLEMENTED_IN_SIMULATION, {"targeting-strategy": self._targeting_strategy})
+
+ def spawn(self) -> Generator[Pedestrian]:
+ free_cells = list([cell for cell in self._cells if cell.is_free()])
+ random.shuffle(free_cells)
+ for spawn_index in range(min(len(free_cells), self._batch_size)):
+ if self.decrement_total_spawns() is False:
+ break
+
+ cell = free_cells[spawn_index]
+ target = self._get_target(cell)
+ speed = self.SPEED_DISTRIBUTION.sample()
+ pedestrian = Pedestrian(cell.get_x(), cell.get_y(), speed, self, target, self._distancing)
+ yield pedestrian
+
+ def get_serialization_data(self) -> dict[str, any]:
+ return {
+ "id": self.get_identifier(),
+ "total_spawns": self._total_spawns,
+ "current_delay": self._current_delay,
+ "spawn_count": self._spawn_count
+ }
+
+ def get_identifier(self) -> str:
+ return self._name
\ No newline at end of file
diff --git a/simulation/core/target.py b/simulation/core/target.py
new file mode 100644
index 0000000..8d311c8
--- /dev/null
+++ b/simulation/core/target.py
@@ -0,0 +1,74 @@
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from simulation.core.cell_state import CellState
+from serialization.serializable import Serializable
+from simulation.core.position import Position
+from utils import utils
+from typing import TYPE_CHECKING
+
+from utils.immutable_list import ImmutableList
+
+if TYPE_CHECKING:
+ from simulation.core.simulation_grid import SimulationGrid
+ from simulation.core.cell import Cell
+ from simulation.heatmaps.heatmap import Heatmap
+ from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+
+class Target(Serializable):
+ def __init__(self, name: str, cells: 'list[Cell]', grid: 'SimulationGrid', heatmap_generator: 'HeatmapGeneratorBase'):
+ self._name: str = name
+ self._cells: 'list[Cell]' = cells
+ self._heatmap_generator: 'HeatmapGeneratorBase' = heatmap_generator
+ self._grid: 'SimulationGrid' = grid
+ self._heatmap: 'Heatmap'|None = None
+ self._exit_count: int = 0
+ self._is_static_heatmap: bool = CellState.OCCUPIED not in heatmap_generator.get_blocked()
+
+ def get_name(self) -> str:
+ return self._name
+
+ def get_cells(self) -> ImmutableList['Cell']:
+ return ImmutableList(self._cells)
+
+ def get_heatmap(self) -> 'Heatmap':
+ if self._heatmap is None:
+ raise SimulationError(SimulationErrorCode.VALUE_NOT_INITIALIZED, {"value": "heatmap"})
+
+ return self._heatmap
+
+ def update_heatmap(self) -> None:
+ if self._is_static_heatmap is False or self._heatmap is None:
+ self._heatmap = self._heatmap_generator.generate_heatmap(self._cells, self._grid)
+
+ def increment_exit_count(self) -> None:
+ self._exit_count += 1
+
+ def get_exit_count(self) -> int:
+ return self._exit_count
+
+ def is_coordinate_inside_target(self, x: int, y: int) -> bool:
+ for cell in self._cells:
+ if cell.get_x() == x and cell.get_y() == y:
+ return True
+
+ def is_inside_target(self, pos: Position) -> bool:
+ for cell in self._cells:
+ if pos.pos_equals(cell):
+ return True
+
+ return False
+
+ def get_serialization_data(self) -> dict[str, any]:
+ data = {
+ "id": self.get_identifier(),
+ "exit_count": self._exit_count,
+ #"heatmap": utils.heatmap_to_base64(self._heatmap)
+ }
+
+ if not self._is_static_heatmap:
+ data["heatmap"] = utils.heatmap_to_base64(self._heatmap)
+
+ return data
+
+ def get_identifier(self) -> str:
+ return self._name
diff --git a/simulation/core/targeting_stratey.py b/simulation/core/targeting_stratey.py
new file mode 100644
index 0000000..6de742b
--- /dev/null
+++ b/simulation/core/targeting_stratey.py
@@ -0,0 +1,7 @@
+from enum import Enum, IntEnum
+
+
+class TargetingStrategy(IntEnum):
+ RANDOM = 1,
+ CLOSEST = 2,
+ FARTHEST = 3
\ No newline at end of file
diff --git a/simulation/core/teleporter.py b/simulation/core/teleporter.py
new file mode 100644
index 0000000..7408a5e
--- /dev/null
+++ b/simulation/core/teleporter.py
@@ -0,0 +1,33 @@
+import random
+from typing import Generator
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from simulation.core.pedestrian import Pedestrian
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.cell import Cell
+ from simulation.core.target import Target
+ from simulation.core.spawner import Spawner
+ from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+class Teleporter():
+
+ def __init__(self, cells: list['Cell']) -> None:
+ self._cells: list['Cell'] = cells
+
+ def has_free_cell(self) -> bool:
+ for cell in self._cells:
+ if (cell.is_free()):
+ return True
+ return False
+
+ def spawn(self, speed, spawner: 'Spawner', target: 'Target', distancing: 'DistanceBase', time_alive: float) -> Pedestrian:
+ free_cells = list([cell for cell in self._cells if cell.is_free()])
+ random.shuffle(free_cells)
+ if len(free_cells) < 1:
+ raise SimulationError(SimulationErrorCode.TELEPORTER_FULL)
+ cell = free_cells[0]
+ pedestrian = Pedestrian(cell.get_x(), cell.get_y(), speed, spawner, target, distancing, time_alive)
+ return pedestrian
\ No newline at end of file
diff --git a/simulation/core/waypoint.py b/simulation/core/waypoint.py
new file mode 100644
index 0000000..baafd89
--- /dev/null
+++ b/simulation/core/waypoint.py
@@ -0,0 +1,64 @@
+import utils.utils
+from serialization.serializable import Serializable
+from simulation.core.cell_state import CellState
+from simulation.core.position import Position
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from simulation.core.pedestrian import Pedestrian
+ from simulation.core.cell import Cell
+
+class Waypoint(Serializable):
+ WAYPOINT_ID_COUNTER = 0
+
+ @staticmethod
+ def get_next_id() -> int:
+ Waypoint.WAYPOINT_ID_COUNTER += 1
+ return Waypoint.WAYPOINT_ID_COUNTER
+
+ def __init__(self, heatmap_generator: HeatmapGeneratorBase, grid: SimulationGrid, cell: 'Cell', pedestrian: 'Pedestrian'):
+ self._id = Waypoint.get_next_id()
+ self._heatmap_generator = heatmap_generator
+ self._heatmap: Heatmap = None
+ self._is_static_heatmap = CellState.OCCUPIED in heatmap_generator.get_blocked()
+ self._grid: SimulationGrid = grid
+ self._cell = cell
+ self._pedestrian: 'Pedestrian' = pedestrian
+ self.update()
+
+ def get_pedestrian(self) -> 'Pedestrian':
+ return self._pedestrian
+
+ def get_heatmap(self) -> Heatmap:
+ return self._heatmap
+
+ def update(self):
+ if not self._is_static_heatmap and self._heatmap is None:
+ self._heatmap = self._heatmap_generator.generate_heatmap([self._cell], self._grid)
+
+ def get_cell(self) -> 'Cell':
+ return self._cell
+
+ def next_cell(self, current: Position) -> 'Cell|None':
+ for cell in sorted(self._grid.get_neighbours_at(current), key=lambda x: self._heatmap.get_cell_at_pos(x)):
+ if cell.is_free():
+ return cell
+
+ return None
+
+ def is_inside_waypoint(self, pos: Position):
+ return self._cell.pos_equals(pos)
+
+ def get_serialization_data(self) -> dict[str, any]:
+ return {
+ "id": self.get_identifier(),
+ "cell": self._cell.get_identifier(),
+ "pedestrian": self._pedestrian.get_identifier(),
+ "heatmap": utils.utils.heatmap_to_base64(self._heatmap)
+ }
+
+ def get_identifier(self) -> str:
+ return str(f"W_{self._id}")
\ No newline at end of file
diff --git a/tests/simulation/__init__.py b/simulation/heatmaps/__init__.py
similarity index 100%
rename from tests/simulation/__init__.py
rename to simulation/heatmaps/__init__.py
diff --git a/simulation/heatmaps/distance_heatmap_generator.py b/simulation/heatmaps/distance_heatmap_generator.py
new file mode 100644
index 0000000..d0896a9
--- /dev/null
+++ b/simulation/heatmaps/distance_heatmap_generator.py
@@ -0,0 +1,31 @@
+from typing import Iterable
+
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+
+class DistanceHeatmapGenerator(HeatmapGeneratorBase):
+ def __init__(self, distancing: DistanceBase, blocked=None):
+ """
+ :param distancing: algorithm for calculating distance between cells
+ :param delta_x: distance between cells
+ :param blocked: set of CellStates that are considered blocked, default is {CellState.OBSTACLE}
+ """
+ super().__init__(blocked if blocked is not None else {CellState.OBSTACLE})
+ self._distancing = distancing
+ self._delta_x = distancing.get_scale()
+
+ def generate_heatmap(self, target: Iterable[Cell], grid: SimulationGrid) -> Heatmap:
+ heatmap = Heatmap(grid.get_width(), grid.get_height())
+ for cell in grid.get_cells():
+ if cell.get_state() in self.get_blocked():
+ heatmap.set_cell_at_pos(cell, Heatmap.INFINITY)
+
+ min_distance = min([self._distancing.calculate_distance(cell, target_cell) for target_cell in target])
+ heatmap.set_cell_at_pos(cell, min_distance)
+
+ return heatmap
\ No newline at end of file
diff --git a/tests/simulation/locomotionAlgorithms/testDijkstra.py b/simulation/heatmaps/distancing/__init__.py
similarity index 100%
rename from tests/simulation/locomotionAlgorithms/testDijkstra.py
rename to simulation/heatmaps/distancing/__init__.py
diff --git a/simulation/heatmaps/distancing/base_distance.py b/simulation/heatmaps/distancing/base_distance.py
new file mode 100644
index 0000000..ffe2c64
--- /dev/null
+++ b/simulation/heatmaps/distancing/base_distance.py
@@ -0,0 +1,19 @@
+from abc import abstractmethod, ABC
+
+from simulation.core.position import Position
+
+
+class DistanceBase(ABC):
+ def __init__(self, scale: float = 1.0):
+ self._scale = scale
+ pass
+
+ @abstractmethod
+ def _calculate_distance(self, pos1: Position, pos2: Position) -> float:
+ pass
+
+ def get_scale(self) -> float:
+ return self._scale
+
+ def calculate_distance(self, pos1: Position, pos2: Position) -> float:
+ return self._calculate_distance(pos1, pos2) * self._scale
\ No newline at end of file
diff --git a/simulation/heatmaps/distancing/euclidean_distance.py b/simulation/heatmaps/distancing/euclidean_distance.py
new file mode 100644
index 0000000..34ccf93
--- /dev/null
+++ b/simulation/heatmaps/distancing/euclidean_distance.py
@@ -0,0 +1,15 @@
+import math
+
+from simulation.core.position import Position
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+
+class EuclideanDistance(DistanceBase):
+ def __init__(self, scale: float):
+ """
+ :param scale: scale of the distance calculation
+ """
+ super().__init__(scale)
+
+ def _calculate_distance(self, pos1: Position, pos2: Position) -> float:
+ return math.sqrt(math.pow (pos1.get_x() - pos2.get_x(), 2) + math.pow(pos1.get_y() - pos2.get_y(), 2))
diff --git a/simulation/heatmaps/distancing/taxi_distance.py b/simulation/heatmaps/distancing/taxi_distance.py
new file mode 100644
index 0000000..f1ff5db
--- /dev/null
+++ b/simulation/heatmaps/distancing/taxi_distance.py
@@ -0,0 +1,13 @@
+from simulation.core.position import Position
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+
+class TaxiDistance(DistanceBase):
+ def __init__(self, scale: float):
+ """
+ :param scale: scale of the distance calculation
+ """
+ super().__init__(scale)
+
+ def _calculate_distance(self, pos1: Position, pos2: Position) -> float:
+ return abs(pos1.get_x() - pos2.get_x()) + abs(pos1.get_y() - pos2.get_y())
\ No newline at end of file
diff --git a/simulation/heatmaps/djisktra_heatmap_generator.py b/simulation/heatmaps/djisktra_heatmap_generator.py
new file mode 100644
index 0000000..f7ba381
--- /dev/null
+++ b/simulation/heatmaps/djisktra_heatmap_generator.py
@@ -0,0 +1,51 @@
+import heapq
+from typing import Iterable
+
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.grid_base import GridBase
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+from simulation.heatmaps.pathfinding_queue import PathfindingQueue
+
+
+class DijkstraHeatmapGenerator(HeatmapGeneratorBase):
+ """
+ Heatmap generator using the Dijkstra algorithm
+ """
+ def __init__(self, distancing: DistanceBase, blocked: set[CellState] = None):
+ """
+ :param distancing: algorithm for calculating distance between cells
+ :param blocked: set of CellStates that are considered blocked, default is {CellState.OBSTACLE}
+ """
+ super().__init__(blocked if blocked is not None else {CellState.OBSTACLE})
+ self._distancing = distancing
+
+
+ def generate_heatmap(self, target: Iterable[Cell], grid: SimulationGrid) -> Heatmap:
+ heatmap = Heatmap(grid.get_width(), grid.get_height())
+ visited: PathfindingQueue[Cell] = PathfindingQueue()
+
+ for cell in target:
+ heatmap.set_cell(cell.get_x(), cell.get_y(), 0)
+ visited.push(cell, 0)
+
+ for cell in grid.get_cells():
+ if cell.get_state() in self._blocked: # can't enter cells which aren't free
+ heatmap.set_cell(cell.get_x(), cell.get_y(), Heatmap.INFINITY)
+ visited.mark_visited(cell)
+
+ while len(visited) > 0:
+ current = visited.pop()
+ current_distance = heatmap.get_cell_at_pos(current)
+ neighbours = grid.get_neighbours_at(current, 1)
+ for neighbour in neighbours:
+ if neighbour not in visited:
+ distance = current_distance + self._distancing.calculate_distance(current, neighbour)
+ if heatmap.get_cell_at_pos(neighbour) > distance:
+ heatmap.set_cell(neighbour.get_x(), neighbour.get_y(), distance)
+ visited.push(neighbour, distance)
+
+ return heatmap
diff --git a/simulation/heatmaps/fast_marching_heatmap_generator.py b/simulation/heatmaps/fast_marching_heatmap_generator.py
new file mode 100644
index 0000000..d1f57a0
--- /dev/null
+++ b/simulation/heatmaps/fast_marching_heatmap_generator.py
@@ -0,0 +1,98 @@
+import math
+from typing import Iterable, Generator
+
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+from simulation.heatmaps.pathfinding_queue import PathfindingQueue
+from simulation.neighbourhood.base_neighbourhood import NeighbourhoodBase
+from simulation.neighbourhood.moore_neighbourhood import MooreNeighbourhood
+from simulation.neighbourhood.neumann_neighbourhood import NeumannNeighbourhood
+
+class FastMarchingHeatmapGenerator(HeatmapGeneratorBase):
+ """
+ Heatmap generator using the Fast Marching Method
+ """
+ def __init__(self, distancing: DistanceBase, blocked=None):
+ """
+ :param distancing: algorithm for calculating distance between cells
+ :param delta_x: distance between cells
+ :param blocked: set of CellStates that are considered blocked, default is {CellState.OBSTACLE}
+ """
+ super().__init__(blocked if blocked is not None else {CellState.OBSTACLE})
+ self._neumann = NeumannNeighbourhood(1, 1)
+ self._distancing = distancing
+ self._delta_x = distancing.get_scale()
+
+ def _get_narrow_band(self, cell: Cell, grid: SimulationGrid) -> Generator[Cell]:
+ for x, y in self._neumann.get_neighbours(cell.get_x(), cell.get_y(), 1, 1):
+ if grid.is_in_bounds(x, y):
+ yield grid.get_cell(x, y)
+
+
+ def _get_fixed_neighbours(self, cell: Cell, fixed: set[Cell], grid: SimulationGrid) -> Generator[Cell]:
+ narrow_band = self._get_narrow_band(cell, grid)
+ for neighbour in narrow_band:
+ if neighbour in fixed and neighbour.get_state() != CellState.OBSTACLE:
+ yield neighbour
+
+
+ def _are_opposite(self, a: Cell, b: Cell) -> bool:
+ return a.get_x() == b.get_x() or a.get_y() == b.get_y()
+
+ def _calculate_travel_time(self, cell: Cell, heatmap: Heatmap, fixed: set[Cell], grid: SimulationGrid) -> float:
+ fixed_neighbours = list(self._get_fixed_neighbours(cell, fixed, grid))
+ if len(fixed_neighbours) == 0:
+ return Heatmap.INFINITY
+ elif len(fixed_neighbours) == 1:
+ return heatmap.get_cell_at_pos(fixed_neighbours[0]) + self._distancing.calculate_distance(cell, fixed_neighbours[0])
+ elif len(fixed_neighbours) == 2:
+ a = heatmap.get_cell_at_pos(fixed_neighbours[0])
+ b = heatmap.get_cell_at_pos(fixed_neighbours[1])
+ return (a + b + math.sqrt(2 * (1/self._delta_x)**2 - ((a - b) ** 2))) / 2
+ elif len(fixed_neighbours) == 3:
+ single = fixed_neighbours[0] if self._are_opposite(fixed_neighbours[1], fixed_neighbours[2]) else fixed_neighbours[1] if self._are_opposite(fixed_neighbours[0], fixed_neighbours[2]) else fixed_neighbours[2]
+ others = [heatmap.get_cell_at_pos(neighbour) for neighbour in fixed_neighbours if neighbour != single]
+ a = heatmap.get_cell_at_pos(single)
+ b = min(others[0], others[1])
+ return (a + b + math.sqrt(2 * (1/self._delta_x)**2 - ((a - b) ** 2))) / 2
+
+
+
+ def generate_heatmap(self, target: Iterable[Cell], grid: SimulationGrid) -> Heatmap:
+ heatmap: Heatmap = Heatmap(grid.get_width(), grid.get_height())
+ visited: PathfindingQueue[Cell] = PathfindingQueue()
+ fixed: set[Cell] = set()
+
+ for cell in target:
+ visited.mark_visited(cell)
+ heatmap.set_cell(cell.get_x(), cell.get_y(), 0)
+ fixed.add(cell)
+
+ for cell in target:
+ for neighbour in self._get_narrow_band(cell, grid):
+ if neighbour not in visited:
+ distance = self._calculate_travel_time(neighbour, heatmap, fixed, grid)
+ heatmap.set_cell(neighbour.get_x(), neighbour.get_y(), distance)
+ visited.push(neighbour, distance)
+
+ while not visited.is_empty():
+ lowest = visited.pop()
+ fixed.add(lowest)
+ for neighbour in self._get_narrow_band(lowest, grid):
+ if neighbour not in visited:
+ if neighbour.get_state() in self._blocked:
+ heatmap.set_cell(neighbour.get_x(), neighbour.get_y(), Heatmap.INFINITY)
+ visited.mark_visited(neighbour)
+ else:
+ distance = self._calculate_travel_time(neighbour, heatmap, fixed, grid)
+ if distance < heatmap.get_cell_at_pos(neighbour):
+ heatmap.set_cell(neighbour.get_x(), neighbour.get_y(), distance)
+ visited.push(neighbour, distance)
+
+ return heatmap
\ No newline at end of file
diff --git a/simulation/heatmaps/heatmap.py b/simulation/heatmaps/heatmap.py
new file mode 100644
index 0000000..344a620
--- /dev/null
+++ b/simulation/heatmaps/heatmap.py
@@ -0,0 +1,28 @@
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+from simulation.core.grid_base import GridBase
+from simulation.core.position import Position
+
+
+class Heatmap(GridBase[float]):
+ """
+ Heatmap represents a special grid of float values, it's main usage is for building 2D navigation maps
+ width: int - width of the heatmap
+ height: int - height of the heatmap
+ initial_value: float - initial value for each cell, default is INFINITY
+ """
+
+ INFINITY = float("inf")
+ def __init__(self, width: int, height: int, initial_value: float = INFINITY):
+ self._initial_value = initial_value
+ super().__init__(width, height)
+
+ def _init_cell(self, x: int, y: int) -> float:
+ return self._initial_value
+
+ def set_cell(self, x: int, y: int, value: float) -> None:
+ self.check_bounds(x, y)
+ self.cells[self._get_cell_index(x, y)] = value
+
+ def set_cell_at_pos(self, pos: Position, value: float) -> None:
+ self.set_cell(pos.get_x(), pos.get_y(), value)
\ No newline at end of file
diff --git a/simulation/heatmaps/heatmap_generator_base.py b/simulation/heatmaps/heatmap_generator_base.py
new file mode 100644
index 0000000..8802162
--- /dev/null
+++ b/simulation/heatmaps/heatmap_generator_base.py
@@ -0,0 +1,32 @@
+from abc import ABC, abstractmethod
+from typing import Iterable
+
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.grid_base import GridBase
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+
+
+class HeatmapGeneratorBase(ABC):
+ """
+ Base class for heatmap generators. Heatmap generators generate heatmaps based on the given target and grid
+ """
+
+ def __init__(self, blocked: set[CellState] ):
+ self._blocked: set[CellState] = blocked
+ pass
+
+
+ @abstractmethod
+ def generate_heatmap(self, target: Iterable[Cell], grid: SimulationGrid) -> Heatmap:
+ """
+ Generates heatmap based on the given target and grid
+ :param target: target cells off the heatmap
+ :param grid: the simulation grid the heatmap should be generated for
+ :return: a heatmap where each cell is assigned a distance to the closest target cell
+ """
+ pass
+
+ def get_blocked(self) -> set[CellState]:
+ return self._blocked
\ No newline at end of file
diff --git a/simulation/heatmaps/infinity_heatmap_generator.py b/simulation/heatmaps/infinity_heatmap_generator.py
new file mode 100644
index 0000000..4a21eb4
--- /dev/null
+++ b/simulation/heatmaps/infinity_heatmap_generator.py
@@ -0,0 +1,27 @@
+from typing import Iterable
+
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+
+
+class InfinityHeatmapGenerator(HeatmapGeneratorBase):
+ def __init__(self, distancing: DistanceBase, blocked=None):
+ """
+ :param distancing: algorithm for calculating distance between cells
+ :param delta_x: distance between cells
+ :param blocked: set of CellStates that are considered blocked, default is {CellState.OBSTACLE}
+ """
+ super().__init__(blocked if blocked is not None else {CellState.OBSTACLE})
+ self._distancing = distancing
+ self._delta_x = distancing.get_scale()
+
+ def generate_heatmap(self, target: Iterable[Cell], grid: SimulationGrid) -> Heatmap:
+ heatmap = Heatmap(grid.get_width(), grid.get_height())
+ for cell in grid.get_cells():
+ heatmap.set_cell_at_pos(cell, Heatmap.INFINITY)
+
+ return heatmap
\ No newline at end of file
diff --git a/simulation/heatmaps/pathfinding_queue.py b/simulation/heatmaps/pathfinding_queue.py
new file mode 100644
index 0000000..fd031fb
--- /dev/null
+++ b/simulation/heatmaps/pathfinding_queue.py
@@ -0,0 +1,66 @@
+import heapq
+
+
+class PriorityItem[T]:
+ """
+ Wrapper class for items in the PathfindingQueue, holds a float for priority and an item of generic type T
+ """
+ def __init__(self, item: T, priority: float):
+ self.item = item
+ self.priority = priority
+
+ def __lt__(self, other):
+ return self.priority < other.priority
+
+ def __eq__(self, other):
+ return self.priority == other.priority
+
+ def __gt__(self, other):
+ return self.priority > other.priority
+
+ def get_item(self) -> T:
+ return self.item
+
+class PathfindingQueue[T]:
+ """
+ Priority queue for pathfinding algorithms, which also keeps track of visited items
+ """
+ def __init__(self):
+ self._queue: list[PriorityItem[T]] = []
+ self._items: set[T] = set()
+
+ def __contains__(self, item: T) -> bool:
+ return item in self._items
+
+ def __len__(self):
+ return len(self._queue)
+
+ def mark_visited(self, item: T) -> None:
+ """
+ Marks an item as visited
+ :param item: item which should be marked as visited
+ """
+ self._items.add(item)
+
+ def pop(self) -> T:
+ """
+ Pops the item with the lowest priority from the queue
+ :return: popped item
+ """
+ item = heapq.heappop(self._queue)
+ return item.get_item()
+
+ def push(self, item: T, priority: float) -> None:
+ """
+ Pushes an item to the queue with the given priority and marks it as visited
+ :param item: the item to push
+ :param priority: the priority of the item, lowest gets popped first
+ """
+ self._items.add(item)
+ heapq.heappush(self._queue, PriorityItem(item, priority))
+
+ def is_empty(self) -> bool:
+ """
+ :return: return true if the queue is empty
+ """
+ return len(self._queue) == 0
\ No newline at end of file
diff --git a/simulation/heatmaps/social_distancing_heatmap_generator.py b/simulation/heatmaps/social_distancing_heatmap_generator.py
new file mode 100644
index 0000000..8508ed5
--- /dev/null
+++ b/simulation/heatmaps/social_distancing_heatmap_generator.py
@@ -0,0 +1,63 @@
+import math
+from typing import Generator, Iterable
+
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from simulation.core.grid_base import GridBase
+from simulation.core.pedestrian import Pedestrian
+from simulation.core.position import Position
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+
+
+class SocialDistancingHeatmapGenerator(HeatmapGeneratorBase):
+ """
+ Heatmap generator which adds a social distancing force from each occupied cell
+ """
+
+ def __init__(self, distancing: DistanceBase, width: float, height: float, blocked: set[CellState] = None):
+ """
+ :param distancing: algorithm for calculating distance between cells
+ :param width: width of the social distancing force
+ :param height: height of the social distancing force
+ :param blocked: set of CellStates that are considered blocked, default is {CellState.OCCUPIED}
+ """
+ super().__init__( blocked or {CellState.OCCUPIED})
+ self._distancing: DistanceBase = distancing
+ self._width: float = width
+ self._height: float = height
+ self._neighbour_width: int = math.ceil(width / 2)
+ self._neighbour_height: int = math.ceil(height / 2)
+ self._intensity: float = distancing.get_scale()
+
+ def get_bias(self, center: Position, neighbour: Position):
+ return self._calculate_value(self._distancing.calculate_distance(center, neighbour))
+
+ def get_max_value(self):
+ return 8 * self._calculate_value(self._distancing.get_scale())
+
+ def _calculate_value(self, distance: float):
+ if abs(distance) < self._width:
+ return self._intensity * (self._height * math.exp(1/(math.pow(distance/self._width, 2) - 1)))
+ else:
+ return 0
+
+ def generate_heatmap(self, target: Iterable[Cell], grid: SimulationGrid) -> Heatmap:
+ """
+ :param target: a list of cells which should be used to generate repulsive force, only cells with state in blocked are considered
+ :param grid: the simulation grid the heatmap should be generated for
+ :return: a heatmap where each obstacle creates a repulsive force
+ """
+ heatmap = Heatmap(grid.get_width(), grid.get_height(), 0.0)
+ for cell in target:
+ if cell.get_state() in self._blocked:
+ heatmap.set_cell_at_pos(cell, self._calculate_value(0))
+ for neighbour in grid.get_neighbours_at(cell, self._neighbour_width, self._neighbour_height):
+ current_value = heatmap.get_cell_at_pos(neighbour)
+ if current_value is not Heatmap.INFINITY :
+ value = self._calculate_value(self._distancing.calculate_distance(cell, neighbour))
+ heatmap.set_cell_at_pos(neighbour, current_value + value)
+
+ return heatmap
\ No newline at end of file
diff --git a/simulation/neighbourhood/__init__.py b/simulation/neighbourhood/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/simulation/neighbourhood/base_neighbourhood.py b/simulation/neighbourhood/base_neighbourhood.py
new file mode 100644
index 0000000..a1b3534
--- /dev/null
+++ b/simulation/neighbourhood/base_neighbourhood.py
@@ -0,0 +1,20 @@
+from abc import abstractmethod, ABC
+from typing import Generator
+
+class NeighbourhoodBase(ABC):
+
+
+ """
+ NeighbourhoodBase represents a basic algorithm which returns a list of neighbouring coordinates given grid dimensions and a radius
+ """
+ def __init__(self, width: int, height: int):
+ """
+ :param width: width of the neighbourhood
+ :param height: height of the neighbourhood
+ """
+ self._height = height
+ self._width = width
+
+ @abstractmethod
+ def get_neighbours(self, x: int, y: int, width: int, height: int) -> Generator[tuple[int, int]]:
+ pass
\ No newline at end of file
diff --git a/simulation/neighbourhood/moore_neighbourhood.py b/simulation/neighbourhood/moore_neighbourhood.py
new file mode 100644
index 0000000..6fb1446
--- /dev/null
+++ b/simulation/neighbourhood/moore_neighbourhood.py
@@ -0,0 +1,22 @@
+from typing import Generator
+
+from simulation.neighbourhood.base_neighbourhood import NeighbourhoodBase
+
+
+class MooreNeighbourhood(NeighbourhoodBase):
+ """
+ MooreNeighbourhood represents a neighbourhood which returns all neighbours in a rectangle defined by width and height around the given cell
+ """
+ def __init__(self, width: int, height: int):
+ super().__init__(width, height)
+
+ def get_neighbours(self, x: int, y: int, width: int, height: int) -> Generator[tuple[int, int]]:
+ for i in range(x - width, x + width + 1):
+ for j in range(y - height, y + height + 1):
+ if i == x and j == y:
+ continue
+
+ if i < 0 or i >= self._width or j < 0 or j >= self._height:
+ continue
+
+ yield i, j
diff --git a/simulation/neighbourhood/neumann_neighbourhood.py b/simulation/neighbourhood/neumann_neighbourhood.py
new file mode 100644
index 0000000..8be5d57
--- /dev/null
+++ b/simulation/neighbourhood/neumann_neighbourhood.py
@@ -0,0 +1,17 @@
+from typing import Generator
+
+from simulation.neighbourhood.base_neighbourhood import NeighbourhoodBase
+
+
+class NeumannNeighbourhood(NeighbourhoodBase):
+ """
+ NeumannNeighbourhood represents a neighbourhood which returns all neighbours in cardinal directions around the given cell
+ """
+ def get_neighbours(self, x: int, y: int, width: int, height: int) -> Generator[tuple[int, int]]:
+ for i in range(x - width, x + width + 1):
+ if i != x:
+ yield i, y
+
+ for j in range(y - height, y + height + 1):
+ if j != y:
+ yield x, j
\ No newline at end of file
diff --git a/simulation_config/__init__.py b/simulation_config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/simulation_config/chicken_test_euklid.json b/simulation_config/chicken_test_euklid.json
new file mode 100644
index 0000000..3c2f46d
--- /dev/null
+++ b/simulation_config/chicken_test_euklid.json
@@ -0,0 +1,54 @@
+{
+ "grid": {
+ "width": 70,
+ "height": 30,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "u-upper",
+ "rect": [10, 7, 30, 7]
+ },
+ {
+ "name": "u-lower",
+ "rect": [10, 22, 30, 22]
+ },
+ {
+ "name": "u-vertical",
+ "rect": [30, 7, 30, 22]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "cells": [[0, 13], [0, 14], [0, 15], [0, 16]],
+ "targets": ["Target"],
+ "total_spawns": 20,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "cells": [[69, 13], [69, 14], [69, 15], [69, 16]],
+ "heatmap_generator": "DistanceHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 2.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ },
+ "log_file": "chicken_test_{0}.json"
+}
\ No newline at end of file
diff --git a/simulation_config/chicken_test_fast_marching.json b/simulation_config/chicken_test_fast_marching.json
new file mode 100644
index 0000000..212d11c
--- /dev/null
+++ b/simulation_config/chicken_test_fast_marching.json
@@ -0,0 +1,54 @@
+{
+ "grid": {
+ "width": 70,
+ "height": 30,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "u-upper",
+ "rect": [10, 7, 30, 7]
+ },
+ {
+ "name": "u-lower",
+ "rect": [10, 22, 30, 22]
+ },
+ {
+ "name": "u-vertical",
+ "rect": [30, 7, 30, 22]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "cells": [[0, 13], [0, 14], [0, 15], [0, 16]],
+ "targets": ["Target"],
+ "total_spawns": 20,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "cells": [[69, 13], [69, 14], [69, 15], [69, 16]],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 2.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ },
+ "log_file": "chicken_test_{0}.json"
+}
\ No newline at end of file
diff --git a/simulation_config/chicken_test_flood_fill.json b/simulation_config/chicken_test_flood_fill.json
new file mode 100644
index 0000000..c1d836d
--- /dev/null
+++ b/simulation_config/chicken_test_flood_fill.json
@@ -0,0 +1,54 @@
+{
+ "grid": {
+ "width": 70,
+ "height": 30,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "u-upper",
+ "rect": [10, 7, 30, 7]
+ },
+ {
+ "name": "u-lower",
+ "rect": [10, 22, 30, 22]
+ },
+ {
+ "name": "u-vertical",
+ "rect": [30, 7, 30, 22]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "cells": [[0, 13], [0, 14], [0, 15], [0, 16]],
+ "targets": ["Target"],
+ "total_spawns": 20,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "cells": [[69, 13], [69, 14], [69, 15], [69, 16]],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 2.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ },
+ "log_file": "chicken_test_{0}.json"
+}
\ No newline at end of file
diff --git a/simulation_config/config_loader.py b/simulation_config/config_loader.py
new file mode 100644
index 0000000..46ea3fb
--- /dev/null
+++ b/simulation_config/config_loader.py
@@ -0,0 +1,64 @@
+import json
+from simulation.core.cell_state import CellState
+from simulation.heatmaps.distance_heatmap_generator import DistanceHeatmapGenerator
+from simulation.heatmaps.infinity_heatmap_generator import InfinityHeatmapGenerator
+from simulation.heatmaps.distancing.euclidean_distance import EuclideanDistance
+from simulation.heatmaps.djisktra_heatmap_generator import DijkstraHeatmapGenerator
+from simulation.heatmaps.fast_marching_heatmap_generator import FastMarchingHeatmapGenerator
+from simulation.neighbourhood.moore_neighbourhood import MooreNeighbourhood
+
+
+class SimulationConfigLoader:
+ """Utility class to load and process simulation configuration."""
+
+ @staticmethod
+ def load_config(config_file):
+ """Load the JSON configuration from a file."""
+ with open(config_file, 'r') as file:
+ return json.load(file)
+
+ @staticmethod
+ def create_heatmap_generator(generator_name, distancing, blocked_states):
+ """Create the appropriate heatmap generator based on configuration."""
+ if generator_name == "FastMarchingHeatmapGenerator":
+ return FastMarchingHeatmapGenerator(distancing, blocked_states)
+ elif generator_name == "DijkstraHeatmapGenerator":
+ return DijkstraHeatmapGenerator(distancing, blocked_states)
+ elif generator_name == "DistanceHeatmapGenerator":
+ return DistanceHeatmapGenerator(distancing, blocked_states)
+ elif generator_name == "InfinityHeatmapGenerator":
+ return InfinityHeatmapGenerator(distancing, blocked_states)
+ elif generator_name is None:
+ return None
+ else:
+ raise ValueError(f"Unsupported heatmap generator: {generator_name}")
+
+ @staticmethod
+ def get_neighbourhood_class(neighbourhood_name):
+ """Map the neighbourhood name from JSON to the actual class."""
+ neighbourhood_mapping = {
+ "MooreNeighbourhood": MooreNeighbourhood,
+ }
+ if neighbourhood_name in neighbourhood_mapping:
+ return neighbourhood_mapping[neighbourhood_name]
+ raise ValueError(f"Unsupported neighbourhood type: {neighbourhood_name}")
+
+ @staticmethod
+ def get_distance_class(distance_name):
+ """Map the distance name from JSON to the actual class."""
+ distance_mapping = {
+ "EuclideanDistance": EuclideanDistance
+ }
+ if distance_name in distance_mapping:
+ return distance_mapping[distance_name]
+ raise ValueError(f"Unsupported distance type: {distance_name}")
+
+ @staticmethod
+ def get_cell_states(cellstate_names):
+ """Map the cell state names from JSON to the actual CellState objects."""
+ try:
+ return None if cellstate_names is None else {CellState[state_name] for state_name in cellstate_names}
+ except KeyError as e:
+ raise ValueError(
+ f"Invalid cellstate name: {e.args[0]}. Ensure it matches the CellState enum."
+ ) from e
diff --git a/simulation_config/demo.json b/simulation_config/demo.json
new file mode 100644
index 0000000..05e4c30
--- /dev/null
+++ b/simulation_config/demo.json
@@ -0,0 +1,84 @@
+{
+ "grid": {
+ "width": 105,
+ "height": 11,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "left_center",
+ "rect": [23, 4, 25, 6]
+ },
+ {
+ "name": "middle_center",
+ "rect": [67, 4, 70, 8]
+ },
+ {
+ "name": "upper_wall",
+ "rect": [0, 0, 104, 0]
+ },
+ {
+ "name": "lower_wall",
+ "rect": [0, 10, 104, 10]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "initial Spawner",
+ "rect": [0, 1, 104, 9],
+ "targets": ["Target"],
+ "total_spawns": 500,
+ "batch_size": 500,
+ "spawn_delay": 1.0,
+ "initial_delay": 0
+ },
+ {
+ "name": "Spawner",
+ "rect": [0, 1, 104, 9],
+ "targets": ["Target"],
+ "total_spawns": 700,
+ "batch_size": 30,
+ "spawn_delay": 45,
+ "initial_delay": 5.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "rect": [104, 1, 104, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "teleports": {
+ "name": "teleporter",
+ "rect": [0,1,0,9]
+ },
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "obstacle_repulsion": {
+ "width": 1,
+ "height": 1,
+ "intensity": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "flow_meters": [
+ {
+ "name": "FlowMeter",
+ "rect": [46, 1, 46, 9],
+ "time_span": 44,
+ "logfile": "flowMeterHigh.csv",
+ "log_interval": 45
+ }
+ ],
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/experiment.json b/simulation_config/experiment.json
new file mode 100644
index 0000000..0158fb2
--- /dev/null
+++ b/simulation_config/experiment.json
@@ -0,0 +1,70 @@
+{
+ "grid": {
+ "width": 70,
+ "height": 35,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ ],
+ "spawners": [
+ {
+ "name": "firstWall",
+ "rect": [35, 16, 35, 16],
+ "targets": ["infinity"],
+ "total_spawns": 1,
+ "batch_size": 3,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ },
+ {
+ "name": "secondWall",
+ "rect": [35,18,35,18],
+ "targets": ["infinity"],
+ "total_spawns": 1,
+ "batch_size": 3,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ },
+ {
+ "name": "pedestrian",
+ "rect": [0,17,0,17],
+ "targets": ["Target"],
+ "total_spawns": 1,
+ "batch_size": 3,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "infinity",
+ "rect": [8, 0, 8, 0],
+ "heatmap_generator": "InfinityHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "Target",
+ "rect": [69, 17, 69, 17],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 50,
+ "height": 50
+ },
+ "obstacle_repulsion": {
+ "width": 2,
+ "height": 2,
+ "intensity": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/freeflow/freeflow_any.json b/simulation_config/freeflow/freeflow_any.json
new file mode 100644
index 0000000..c1c174e
--- /dev/null
+++ b/simulation_config/freeflow/freeflow_any.json
@@ -0,0 +1,88 @@
+{
+ "grid": {
+ "width": 51,
+ "height": 51,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "upper_left",
+ "rect": [24, 24, 24, 24],
+ "targets": ["door_upper_left"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ },
+ {
+ "name": "upper_right",
+ "rect": [25, 25, 25, 25],
+ "targets": ["door_upper_right"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ },
+ {
+ "name": "lower_left",
+ "rect": [24, 24, 25, 25],
+ "targets": ["door_lower_left"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ },
+ {
+ "name": "lower_right",
+ "rect": [25, 25, 25, 25],
+ "targets": ["door_lower_right"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "door_upper_left",
+ "rect": [13, 0, 13, 0],
+ "heatmap_generator": "DistanceHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "door_upper_right",
+ "rect": [50, 13, 50, 13],
+ "heatmap_generator": "DistanceHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "door_lower_right",
+ "rect": [37, 50, 37, 50],
+ "heatmap_generator": "DistanceHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "door_lower_left",
+ "rect": [0, 37, 0, 37],
+ "heatmap_generator": "DistanceHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/freeflow/freeflow_bottom_top.json b/simulation_config/freeflow/freeflow_bottom_top.json
new file mode 100644
index 0000000..07d4cd1
--- /dev/null
+++ b/simulation_config/freeflow/freeflow_bottom_top.json
@@ -0,0 +1,40 @@
+{
+ "grid": {
+ "width": 50,
+ "height": 50,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [24, 24, 25, 25],
+ "targets": ["door"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "door",
+ "rect": [25, 0, 25, 0],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/freeflow/freeflow_diagonal.json b/simulation_config/freeflow/freeflow_diagonal.json
new file mode 100644
index 0000000..e7f83ca
--- /dev/null
+++ b/simulation_config/freeflow/freeflow_diagonal.json
@@ -0,0 +1,88 @@
+{
+ "grid": {
+ "width": 51,
+ "height": 51,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "upper_left",
+ "rect": [24, 24, 24, 24],
+ "targets": ["door_upper_left"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ },
+ {
+ "name": "upper_right",
+ "rect": [25, 25, 25, 25],
+ "targets": ["door_upper_right"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ },
+ {
+ "name": "lower_left",
+ "rect": [24, 24, 25, 25],
+ "targets": ["door_lower_left"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ },
+ {
+ "name": "lower_right",
+ "rect": [25, 25, 25, 25],
+ "targets": ["door_lower_right"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "door_upper_left",
+ "rect": [0, 0, 0, 0],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "door_upper_right",
+ "rect": [50, 0, 50, 0],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "door_lower_right",
+ "rect": [0, 50, 0, 50],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "door_lower_left",
+ "rect": [50, 50, 50, 50],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/freeflow/freeflow_left_right.json b/simulation_config/freeflow/freeflow_left_right.json
new file mode 100644
index 0000000..c28440a
--- /dev/null
+++ b/simulation_config/freeflow/freeflow_left_right.json
@@ -0,0 +1,40 @@
+{
+ "grid": {
+ "width": 51,
+ "height": 51,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [25, 25, 25, 25],
+ "targets": ["door"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "door",
+ "rect": [50, 25, 50, 25],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/freeflow/freeflow_right_left.json b/simulation_config/freeflow/freeflow_right_left.json
new file mode 100644
index 0000000..c0c4b12
--- /dev/null
+++ b/simulation_config/freeflow/freeflow_right_left.json
@@ -0,0 +1,40 @@
+{
+ "grid": {
+ "width": 51,
+ "height": 51,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [25, 25, 25, 25],
+ "targets": ["door"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "door",
+ "rect": [0, 25, 0, 25],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/freeflow/freeflow_top_bottom.json b/simulation_config/freeflow/freeflow_top_bottom.json
new file mode 100644
index 0000000..07d4cd1
--- /dev/null
+++ b/simulation_config/freeflow/freeflow_top_bottom.json
@@ -0,0 +1,40 @@
+{
+ "grid": {
+ "width": 50,
+ "height": 50,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [24, 24, 25, 25],
+ "targets": ["door"],
+ "total_spawns": 1,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "door",
+ "rect": [25, 0, 25, 0],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/rimea_4/high.json b/simulation_config/rimea_4/high.json
new file mode 100644
index 0000000..1891cf8
--- /dev/null
+++ b/simulation_config/rimea_4/high.json
@@ -0,0 +1,84 @@
+{
+ "grid": {
+ "width": 105,
+ "height": 11,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "left_center",
+ "rect": [23, 4, 25, 6]
+ },
+ {
+ "name": "middle_center",
+ "rect": [67, 4, 70, 8]
+ },
+ {
+ "name": "upper_wall",
+ "rect": [0, 0, 104, 0]
+ },
+ {
+ "name": "lower_wall",
+ "rect": [0, 10, 104, 10]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "initial Spawner",
+ "rect": [0, 1, 104, 9],
+ "targets": ["Target"],
+ "total_spawns": 600,
+ "batch_size": 600,
+ "spawn_delay": 1.0,
+ "initial_delay": 0
+ },
+ {
+ "name": "Spawner",
+ "rect": [0, 1, 104, 9],
+ "targets": ["Target"],
+ "total_spawns": 600,
+ "batch_size": 30,
+ "spawn_delay": 45,
+ "initial_delay": 5.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "rect": [104, 1, 104, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "teleports": {
+ "name": "teleporter",
+ "rect": [0,1,0,9]
+ },
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "obstacle_repulsion": {
+ "width": 1,
+ "height": 1,
+ "intensity": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "flow_meters": [
+ {
+ "name": "FlowMeter",
+ "rect": [46, 1, 46, 9],
+ "time_span": 44,
+ "logfile": "flowMeterHigh.csv",
+ "log_interval": 45
+ }
+ ],
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/rimea_4/low.json b/simulation_config/rimea_4/low.json
new file mode 100644
index 0000000..02ec74b
--- /dev/null
+++ b/simulation_config/rimea_4/low.json
@@ -0,0 +1,75 @@
+{
+ "grid": {
+ "width": 105,
+ "height": 11,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "left_center",
+ "rect": [23, 4, 25, 6]
+ },
+ {
+ "name": "middle_center",
+ "rect": [67, 4, 70, 8]
+ },
+ {
+ "name": "upper_wall",
+ "rect": [0, 0, 104, 0]
+ },
+ {
+ "name": "lower_wall",
+ "rect": [0, 10, 104, 10]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [0, 1, 104, 9],
+ "targets": ["Target"],
+ "total_spawns": 600,
+ "batch_size": 10,
+ "spawn_delay": 45,
+ "initial_delay": 5.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "rect": [104, 1, 104, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "teleports": {
+ "name": "teleporter",
+ "rect": [0,1,0,9]
+ },
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "obstacle_repulsion": {
+ "width": 1,
+ "height": 1,
+ "intensity": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "flow_meters": [
+ {
+ "name": "FlowMeter",
+ "rect": [46, 1, 46, 9],
+ "time_span": 44,
+ "logfile": "flowMeter.csv",
+ "log_interval": 45
+ }
+ ],
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/rimea_test_4.json b/simulation_config/rimea_test_4.json
new file mode 100644
index 0000000..9a4e53a
--- /dev/null
+++ b/simulation_config/rimea_test_4.json
@@ -0,0 +1,69 @@
+{
+ "grid": {
+ "width": 210,
+ "height": 22,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "left_center",
+ "rect": [7, 8, 11, 12]
+ },
+ {
+ "name": "middle_center",
+ "rect": [97, 8, 101, 16]
+ },
+ {
+ "name": "upper_wall",
+ "rect": [0, 0, 209, 0]
+ },
+ {
+ "name": "lower_wall",
+ "rect": [0, 21, 209, 21]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [0, 1, 0, 20],
+ "targets": ["Target"],
+ "total_spawns": 100,
+ "batch_size": 3,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "rect": [209, 1, 209, 20],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "obstacle_repulsion": {
+ "width": 2,
+ "height": 2,
+ "intensity": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "flow_meters": [
+ {
+ "name": "FlowMeter",
+ "rect": [95, 1, 95, 20],
+ "time_span": 5.0
+ }
+ ],
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/rimea_test_4_half_size.json b/simulation_config/rimea_test_4_half_size.json
new file mode 100644
index 0000000..753e3b5
--- /dev/null
+++ b/simulation_config/rimea_test_4_half_size.json
@@ -0,0 +1,69 @@
+{
+ "grid": {
+ "width": 105,
+ "height": 11,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "left_center",
+ "rect": [3, 4, 5, 6]
+ },
+ {
+ "name": "middle_center",
+ "rect": [47, 4, 50, 8]
+ },
+ {
+ "name": "upper_wall",
+ "rect": [0, 0, 104, 0]
+ },
+ {
+ "name": "lower_wall",
+ "rect": [0, 10, 104, 10]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [0, 1, 0, 9],
+ "targets": ["Target"],
+ "total_spawns": 100,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "rect": [104, 1, 104, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "obstacle_repulsion": {
+ "width": 1,
+ "height": 1,
+ "intensity": 1.0
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "flow_meters": [
+ {
+ "name": "FlowMeter",
+ "rect": [46, 1, 46, 9],
+ "time_span": 5.0
+ }
+ ],
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.5
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/rimea_test_9_four_doors.json b/simulation_config/rimea_test_9_four_doors.json
new file mode 100644
index 0000000..911f43b
--- /dev/null
+++ b/simulation_config/rimea_test_9_four_doors.json
@@ -0,0 +1,58 @@
+{
+ "grid": {
+ "width": 75,
+ "height": 50,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [5, 5, 70, 45],
+ "targets": ["upper-left-door", "upper-right-door", "lower-left-door", "lower-right-door"],
+ "total_spawns": 1000,
+ "batch_size": 1000,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "upper-left-door",
+ "rect": [10, 0, 13, 0],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "upper-right-door",
+ "rect": [62, 0, 65, 0],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "lower-left-door",
+ "rect": [10, 49, 13, 49],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "lower-right-door",
+ "rect": [62, 49, 65, 49],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/rimea_test_9_two_doors.json b/simulation_config/rimea_test_9_two_doors.json
new file mode 100644
index 0000000..2c09b99
--- /dev/null
+++ b/simulation_config/rimea_test_9_two_doors.json
@@ -0,0 +1,46 @@
+{
+ "grid": {
+ "width": 75,
+ "height": 50,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "spawners": [
+ {
+ "name": "Spawner",
+ "rect": [5, 5, 70, 45],
+ "targets": ["lower-left-door", "lower-right-door"],
+ "total_spawns": 1000,
+ "batch_size": 1000,
+ "spawn_delay": 1,
+ "initial_delay": 0.0,
+ "targeting": "CLOSEST"
+ }
+ ],
+ "targets": [
+ {
+ "name": "lower-left-door",
+ "rect": [10, 49, 13, 49],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "lower-right-door",
+ "rect": [62, 49, 65, 49],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 0.4
+ }
+}
\ No newline at end of file
diff --git a/simulation_config/simulation_config.json b/simulation_config/simulation_config.json
new file mode 100644
index 0000000..0e01ca0
--- /dev/null
+++ b/simulation_config/simulation_config.json
@@ -0,0 +1,50 @@
+{
+ "grid": {
+ "width": 10,
+ "height": 10,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "walls",
+ "cells": [[2, 0], [2, 1], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [2, 7]]
+ },
+ {
+ "name": "barriers",
+ "cells": [[5, 9], [5, 8], [5, 7], [5, 6], [5, 5], [5, 4], [5, 3]]
+ }
+ ],
+ "targets": [
+ {
+ "name": "Target",
+ "cells": [[9, 9], [9, 8], [9, 7]],
+ "heatmap_generator": "FastMarchingHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "Spawner",
+ "cells": [[0, 0], [0, 1], [1, 0], [1, 1]],
+ "targets": ["Target"],
+ "total_spawns": 20,
+ "batch_size": 1,
+ "spawn_delay": 4,
+ "initial_delay": 0.0
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 2.0,
+ "retargeting_threshold": -2.0
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 1
+ }
+}
+
\ No newline at end of file
diff --git a/simulation_config/waypoint_test.json b/simulation_config/waypoint_test.json
new file mode 100644
index 0000000..3dcced1
--- /dev/null
+++ b/simulation_config/waypoint_test.json
@@ -0,0 +1,63 @@
+{
+ "grid": {
+ "width": 30,
+ "height": 10,
+ "neighbourhood": "MooreNeighbourhood"
+ },
+ "obstacles": [
+ {
+ "name": "block",
+ "rect": [4, 4, 15, 8]
+ }
+ ],
+ "spawners": [
+ {
+ "name": "S_left",
+ "cells": [[4, 9]],
+ "targets": ["T_right"],
+ "total_spawns": 2,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ },
+ {
+ "name": "S_right",
+ "cells": [[15, 9]],
+ "targets": ["T_left"],
+ "total_spawns": 2,
+ "batch_size": 1,
+ "spawn_delay": 1,
+ "initial_delay": 0.0
+ }
+ ],
+ "targets": [
+ {
+ "name": "T_left",
+ "rect": [0, 0, 0, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ },
+ {
+ "name": "T_right",
+ "rect": [29, 0, 29, 9],
+ "heatmap_generator": "DijkstraHeatmapGenerator",
+ "cellstate": ["OBSTACLE"]
+ }
+ ],
+ "social_distancing": {
+ "width": 3,
+ "height": 3
+ },
+ "distancing": {
+ "type": "EuclideanDistance",
+ "scale": 1
+ },
+ "simulation": {
+ "time_resolution": 0.1,
+ "occupation_bias_modifier": 1.0,
+ "retargeting_threshold": -2.0,
+ "waypoint_threshold": -3.0,
+ "waypoint_heatmap_generator": "FastMarchingHeatmapGenerator",
+ "waypoint_distance": 15
+ }
+}
\ No newline at end of file
diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc
deleted file mode 100644
index ce6deff..0000000
Binary files a/src/__pycache__/__init__.cpython-311.pyc and /dev/null differ
diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index a892ee8..0000000
Binary files a/src/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/baseSimulation.cpython-312.pyc b/src/__pycache__/baseSimulation.cpython-312.pyc
deleted file mode 100644
index c2ed80a..0000000
Binary files a/src/__pycache__/baseSimulation.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/pedestrian.cpython-312.pyc b/src/__pycache__/pedestrian.cpython-312.pyc
deleted file mode 100644
index 5fcd37c..0000000
Binary files a/src/__pycache__/pedestrian.cpython-312.pyc and /dev/null differ
diff --git a/src/config/__pycache__/config.cpython-311.pyc b/src/config/__pycache__/config.cpython-311.pyc
deleted file mode 100644
index 28d2a6c..0000000
Binary files a/src/config/__pycache__/config.cpython-311.pyc and /dev/null differ
diff --git a/src/config/__pycache__/config.cpython-312.pyc b/src/config/__pycache__/config.cpython-312.pyc
deleted file mode 100644
index c7d892b..0000000
Binary files a/src/config/__pycache__/config.cpython-312.pyc and /dev/null differ
diff --git a/src/config/config.py b/src/config/config.py
deleted file mode 100644
index 4c9a407..0000000
--- a/src/config/config.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import os
-
-class _Config():
-
- _DEFAULT_MAX_NUMBER_OF_ITTERATIONS: int = 200
-
- def __init__(self):
- self.savePath = self._getStandartPath()
- self.maxItterations = self._DEFAULT_MAX_NUMBER_OF_ITTERATIONS
- pass
-
- def getSavePath(self) -> str:
- return self.savePath
-
- def setSavePath(self, path: str) -> None:
- self.savePath = path
-
- def getMaxItterations(self) -> int:
- return self.maxItterations
-
- def setMaxNumberOfItterations(self, maxItterations) -> None:
- self.maxItterations = maxItterations
-
- def _getStandartPath(self) -> str:
- pwd = os.getcwd()
- return os.path.join(pwd, "data")
-
- def __repr__(self) -> str:
- return f"---Config---\nsavePath: <{self.savePath}>\nMaxItterations: <{self.maxItterations}>"
-
-
-class ConfigBuilder():
-
- def __init__(self):
- self.config = _Config()
-
- def setSavePath(self, path: str):
- self.config.setSavePath(path)
- return self
-
- def setNumberOfMaxItterations(self, maxItterations):
- self.config.setMaxNumberOfItterations(maxItterations)
- return self
-
- def build(self) -> _Config:
- return self.config
\ No newline at end of file
diff --git a/src/dtoModule/__pycache__/tileDTO.cpython-311.pyc b/src/dtoModule/__pycache__/tileDTO.cpython-311.pyc
deleted file mode 100644
index a5475de..0000000
Binary files a/src/dtoModule/__pycache__/tileDTO.cpython-311.pyc and /dev/null differ
diff --git a/src/dtoModule/__pycache__/tileDTO.cpython-312.pyc b/src/dtoModule/__pycache__/tileDTO.cpython-312.pyc
deleted file mode 100644
index b98c3f0..0000000
Binary files a/src/dtoModule/__pycache__/tileDTO.cpython-312.pyc and /dev/null differ
diff --git a/src/dtoModule/tileDTO.py b/src/dtoModule/tileDTO.py
deleted file mode 100644
index 4cb7c81..0000000
--- a/src/dtoModule/tileDTO.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from src.enums.tileStatus import TileStatus
-
-class TileDTO():
-
- def __init__(self, tileStatus: TileStatus, pedastrianValue: float) -> None:
- self.tileStatus = tileStatus
- self.pedastrianValue = pedastrianValue
-
- def getTileStatus(self) -> TileStatus:
- return self.tileStatus
-
- def setTileStatus(self, tileStatus: TileStatus) -> None:
- self.tileStatus = tileStatus
-
- def getPedestrianValue(self) -> float:
- return self.pedastrianValue
-
- def setPedestrianValue(self, pedastrianValue: float) -> None:
- self.pedastrianValue = pedastrianValue
\ No newline at end of file
diff --git a/src/dtoModule/tileValueDTO.py b/src/dtoModule/tileValueDTO.py
deleted file mode 100644
index 77e388a..0000000
--- a/src/dtoModule/tileValueDTO.py
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-class TileValueDTO():
-
- def __init__(self, x: int, y: int, value: float = float("inf")) -> None:
- self.x = x
- self.y = y
- self.value = value
-
- def getX(self) -> int:
- return self.x
-
- def setX(self, x) -> None:
- self.x = x
-
- def getY(self) -> int:
- return self.y
-
- def setY(self, y) -> None:
- self.y = y
\ No newline at end of file
diff --git a/src/enums/__pycache__/locomotionAlgorithms.cpython-311.pyc b/src/enums/__pycache__/locomotionAlgorithms.cpython-311.pyc
deleted file mode 100644
index 5cf892b..0000000
Binary files a/src/enums/__pycache__/locomotionAlgorithms.cpython-311.pyc and /dev/null differ
diff --git a/src/enums/__pycache__/locomotionAlgorithms.cpython-312.pyc b/src/enums/__pycache__/locomotionAlgorithms.cpython-312.pyc
deleted file mode 100644
index 3de644a..0000000
Binary files a/src/enums/__pycache__/locomotionAlgorithms.cpython-312.pyc and /dev/null differ
diff --git a/src/enums/__pycache__/tileStatus.cpython-311.pyc b/src/enums/__pycache__/tileStatus.cpython-311.pyc
deleted file mode 100644
index 6f5a5f6..0000000
Binary files a/src/enums/__pycache__/tileStatus.cpython-311.pyc and /dev/null differ
diff --git a/src/enums/__pycache__/tileStatus.cpython-312.pyc b/src/enums/__pycache__/tileStatus.cpython-312.pyc
deleted file mode 100644
index 4f305fa..0000000
Binary files a/src/enums/__pycache__/tileStatus.cpython-312.pyc and /dev/null differ
diff --git a/src/enums/locomotionAlgorithms.py b/src/enums/locomotionAlgorithms.py
deleted file mode 100644
index 00f7daa..0000000
--- a/src/enums/locomotionAlgorithms.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from enum import Enum
-
-class LocomationAlgorihms(Enum):
- dijksta = 0
\ No newline at end of file
diff --git a/src/enums/tileStatus.py b/src/enums/tileStatus.py
deleted file mode 100644
index 934528b..0000000
--- a/src/enums/tileStatus.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from enum import Enum
-
-class TileStatus(Enum):
- FREE = 1
- BLOCKED = 2
- PEDESTRIAN = 3
- SOURCE = 4
- DESTINATION = 5
\ No newline at end of file
diff --git a/src/exceptions/__pycache__/SimulationException.cpython-312.pyc b/src/exceptions/__pycache__/SimulationException.cpython-312.pyc
deleted file mode 100644
index b79d38b..0000000
Binary files a/src/exceptions/__pycache__/SimulationException.cpython-312.pyc and /dev/null differ
diff --git a/src/exceptions/__pycache__/__init__.cpython-311.pyc b/src/exceptions/__pycache__/__init__.cpython-311.pyc
deleted file mode 100644
index 07f2710..0000000
Binary files a/src/exceptions/__pycache__/__init__.cpython-311.pyc and /dev/null differ
diff --git a/src/exceptions/__pycache__/__init__.cpython-312.pyc b/src/exceptions/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 58ca203..0000000
Binary files a/src/exceptions/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/src/exceptions/__pycache__/simulationErrorCodes.cpython-311.pyc b/src/exceptions/__pycache__/simulationErrorCodes.cpython-311.pyc
deleted file mode 100644
index 8b94b7b..0000000
Binary files a/src/exceptions/__pycache__/simulationErrorCodes.cpython-311.pyc and /dev/null differ
diff --git a/src/exceptions/__pycache__/simulationErrorCodes.cpython-312.pyc b/src/exceptions/__pycache__/simulationErrorCodes.cpython-312.pyc
deleted file mode 100644
index 3e4c11b..0000000
Binary files a/src/exceptions/__pycache__/simulationErrorCodes.cpython-312.pyc and /dev/null differ
diff --git a/src/exceptions/__pycache__/simulationException.cpython-311.pyc b/src/exceptions/__pycache__/simulationException.cpython-311.pyc
deleted file mode 100644
index a987f3f..0000000
Binary files a/src/exceptions/__pycache__/simulationException.cpython-311.pyc and /dev/null differ
diff --git a/src/exceptions/simulationErrorCodes.py b/src/exceptions/simulationErrorCodes.py
deleted file mode 100644
index 97ea740..0000000
--- a/src/exceptions/simulationErrorCodes.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from enum import Enum
-from dataclasses import dataclass, field
-
-class ErrorCodeDataMixin:
- errorcode: int
- message: str
-
-class SimulationErrorCodes(ErrorCodeDataMixin, Enum):
- ExampleError = 1, "Example ErrorCode"
- NOT_IMPLEMENTED_IN_SIMULATION = 2, "A Function in the simulation is not implemented"
- LENGTH_OF_GRID_INVALID = 3, "The legth of the grid has to be more than 0 and of type int"
- POINT_NOT_ON_GRID = 4, "The requested point is outside of the simulated grid."
- PEDESTRIAN_HEAT_MAP_CALCULATION_FAILED = 5, "The calculation of the pedestrian heat map failed"
- PEDESTRIAN_HEAT_MAP_NOT_FOUND = 6, "The Pedestrian Heat Map could not be found."
-
-if __name__ == "__main__":
- example = SimulationErrorCodes.ExampleError
- print(example.value)
\ No newline at end of file
diff --git a/src/exceptions/simulationException.py b/src/exceptions/simulationException.py
deleted file mode 100644
index 93db7ad..0000000
--- a/src/exceptions/simulationException.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from src.exceptions.simulationErrorCodes import SimulationErrorCodes
-
-class SimulationException(Exception):
- def __init__(self, errorCode: SimulationErrorCodes) -> None:
- self.errorCode = errorCode
-
- def getMessage(self) -> str:
- return self.errorCode.value[1]
-
- def getCode(self) -> str:
- return self.errorCode.value[0]
\ No newline at end of file
diff --git a/src/simulation/__pycache__/baseSimulation.cpython-311.pyc b/src/simulation/__pycache__/baseSimulation.cpython-311.pyc
deleted file mode 100644
index 7529682..0000000
Binary files a/src/simulation/__pycache__/baseSimulation.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/__pycache__/baseSimulation.cpython-312.pyc b/src/simulation/__pycache__/baseSimulation.cpython-312.pyc
deleted file mode 100644
index b98c1ca..0000000
Binary files a/src/simulation/__pycache__/baseSimulation.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/__pycache__/grid.cpython-311.pyc b/src/simulation/__pycache__/grid.cpython-311.pyc
deleted file mode 100644
index 36eabd8..0000000
Binary files a/src/simulation/__pycache__/grid.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/__pycache__/grid.cpython-312.pyc b/src/simulation/__pycache__/grid.cpython-312.pyc
deleted file mode 100644
index 65ba268..0000000
Binary files a/src/simulation/__pycache__/grid.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/__pycache__/tile.cpython-311.pyc b/src/simulation/__pycache__/tile.cpython-311.pyc
deleted file mode 100644
index f43d4fd..0000000
Binary files a/src/simulation/__pycache__/tile.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/__pycache__/tile.cpython-312.pyc b/src/simulation/__pycache__/tile.cpython-312.pyc
deleted file mode 100644
index d935cdc..0000000
Binary files a/src/simulation/__pycache__/tile.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/agent/__pycache__/baseAgent.cpython-312.pyc b/src/simulation/agent/__pycache__/baseAgent.cpython-312.pyc
deleted file mode 100644
index 3c377c7..0000000
Binary files a/src/simulation/agent/__pycache__/baseAgent.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/agent/baseAgent.py b/src/simulation/agent/baseAgent.py
deleted file mode 100644
index 9fc2142..0000000
--- a/src/simulation/agent/baseAgent.py
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-class BaseAgent():
-
- def __init__(self, currentLocationX: int, currentLocationY: int, locomotionHeatMapName: str) -> None:
- self.locomotionHeatMapName = locomotionHeatMapName
- self.currentLocationX = currentLocationX
- self.currentLocationY = currentLocationY
-
- def getCurrentLocationX(self) -> int:
- return self.currentLocationX
-
- def setCurrentLocationX(self, currentLocationX: int) -> None:
- self.currentLocationX = currentLocationX
-
- def getCurrentLocationY(self) -> int:
- return self.currentLocationY
-
- def setCurrentLocationY(self, currentLocationY: int) -> None:
- self.currentLocationY
-
- def getLocomotionHeatMapName(self) -> str:
- return self.locomotionHeatMapName
-
- def setLocomotionHeatMapName(self, locomotionHeatMapName: str) -> None:
- self.locomotionHeatMapName = locomotionHeatMapName
\ No newline at end of file
diff --git a/src/simulation/baseSimulation.py b/src/simulation/baseSimulation.py
deleted file mode 100644
index ccf0c7e..0000000
--- a/src/simulation/baseSimulation.py
+++ /dev/null
@@ -1,119 +0,0 @@
-from src.enums.tileStatus import TileStatus
-from src.exceptions.simulationErrorCodes import SimulationErrorCodes
-from src.exceptions.simulationException import SimulationException
-from src.simulation.spawner.spawnerConfiguration import SpawnerConfiguration
-from src.simulation.destination.baseDestination import BaseDestination
-from src.simulation.destination.destinationConfiguration import DestinationConfiguration
-from src.simulation.spawner.baseSpawner import BaseSpawner
-from src.simulation.agent.baseAgent import BaseAgent
-from src.simulation.destination.pedestrianHeapMap import PedestrianHeapMap
-from src.simulation.grid import Grid
-from src.simulation.tile import Tile
-from src.dtoModule.tileDTO import TileDTO
-from src.config.config import _Config
-from typing import List, Tuple
-import os
-import numpy as np
-
-class BaseSimulation():
-
- def __init__(self, grid: Grid, config: _Config) -> None:
- self.grid: Grid = grid
- self.config: _Config = config
- self.agents: List[BaseAgent] = []
- self.tilesOverTime: List[List[List[int]]] = []
- self.spawners: List[BaseSpawner] = []
- self.destinations: List[BaseDestination] = []
-
- def simulateStep(self) -> None:
- self._calculateDestinationHeatMaps()
- self._spawn()
- self._step()
- self._saveStep()
-
- def setSpawner(self, spawnerConfiguration: SpawnerConfiguration) -> None:
- spawnerTiles: List[Tile] = []
- for x, y in spawnerConfiguration.getSpawnerTilesCoordinates():
- spawnerTiles.append(self.grid.getTile(x, y))
- baseSpawner: BaseSpawner = BaseSpawner(spawnerTiles, **spawnerConfiguration.getArgs())
- self.spawners.append(baseSpawner)
-
- def setDestination(self, destinationConfiguration: DestinationConfiguration) -> None:
- baseDestination: BaseDestination = BaseDestination(destinationName=destinationConfiguration.getName(), destinationTiles=destinationConfiguration.getDestinationTilesCoordination(), grid=self.grid, locomtionAlgorithm=destinationConfiguration.getLocomotionAlgorithm())
- self.destinations.append(baseDestination)
-
- def setBlockingCells(self, blockList: List[tuple]) -> None:
- for x, y in blockList:
- self.grid.setTileStatus(x, y, TileStatus.BLOCKED)
-
- def getTilesOverTime(self) -> List[List[List[Tile]]]:
- return self.tilesOverTime
-
- def save(self, name: str) -> None:
- if not os.path.exists(self.config.getSavePath()):
- os.makedirs(self.config.getSavePath())
- savePath = os.path.join(self.config.getSavePath(), f"{name}.npy")
- numpyArray = np.array(self.tilesOverTime)
- np.save(savePath, numpyArray)
-
- def _calculateDestinationHeatMaps(self) -> None:
- for destination in self.destinations:
- destination.generatePedestrianHeatMap()
-
- def _spawn(self) -> None:
- for spawner in self.spawners:
- self.agents.extend(spawner.update())
-
- def _step(self) -> None:
- for agent in self.agents:
- self._updateAgent(agent)
-
- def _updateAgent(self, agent: BaseAgent) -> None:
- pedestrianHeatMap: PedestrianHeapMap = self._getPedestrianHeatMapByName(agent.getLocomotionHeatMapName())
- targetPostion: Tuple[int, int] = self._getLowestFreePosition(agent, pedestrianHeatMap)
- if targetPostion:
- self.grid.setTileStatus(*targetPostion, TileStatus.PEDESTRIAN)
- self.grid.setTileStatus(agent.getCurrentLocationX(), agent.getCurrentLocationY(), TileStatus.FREE)
-
- agent.setCurrentLocationX(targetPostion[0])
- agent.setCurrentLocationY(targetPostion[1])
-
- def _getPedestrianHeatMapByName(self, pedestrianHeatMapName: str) -> PedestrianHeapMap:
- for destination in self.destinations:
- if destination.getDestinationName() == pedestrianHeatMapName:
- return destination.getPedestrianHeatMap() if destination.getPedestrianHeatMap() != None else destination.generatePedestrianHeatMap()
- raise SimulationException(SimulationErrorCodes.PEDESTRIAN_HEAT_MAP_NOT_FOUND)
-
- def _getLowestFreePosition(self, agent: BaseAgent, pedestrianHeatMap: PedestrianHeapMap) -> Tuple[int, int]:
- reachableFields: Tuple[int, int, float] = self._getReachableFields(agent, pedestrianHeatMap)
- reachableFields = sorted(reachableFields, key=lambda x: x[2])
- for x, y, _ in reachableFields:
- if (self.grid.getTileStatus(x, y) == TileStatus.FREE):
- return (x, y)
- return None
-
- def _getReachableFields(self, agent: BaseAgent, pedestrianHeatMap: PedestrianHeapMap) -> Tuple[int, int, float]:
- returnList: Tuple[int, int, float] = []
- for x in range(agent.getCurrentLocationX() - 1, agent.getCurrentLocationX() + 2):
- for y in range(agent.getCurrentLocationY() - 1, agent.getCurrentLocationY() + 2):
- if (pedestrianHeatMap.isOnHeatMap(x, y)):
- returnList.append((x, y, pedestrianHeatMap.getValue(x, y)))
- return returnList
-
- def _saveStep(self) -> None:
- self.tilesOverTime.append(self.grid.getTilesAsInteger())
-
- def __repr__(self) -> str:
- returnValue: str = "--- Base Simulation ---\n -- Grid --\n"
- returnValue += str(self.grid)
- returnValue += "\n -- Config --\n"
- returnValue += str(self.config)
- returnValue += "\n -- Spawners --\n"
- for spawner in self.spawners:
- returnValue += "---\n"
- returnValue += str(spawner)
- returnValue += "\n -- Destinations -- \n"
- for destination in self.destinations:
- returnValue += "---\n"
- returnValue += str(destination)
- return returnValue
\ No newline at end of file
diff --git a/src/simulation/destination/__pycache__/baseDestination.cpython-311.pyc b/src/simulation/destination/__pycache__/baseDestination.cpython-311.pyc
deleted file mode 100644
index 9990c8a..0000000
Binary files a/src/simulation/destination/__pycache__/baseDestination.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/destination/__pycache__/baseDestination.cpython-312.pyc b/src/simulation/destination/__pycache__/baseDestination.cpython-312.pyc
deleted file mode 100644
index d6022b7..0000000
Binary files a/src/simulation/destination/__pycache__/baseDestination.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/destination/__pycache__/destinationConfiguration.cpython-311.pyc b/src/simulation/destination/__pycache__/destinationConfiguration.cpython-311.pyc
deleted file mode 100644
index 0a6b186..0000000
Binary files a/src/simulation/destination/__pycache__/destinationConfiguration.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/destination/__pycache__/destinationConfiguration.cpython-312.pyc b/src/simulation/destination/__pycache__/destinationConfiguration.cpython-312.pyc
deleted file mode 100644
index 62d074f..0000000
Binary files a/src/simulation/destination/__pycache__/destinationConfiguration.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/destination/__pycache__/pedestrianHeapMap.cpython-311.pyc b/src/simulation/destination/__pycache__/pedestrianHeapMap.cpython-311.pyc
deleted file mode 100644
index 5922ad8..0000000
Binary files a/src/simulation/destination/__pycache__/pedestrianHeapMap.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/destination/__pycache__/pedestrianHeapMap.cpython-312.pyc b/src/simulation/destination/__pycache__/pedestrianHeapMap.cpython-312.pyc
deleted file mode 100644
index 60821ba..0000000
Binary files a/src/simulation/destination/__pycache__/pedestrianHeapMap.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/destination/baseDestination.py b/src/simulation/destination/baseDestination.py
deleted file mode 100644
index cafe6a7..0000000
--- a/src/simulation/destination/baseDestination.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from typing import List
-from src.simulation.destination.pedestrianHeapMap import PedestrianHeapMap
-from src.simulation.locomotionAlgorithms.dijkstra import Dijkstra
-from src.simulation.tile import Tile
-from src.simulation.grid import Grid
-from src.enums.locomotionAlgorithms import LocomationAlgorihms
-
-class BaseDestination():
- def __init__(self, destinationName: str, destinationTiles: List[tuple], grid: Grid, locomtionAlgorithm: LocomationAlgorihms) -> None:
- self.destinationName: str = destinationName
- self.destinationTiles: List[tuple] = destinationTiles
- self.grid: Grid = grid
- self.locomotionAlgorithm: LocomationAlgorihms = locomtionAlgorithm
- self.pedestrianHeatMap: PedestrianHeapMap = None
-
- def getDestinationName(self) -> str:
- return self.destinationName
-
- def generatePedestrianHeatMap(self) -> PedestrianHeapMap:
- if self.locomotionAlgorithm == LocomationAlgorihms.dijksta:
- self.pedestrianHeatMap = Dijkstra.generatePedestrianHeatMap(self.grid, self.destinationTiles)
- return self.pedestrianHeatMap
-
- def getPedestrianHeatMap(self) -> PedestrianHeapMap:
- return self.pedestrianHeatMap
-
- def __repr__(self) -> str:
- return f"DestiantionName: <{self.destinationName}>\nlocomationAlgorithm: <{self.locomotionAlgorithm}>\n-pedestrianHeatMap- \n{str(self.pedestrianHeatMap)}"
-
-
\ No newline at end of file
diff --git a/src/simulation/destination/destinationConfiguration.py b/src/simulation/destination/destinationConfiguration.py
deleted file mode 100644
index 5b9eef9..0000000
--- a/src/simulation/destination/destinationConfiguration.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from typing import List
-
-from src.enums.locomotionAlgorithms import LocomationAlgorihms
-
-
-class DestinationConfiguration():
-
- def __init__(self, name:str, destinationTilesCoordination: List[tuple], locomotionAlgorithm: LocomationAlgorihms) -> None:
- self.name = name
- self.destinationTilesCoordination: List[tuple] = destinationTilesCoordination
- self.locomotionAlgorithm = locomotionAlgorithm
-
- def getName(self) -> str:
- return self.name
-
- def getDestinationTilesCoordination(self) -> List[tuple]:
- return self.destinationTilesCoordination
-
- def getLocomotionAlgorithm(self) -> LocomationAlgorihms:
- return self.locomotionAlgorithm
\ No newline at end of file
diff --git a/src/simulation/destination/pedestrianHeapMap.py b/src/simulation/destination/pedestrianHeapMap.py
deleted file mode 100644
index 279ee43..0000000
--- a/src/simulation/destination/pedestrianHeapMap.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from typing import List
-
-
-class PedestrianHeapMap():
-
- def __init__(self, length: int, width: int) -> None:
- self.heatMap: List[List[float]] = [[float("inf") for _ in range(width)] for _ in range(length)]
-
- def updateValue(self, x: int, y: int, value: float) -> None:
- #TODO Check for bounderies
- self.heatMap[x][y] = value
-
- def isOnHeatMap(self, x: int, y: int) -> bool:
- return not (x < 0 or y < 0 or x >= len(self.heatMap) or y >= len(self.heatMap[0]))
-
- def getValue(self, x: int, y:int) -> float:
- return self.heatMap[x][y]
-
- def __repr__(self) -> str:
- returnValue = ""
- for line in self.heatMap:
- returnValue += "["
- for value in line:
- returnValue +=f"{value: .2f}, "
- returnValue += "]\n"
- return returnValue
\ No newline at end of file
diff --git a/src/simulation/grid.py b/src/simulation/grid.py
deleted file mode 100644
index 51c1ce7..0000000
--- a/src/simulation/grid.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from src.dtoModule.tileDTO import TileDTO
-from src.simulation.tile import Tile
-from src.enums.tileStatus import TileStatus
-from src.exceptions.simulationException import SimulationException
-from src.exceptions.simulationErrorCodes import SimulationErrorCodes
-from typing import List
-import copy
-
-class Grid():
-
- def __init__(self, length, width) -> None:
- self.length = length
- self.width = width
- self.tiles: List[List[Tile]] = self._initTiles()
-
- def __repr__(self) -> str:
- returnString = ""
- for line in self.tiles:
- for tile in line:
- returnString += str(tile)
- returnString += "\n"
- return returnString
-
- def getTiles(self) -> list[list]:
- return self.tiles
-
- def getLength(self) -> int:
- return self.length
-
- def getWidth(self) -> int:
- return self.width
-
- def getTile(self, x: int, y: int) -> Tile:
- if not self.contains(x, y):
- raise SimulationException(SimulationErrorCodes.POINT_NOT_ON_GRID)
- return self.tiles[x][y]
-
- def getTiles(self) -> List[List[Tile]]:
- return copy.deepcopy(self.tiles)
-
- def getTilesAsInteger(self) -> List[List[int]]:
- return copy.deepcopy([[tile.getTileStatus().value for tile in x] for x in self.tiles])
-
- def getTileStatus(self, x, y) -> TileStatus:
- tile = self.getTile(x,y)
- return tile.getTileStatus()
-
- def setTileStatus(self, x, y, tileStatus: TileStatus) -> None:
- self.getTile(x,y).changeState(tileStatus)
-
- def contains(self, x: int, y: int) -> bool:
- return (-1 < x < self.length) and (-1 < y < self.width)
-
- def _initTiles(self) -> List[List[Tile]]:
- return [
- [Tile(x, y, TileStatus.FREE) for y in range(self.width)]
- for x in range(self.length)
- ]
-
\ No newline at end of file
diff --git a/src/simulation/locomotionAlgorithms/__pycache__/dijkstra.cpython-311.pyc b/src/simulation/locomotionAlgorithms/__pycache__/dijkstra.cpython-311.pyc
deleted file mode 100644
index 29dce8c..0000000
Binary files a/src/simulation/locomotionAlgorithms/__pycache__/dijkstra.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/locomotionAlgorithms/__pycache__/dijkstra.cpython-312.pyc b/src/simulation/locomotionAlgorithms/__pycache__/dijkstra.cpython-312.pyc
deleted file mode 100644
index 6b82d86..0000000
Binary files a/src/simulation/locomotionAlgorithms/__pycache__/dijkstra.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/locomotionAlgorithms/dijkstra.py b/src/simulation/locomotionAlgorithms/dijkstra.py
deleted file mode 100644
index 6527d02..0000000
--- a/src/simulation/locomotionAlgorithms/dijkstra.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from typing import List
-from src.enums.tileStatus import TileStatus
-from src.simulation.destination.pedestrianHeapMap import PedestrianHeapMap
-from src.exceptions.simulationException import SimulationException
-from src.exceptions.simulationErrorCodes import SimulationErrorCodes
-from src.simulation.grid import Grid
-from src.simulation.tile import Tile
-
-import numpy as np
-
-class Dijkstra():
-
- def __init__(self, grid: Grid, destinationTiles: List[tuple]) -> None:
- #Dont use the constructor directly
- self.grid = grid
- self.destinationTiles = destinationTiles
- self.stack: List[tuple] = []
- self.pedestrianHeatMap: PedestrianHeapMap = PedestrianHeapMap(grid.length, grid.width)
-
- def calculatePedestrianHeapMap(self) -> PedestrianHeapMap:
- self._setStartNodes()
- while len(self.stack) > 0:
- currentNode = self._getNextNode()
- self._evaluateNode(currentNode)
- return self.pedestrianHeatMap
-
- def _setStartNodes(self):
- for x, y in self.destinationTiles:
- self.pedestrianHeatMap.updateValue(x, y, 0)
- self.stack.append((x, y, 0))
-
- def _getNextNode(self) -> tuple:
- returnTuple = min(self.stack, key=lambda x: x[2])
- self.stack.remove(returnTuple)
- return returnTuple
-
- def _evaluateNode(self, currentNode) -> None:
- for newX in range(currentNode[0] - 1, currentNode[0] + 2):
- for newY in range(currentNode[1] - 1, currentNode[1] + 2):
- if self.grid.contains(newX, newY) and self.grid.getTileStatus(newX, newY) != TileStatus.BLOCKED:
- distance = np.sqrt(2) if self._isDiagonalStep(currentNode[0], currentNode[1], newX, newY) else 1
- newValue = currentNode[2] + distance
- if (self.pedestrianHeatMap.getValue(newX, newY) > newValue):
- self.pedestrianHeatMap.updateValue(newX, newY, newValue)
- self.stack.append((newX, newY, newValue))
-
- def _isDiagonalStep(self, x:int, y:int, xNew:int, yNew: int) -> bool:
- return (x - xNew)*(y - yNew) != 0
-
-
- @classmethod
- def generatePedestrianHeatMap(cls, grid: Grid, destinationTiles: List[tuple]) -> PedestrianHeapMap:
- cls._checkInput(grid, destinationTiles)
- dijkstra = Dijkstra(grid, destinationTiles)
- return dijkstra.calculatePedestrianHeapMap()
-
- @classmethod
- def _checkInput(cls, grid: Grid, destinationTiles: List[tuple]) -> None:
- if grid == None or destinationTiles == None or len(destinationTiles) < 1:
- raise SimulationException(SimulationErrorCodes.PEDESTRIAN_HEAT_MAP_CALCULATION_FAILED)
- for x, y in destinationTiles:
- if not grid.contains(x, y):
- raise SimulationException(SimulationErrorCodes.PEDESTRIAN_HEAT_MAP_CALCULATION_FAILED)
-
\ No newline at end of file
diff --git a/src/simulation/spawner/__pycache__/baseSpawner.cpython-311.pyc b/src/simulation/spawner/__pycache__/baseSpawner.cpython-311.pyc
deleted file mode 100644
index b711435..0000000
Binary files a/src/simulation/spawner/__pycache__/baseSpawner.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/spawner/__pycache__/baseSpawner.cpython-312.pyc b/src/simulation/spawner/__pycache__/baseSpawner.cpython-312.pyc
deleted file mode 100644
index 5a76851..0000000
Binary files a/src/simulation/spawner/__pycache__/baseSpawner.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/spawner/__pycache__/spawnerConfiguration.cpython-311.pyc b/src/simulation/spawner/__pycache__/spawnerConfiguration.cpython-311.pyc
deleted file mode 100644
index 7417474..0000000
Binary files a/src/simulation/spawner/__pycache__/spawnerConfiguration.cpython-311.pyc and /dev/null differ
diff --git a/src/simulation/spawner/__pycache__/spawnerConfiguration.cpython-312.pyc b/src/simulation/spawner/__pycache__/spawnerConfiguration.cpython-312.pyc
deleted file mode 100644
index 0117efb..0000000
Binary files a/src/simulation/spawner/__pycache__/spawnerConfiguration.cpython-312.pyc and /dev/null differ
diff --git a/src/simulation/spawner/baseSpawner.py b/src/simulation/spawner/baseSpawner.py
deleted file mode 100644
index b77b214..0000000
--- a/src/simulation/spawner/baseSpawner.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from src.enums.tileStatus import TileStatus
-from src.simulation.tile import Tile
-from src.simulation.agent.baseAgent import BaseAgent
-from typing import List
-import random
-
-class BaseSpawner():
- def __init__(self, spawnerTiles: List[Tile], locomotionHeatMapName: str, numberOfTotalSpawns: float=float("inf"), maxSpawnsPerBatch:float=float("inf"), delayBeforeNextSpawn:int = 0, initialDelay:int=0) -> None:
- self.spawnerTiles = spawnerTiles
- self.locomotionHeatMapName = locomotionHeatMapName
- self.numberOfTotalSpawns = numberOfTotalSpawns
- self.maxSpawnsPerBatch = maxSpawnsPerBatch
- self.delayBeforeNextSpawn = delayBeforeNextSpawn
- self.initalDelay = initialDelay
- self.timestampsElapsedSinceLastSpawn = 0
- self.totalSpawend = 0
-
- def update(self) -> List[BaseAgent]:
- if(self.timestampsElapsedSinceLastSpawn <= self.delayBeforeNextSpawn):
- self.timestampsElapsedSinceLastSpawn = 0
- return self._spawn()
- else:
- self.timestampsElapsedSinceLastSpawn += 1
- return []
-
- def _spawn(self) -> List[BaseAgent]:
- numberOfSpawns = self._getNumberOfSpawns()
- random.shuffle(self.spawnerTiles)
- spawnedAgents = []
- currentSpawnerTilePosition = 0
- while len(spawnedAgents) < numberOfSpawns:
- if (self.spawnerTiles[currentSpawnerTilePosition].getTileStatus() == TileStatus.FREE):
- self.spawnerTiles[currentSpawnerTilePosition].changeState(TileStatus.PEDESTRIAN)
- spawnedXPosition = self.spawnerTiles[currentSpawnerTilePosition].getXPositionOnGrid()
- spawnedYPosition = self.spawnerTiles[currentSpawnerTilePosition].getYPositionOnGrid()
- spawnedAgents.append(BaseAgent(spawnedXPosition, spawnedYPosition, self.locomotionHeatMapName))
- currentSpawnerTilePosition += 1
- self.totalSpawend += len(spawnedAgents)
- return spawnedAgents
-
-
- def _getNumberOfSpawns(self) -> float:
- if(self.totalSpawend >= self.numberOfTotalSpawns):
- return 0
- return min(self.maxSpawnsPerBatch, self.numberOfTotalSpawns - self.totalSpawend, self._getNumberOfFreeTiles())
-
- def _getNumberOfFreeTiles(self) -> float:
- freeTiles: float = 0.0
- for tile in self.spawnerTiles:
- if tile.getTileStatus() == TileStatus.FREE:
- freeTiles += 1.0
- return freeTiles
-
- def __repr__(self) -> str:
- return str(self.totalSpawend)
\ No newline at end of file
diff --git a/src/simulation/spawner/spawnerConfiguration.py b/src/simulation/spawner/spawnerConfiguration.py
deleted file mode 100644
index cc7cfe8..0000000
--- a/src/simulation/spawner/spawnerConfiguration.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from typing import List
-
-class SpawnerConfiguration():
- def __init__(self,
- spawnerTilesCoordinates: List[tuple],
- locomotionHeatMapName: str,
- numberOfTotalSpawns: float=float("inf"),
- maxSpawnsPerBatch:float=float("inf"),
- delayBeforeNextSpawn:int = 0,
- initialDelay:int=0):
- self.spawnerTilesCoordinates = spawnerTilesCoordinates
- self.locomotionHeatMapName = locomotionHeatMapName
- self.numberOfTotalSpawns = numberOfTotalSpawns
- self.maxSpawnsPerBatch = maxSpawnsPerBatch
- self.delayBeforeNextSpawn = delayBeforeNextSpawn
- self.initialDelay = initialDelay
-
- def getArgs(self)-> dict:
- return {
- "numberOfTotalSpawns": self.numberOfTotalSpawns,
- "locomotionHeatMapName": self.locomotionHeatMapName,
- "maxSpawnsPerBatch": self.maxSpawnsPerBatch,
- "delayBeforeNextSpawn": self.delayBeforeNextSpawn,
- "initialDelay": self.initialDelay
- }
-
- def getSpawnerTilesCoordinates(self) -> List[tuple]:
- return self.spawnerTilesCoordinates
\ No newline at end of file
diff --git a/src/simulation/tile.py b/src/simulation/tile.py
deleted file mode 100644
index ff84e2c..0000000
--- a/src/simulation/tile.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from src.dtoModule.tileDTO import TileDTO
-from src.enums.tileStatus import TileStatus
-
-from random import uniform
-
-class Tile():
-
- def __init__(self, xPositionOnGrid: int, yPositionOnGrid: int, tileStatus: TileStatus) -> None:
- self.xPositionOnGrid = xPositionOnGrid
- self.yPositionOnGrid= yPositionOnGrid
- self.tileStatus: TileStatus = tileStatus
-
- def getTileStatus(self) -> TileStatus:
- return self.tileStatus
-
- def getXPositionOnGrid(self) -> int:
- return self.xPositionOnGrid
-
- def getYPositionOnGrid(self) -> int:
- return self.yPositionOnGrid
-
- def changeState(self, tileStatus: TileStatus) -> bool:
- # TODO make some changeChecks
- self.tileStatus = tileStatus
- return True
-
- def __repr__(self) -> str:
- if self.tileStatus == TileStatus.FREE:
- return "-"
- if self.tileStatus == TileStatus.BLOCKED:
- return "X"
- if self.tileStatus == TileStatus.PEDESTRIAN:
- return "O"
- if self.tileStatus == TileStatus.SOURCE:
- return "S"
- if self.tileStatus == TileStatus.DESTINATION:
- return "D"
- return "N"
\ No newline at end of file
diff --git a/src/userSimulation/__pycache__/exampleSimulation.cpython-311.pyc b/src/userSimulation/__pycache__/exampleSimulation.cpython-311.pyc
deleted file mode 100644
index f78e9e1..0000000
Binary files a/src/userSimulation/__pycache__/exampleSimulation.cpython-311.pyc and /dev/null differ
diff --git a/src/userSimulation/__pycache__/exampleSimulation.cpython-312.pyc b/src/userSimulation/__pycache__/exampleSimulation.cpython-312.pyc
deleted file mode 100644
index 6b49ea0..0000000
Binary files a/src/userSimulation/__pycache__/exampleSimulation.cpython-312.pyc and /dev/null differ
diff --git a/src/userSimulation/exampleSimulation.py b/src/userSimulation/exampleSimulation.py
deleted file mode 100644
index 5420745..0000000
--- a/src/userSimulation/exampleSimulation.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from src.enums.locomotionAlgorithms import LocomationAlgorihms
-from src.simulation.baseSimulation import BaseSimulation
-from src.config.config import ConfigBuilder
-from src.simulation.destination.destinationConfiguration import DestinationConfiguration
-from src.simulation.spawner.spawnerConfiguration import SpawnerConfiguration
-from src.simulation.grid import Grid
-import os
-
-def run():
- configBuilder = ConfigBuilder()
- config = configBuilder.build()
- grid = Grid(10, 10)
- baseSimulation: BaseSimulation = BaseSimulation(grid=grid, config=config)
-
- blocks = [(4,3),(5,3),(4,5),(5,4),(5,5),]
- baseSimulation.setBlockingCells(blocks)
-
- spawnerTilesCoordinates = [(0, 0), (0, 1), (1, 0), (1, 1)]
- spawnerConfiguration = SpawnerConfiguration(spawnerTilesCoordinates, "Final", 4, 4)
- baseSimulation.setSpawner(spawnerConfiguration)
-
- destinationCoordinates = [(9,9), (9,8), (9,7)]
- destinationName = "Final"
- locomotionAlgorithm = LocomationAlgorihms.dijksta
- destinationConfiguration = DestinationConfiguration(
- name=destinationName,
- destinationTilesCoordination=destinationCoordinates,
- locomotionAlgorithm=locomotionAlgorithm)
- baseSimulation.setDestination(destinationConfiguration)
-
- for i in range(20):
- baseSimulation.simulateStep()
- print(baseSimulation)
-
- baseSimulation.save("exampleSimulation")
\ No newline at end of file
diff --git a/src/visualization/__pycache__/visualization.cpython-311.pyc b/src/visualization/__pycache__/visualization.cpython-311.pyc
deleted file mode 100644
index 5a59dc9..0000000
Binary files a/src/visualization/__pycache__/visualization.cpython-311.pyc and /dev/null differ
diff --git a/src/visualization/__pycache__/visualization.cpython-312.pyc b/src/visualization/__pycache__/visualization.cpython-312.pyc
deleted file mode 100644
index d03f4d4..0000000
Binary files a/src/visualization/__pycache__/visualization.cpython-312.pyc and /dev/null differ
diff --git a/src/visualization/visualization.py b/src/visualization/visualization.py
deleted file mode 100644
index bf12851..0000000
--- a/src/visualization/visualization.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import pygame
-import sys
-from src.enums.tileStatus import TileStatus
-
-
-class Visualization:
- def __init__(self, width=800, height=600):
- pygame.init()
- self.width = width
- self.height = height
- self.screen = pygame.display.set_mode((width, height))
- self.clock = pygame.time.Clock()
- self.running = True
- self.fps = 60
-
- def handle_events(self):
- for event in pygame.event.get():
- if event.type == pygame.QUIT:
- self.running = False
- # TODO Add more event handlers
-
- def update(self):
- # TODO simulation state
- pass
-
- def draw(self):
- self.screen.fill((255, 255, 255))
- # TODO drawing code
- pygame.display.flip()
-
- def run(self):
- while self.running:
- self.handle_events()
- self.update()
- self.draw()
- self.clock.tick(self.fps)
-
- pygame.quit()
- sys.exit()
-
-if __name__ == "__main__":
- sim = Visualization()
- sim.run()
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
index b5ee836..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1 +0,0 @@
-import src
\ No newline at end of file
diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index e907c2d..0000000
Binary files a/tests/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/tests/__pycache__/baseSimulationTest.cpython-312.pyc b/tests/__pycache__/baseSimulationTest.cpython-312.pyc
deleted file mode 100644
index ccac076..0000000
Binary files a/tests/__pycache__/baseSimulationTest.cpython-312.pyc and /dev/null differ
diff --git a/tests/config/__pycache__/__init__.cpython-312.pyc b/tests/config/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 38872c6..0000000
Binary files a/tests/config/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/tests/config/__pycache__/configTest.cpython-312.pyc b/tests/config/__pycache__/configTest.cpython-312.pyc
deleted file mode 100644
index 7531890..0000000
Binary files a/tests/config/__pycache__/configTest.cpython-312.pyc and /dev/null differ
diff --git a/tests/config/configTest.py b/tests/config/configTest.py
deleted file mode 100644
index 37db815..0000000
--- a/tests/config/configTest.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import unittest
-from src.config.config import ConfigBuilder
-from src.config.config import _Config
-
-class ConfigBuilderTest(unittest.TestCase):
-
- def test_build_returnsDefaultConfig(self):
- # ARRANGE
- configBuilder: ConfigBuilder = ConfigBuilder()
- expectedMaxItterations: int = _Config._DEFAULT_MAX_NUMBER_OF_ITTERATIONS
- expectedPath: str =_Config()._getStandartPath()
-
- # ACT
- config: _Config = configBuilder.build()
-
- # ASSERT
- self.assertEqual(expectedMaxItterations, config.getMaxItterations())
- self.assertEqual(expectedPath, config.getSavePath())
-
- def test_setSavePath_hasNewPath(self):
- # ARRANGE
- configBuilder: ConfigBuilder = ConfigBuilder()
- expectedMaxItterations: int = _Config._DEFAULT_MAX_NUMBER_OF_ITTERATIONS
- expectedPath: str = "AnOtherPath"
-
- # ACT
- config: _Config = configBuilder.setSavePath(expectedPath).build()
-
- # ASSERT
- self.assertEqual(expectedMaxItterations, config.getMaxItterations())
- self.assertEqual(expectedPath, config.getSavePath())
-
- def test_setMaxItterations_hasNewMaxItterations(self):
- # ARRANGE
- configBuilder: ConfigBuilder = ConfigBuilder()
- expectedMaxItterations: int = 20
- expectedPath: str =_Config()._getStandartPath()
-
- # ACT
- config: _Config = configBuilder.setNumberOfMaxItterations(expectedMaxItterations).build()
-
- # ASSERT
- self.assertEqual(expectedMaxItterations, config.getMaxItterations())
- self.assertEqual(expectedPath, config.getSavePath())
-
-
-if __name__ == "__main__":
- unittest.main()
\ No newline at end of file
diff --git a/tests/core_tests/test_cell.py b/tests/core_tests/test_cell.py
new file mode 100644
index 0000000..b97d0db
--- /dev/null
+++ b/tests/core_tests/test_cell.py
@@ -0,0 +1,88 @@
+import unittest
+from unittest.mock import Mock
+from simulation.core.cell import Cell
+from simulation.core.cell_state import CellState
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+
+class TestCell(unittest.TestCase):
+ def setUp(self):
+ self.cell = Cell(1, 2)
+
+ def test_initialization(self):
+ """Tests the initialization of the cell with the default state."""
+ self.assertEqual(self.cell.get_state(), CellState.FREE)
+ self.assertIsNone(self.cell.get_pedestrian())
+
+ def test_set_obstacle(self):
+ """Tests setting the cell to OBSTACLE."""
+ self.cell.set_osbtacle()
+ self.assertEqual(self.cell.get_state(), CellState.OBSTACLE)
+
+ def test_set_pedestrian_success(self):
+ """Tests successfully placing a pedestrian in a free cell."""
+ pedestrian = Mock()
+ self.cell.set_pedestrian(pedestrian)
+ self.assertEqual(self.cell.get_state(), CellState.OCCUPIED)
+ self.assertEqual(self.cell.get_pedestrian(), pedestrian)
+
+ def test_set_pedestrian_obstacle(self):
+ """Tests that placing a pedestrian in an obstacle cell raises an exception."""
+ self.cell.set_osbtacle()
+ pedestrian = Mock()
+ with self.assertRaises(SimulationError) as context:
+ self.cell.set_pedestrian(pedestrian)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.CELL_BLOCKED.value[0])
+
+ def test_eq_method(self):
+ """Tests the equality method of the cell."""
+ other_cell = Cell(1, 2)
+ different_cell = Cell(2, 3)
+ self.assertEqual(self.cell, other_cell)
+ self.assertNotEqual(self.cell, different_cell)
+
+ def test_hash_method(self):
+ """Tests the hash method of the cell."""
+ cell_set = {self.cell, Cell(1, 2), Cell(2, 3)}
+ self.assertIn(Cell(1, 2), cell_set)
+ self.assertIn(Cell(2, 3), cell_set)
+ self.assertEqual(len(cell_set), 2)
+
+ def test_remove_pedestrian_success(self):
+ """Tests successfully removing a pedestrian from an occupied cell."""
+ pedestrian = Mock()
+ self.cell.set_pedestrian(pedestrian)
+ self.cell.remove_pedestrian()
+ self.assertEqual(self.cell.get_state(), CellState.FREE)
+ self.assertIsNone(self.cell.get_pedestrian())
+
+ def test_remove_pedestrian_not_occupied(self):
+ """Tests that removing a pedestrian from a free cell raises an exception."""
+ with self.assertRaises(SimulationError) as context:
+ self.cell.remove_pedestrian()
+
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.CELL_NOT_OCCUPIED.value[0])
+
+ def test_is_free(self):
+ """Tests the is_free method."""
+ self.assertTrue(self.cell.is_free())
+ pedestrian = Mock()
+ self.cell.set_pedestrian(pedestrian)
+ self.assertFalse(self.cell.is_free())
+
+ def test_is_occupied(self):
+ """Tests the is_occupied method."""
+ self.assertFalse(self.cell.is_occupied())
+ pedestrian = Mock()
+ self.cell.set_pedestrian(pedestrian)
+ self.assertTrue(self.cell.is_occupied())
+
+ def test_get_pedestrian(self):
+ """Tests the get_pedestrian method."""
+ self.assertIsNone(self.cell.get_pedestrian())
+ pedestrian = Mock()
+ self.cell.set_pedestrian(pedestrian)
+ self.assertEqual(self.cell.get_pedestrian(), pedestrian)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/core_tests/test_cell_state.py b/tests/core_tests/test_cell_state.py
new file mode 100644
index 0000000..f104da7
--- /dev/null
+++ b/tests/core_tests/test_cell_state.py
@@ -0,0 +1,30 @@
+import unittest
+from simulation.core.cell_state import CellState
+
+class TestCellState(unittest.TestCase):
+ def test_cell_state_values_exist(self):
+ """Test that all expected CellState values exist"""
+ self.assertTrue(hasattr(CellState, 'FREE'))
+ self.assertTrue(hasattr(CellState, 'OCCUPIED'))
+ self.assertTrue(hasattr(CellState, 'OBSTACLE'))
+
+ def test_cell_state_values(self):
+ """Test that CellState values are unique"""
+ self.assertNotEqual(CellState.FREE, CellState.OCCUPIED)
+ self.assertNotEqual(CellState.FREE, CellState.OBSTACLE)
+ self.assertNotEqual(CellState.OCCUPIED, CellState.OBSTACLE)
+
+ def test_cell_state_type(self):
+ """Test that CellState is an Enum"""
+ self.assertTrue(isinstance(CellState.FREE, CellState))
+ self.assertTrue(isinstance(CellState.OCCUPIED, CellState))
+ self.assertTrue(isinstance(CellState.OBSTACLE, CellState))
+
+ def test_cell_state_comparison(self):
+ """Test that CellState values can be compared"""
+ states = [CellState.FREE, CellState.OCCUPIED, CellState.OBSTACLE]
+ for state in states:
+ self.assertEqual(state, state)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/core_tests/test_grid_base.py b/tests/core_tests/test_grid_base.py
new file mode 100644
index 0000000..0988603
--- /dev/null
+++ b/tests/core_tests/test_grid_base.py
@@ -0,0 +1,114 @@
+import unittest
+from typing import TypeVar
+from abc import ABC
+
+from simulation.core.grid_base import GridBase
+from simulation.core.cell import Cell
+from simulation.core.position import Position
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+
+T = TypeVar('T')
+
+class MockGrid(GridBase[Cell], ABC):
+ def __init__(self, width: int, height: int):
+ self._width = width
+ self._height = height
+ self.cells = [self._init_cell(x, y) for y in range(height) for x in range(width)]
+
+ def _init_cell(self, x: int, y: int) -> Cell:
+ return Cell(x, y)
+
+ def _get_cell_index(self, x: int, y: int) -> int:
+ return y * self._width + x
+
+class TestGridBase(unittest.TestCase):
+ def setUp(self):
+ self.width = 5
+ self.height = 5
+ self.grid = MockGrid(self.width, self.height)
+
+ def test_is_in_bounds_true(self):
+ """Tests whether is_in_bounds returns True for coordinates within bounds."""
+ self.assertTrue(self.grid.is_in_bounds(0, 0))
+ self.assertTrue(self.grid.is_in_bounds(4, 4))
+ self.assertTrue(self.grid.is_in_bounds(2, 3))
+
+ def test_is_in_bounds_false(self):
+ """Tests whether is_in_bounds returns False for coordinates outside the bounds."""
+ self.assertFalse(self.grid.is_in_bounds(-1, 0))
+ self.assertFalse(self.grid.is_in_bounds(0, -1))
+ self.assertFalse(self.grid.is_in_bounds(5, 5))
+ self.assertFalse(self.grid.is_in_bounds(6, 2))
+
+ def test_check_bounds_inside(self):
+ """Tests that check_bounds does not raise an exception for valid coordinates."""
+ try:
+ self.grid.check_bounds(2, 2)
+ except SimulationError:
+ self.fail("check_bounds raised a SimulationError for valid coordinates.")
+
+ def test_check_bounds_outside(self):
+ """Tests that check_bounds raises a SimulationError for invalid coordinates."""
+ with self.assertRaises(SimulationError) as context:
+ self.grid.check_bounds(5, 5)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.INVALID_COORDINATES.value[0])
+ self.assertEqual(context.exception._context, {"x": 5, "y": 5})
+
+ def test_get_cell_valid(self):
+ """Tests whether get_cell returns the correct cell for valid coordinates."""
+ cell = self.grid.get_cell(1, 1)
+ self.assertIsInstance(cell, Cell)
+ self.assertEqual(cell.get_x(), 1)
+ self.assertEqual(cell.get_y(), 1)
+
+ def test_get_cell_invalid(self):
+ """Tests whether get_cell raises a SimulationError for invalid coordinates."""
+ with self.assertRaises(SimulationError) as context:
+ self.grid.get_cell(-1, 0)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.INVALID_COORDINATES.value[0])
+
+ def test_get_cell_at_pos_valid(self):
+ """Tests whether get_cell_at_pos returns the correct cell for a valid position."""
+ pos = Position(3, 3)
+ cell = self.grid.get_cell_at_pos(pos)
+ self.assertIsInstance(cell, Cell)
+ self.assertEqual(cell.get_x(), 3)
+ self.assertEqual(cell.get_y(), 3)
+
+ def test_get_cell_at_pos_invalid(self):
+ """Tests whether get_cell_at_pos raises a SimulationError for an invalid position."""
+ pos = Position(5, 5)
+ with self.assertRaises(SimulationError) as context:
+ self.grid.get_cell_at_pos(pos)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.INVALID_COORDINATES.value[0])
+
+ def test_contains_with_cell(self):
+ """Tests the __contains__ method with a Cell."""
+ cell = self.grid.get_cell(2, 2)
+ self.assertIn(cell, self.grid)
+ other_cell = Cell(2, 2)
+ self.assertIn(other_cell, self.grid)
+ non_existent_cell = Cell(5, 5)
+ self.assertNotIn(non_existent_cell, self.grid)
+
+ def test_contains_with_position(self):
+ """Tests the __contains__ method with a Position."""
+ pos_inside = Position(1, 1)
+ pos_outside = Position(5, 5)
+ self.assertIn(pos_inside, self.grid)
+ self.assertNotIn(pos_outside, self.grid)
+
+ def test_contains_with_tuple(self):
+ """Tests the __contains__ method with a tuple."""
+ self.assertIn((0, 0), self.grid)
+ self.assertIn((4, 4), self.grid)
+ self.assertNotIn((5, 5), self.grid)
+
+ def test_contains_with_invalid_type(self):
+ """Tests the __contains__ method with an invalid type."""
+ self.assertNotIn("invalid", self.grid)
+ self.assertNotIn(123, self.grid)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/core_tests/test_pedestrian.py b/tests/core_tests/test_pedestrian.py
new file mode 100644
index 0000000..38f61e9
--- /dev/null
+++ b/tests/core_tests/test_pedestrian.py
@@ -0,0 +1,118 @@
+import unittest
+from unittest.mock import Mock
+from simulation.core.pedestrian import Pedestrian
+from simulation.core.position import Position
+from simulation.core.cell import Cell
+from simulation.core.target import Target
+from simulation.core.spawner import Spawner
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+
+class TestPedestrian(unittest.TestCase):
+ def setUp(self):
+ self.distancing = Mock(spec=DistanceBase)
+ self.distancing.calculate_distance.return_value = 1.0
+
+ self.spawner = Mock(spec=Spawner)
+ self.target = Mock(spec=Target)
+ self.target.is_inside_target.return_value = False
+
+ self.real_cell = Cell(1, 2)
+ self.cell = Mock(spec=Cell, wraps=self.real_cell)
+ self.cell.is_free.return_value = True
+
+ self.pedestrian = Pedestrian(
+ x=0,
+ y=0,
+ speed=1.5,
+ spawner=self.spawner,
+ target=self.target,
+ distancing=self.distancing
+ )
+
+ def test_initialization(self):
+ """Tests the correct initialization of a Pedestrian."""
+ self.assertEqual(self.pedestrian.get_x(), 0)
+ self.assertEqual(self.pedestrian.get_y(), 0)
+ self.assertEqual(self.pedestrian.get_optimal_speed(), 1.5)
+ self.assertEqual(self.pedestrian.get_current_distance(), float('inf'))
+ self.assertFalse(self.pedestrian.has_reached_target())
+
+ def test_set_reached_target(self):
+ """Tests the methods set_reached_target and has_reached_target."""
+ self.pedestrian.set_reached_target()
+ self.assertTrue(self.pedestrian.has_reached_target())
+
+ def test_set_target_cell_success(self):
+ """Tests successful setting of a target cell."""
+ self.pedestrian.set_target_cell(self.cell)
+ self.assertEqual(self.pedestrian.get_targeted_cell(), self.cell)
+ self.assertEqual(self.pedestrian.get_current_distance(), 1.0)
+
+ def test_set_target_cell_none(self):
+ """Tests setting the target cell to None."""
+ self.pedestrian.set_target_cell(None)
+ self.assertIsNone(self.pedestrian.get_targeted_cell())
+ self.assertEqual(self.pedestrian.get_current_distance(), float('-inf'))
+
+ def test_set_target_cell_already_in_cell(self):
+ """Tests that an exception is raised when the target cell is the current position."""
+ cell = Cell(0, 0)
+ with self.assertRaises(SimulationError) as context:
+ self.pedestrian.set_target_cell(cell)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.ALREADY_IN_CELL.value[0])
+
+ def test_can_move_true(self):
+ """Tests that can_move returns True when the pedestrian can move."""
+ self.pedestrian.set_target_cell(self.cell)
+ self.pedestrian._current_distance = -0.1 # Simulate time has passed
+ self.cell.is_free.return_value = True
+ self.assertTrue(self.pedestrian.can_move())
+
+ def test_can_move_false(self):
+ """Tests that can_move returns False when the pedestrian cannot move."""
+ self.pedestrian.set_target_cell(self.cell)
+ self.pedestrian._current_distance = 0.5
+ self.cell.is_free.return_value = True
+ self.assertFalse(self.pedestrian.can_move())
+
+ def test_move_success(self):
+ """Tests successful movement of the pedestrian."""
+ self.pedestrian.set_target_cell(self.cell)
+ self.pedestrian._current_distance = -0.1
+ self.cell.is_free.return_value = True
+ self.pedestrian.move()
+ self.assertEqual(self.pedestrian.get_x(), 1)
+ self.assertEqual(self.pedestrian.get_y(), 2)
+ self.assertEqual(self.pedestrian._total_distance_moved, 1.0)
+ self.assertEqual(self.pedestrian._distance_to_target, float('inf'))
+
+ def test_move_failure(self):
+ """Tests that an exception is raised when the pedestrian cannot move."""
+ self.pedestrian.set_target_cell(self.cell)
+ self.pedestrian._current_distance = 0.5
+ with self.assertRaises(SimulationError) as context:
+ self.pedestrian.move()
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.CANNOT_MOVE.value[0])
+
+ def test_update(self):
+ """Tests the update method."""
+ self.pedestrian._current_distance = 1.0
+ self.pedestrian.update(0.5)
+ self.assertEqual(self.pedestrian._current_distance, 0.25)
+ self.assertEqual(self.pedestrian._time_alive, 0.5)
+
+ def test_is_inside_target(self):
+ """Tests the is_inside_target method."""
+ self.target.is_inside_target.return_value = True
+ self.assertTrue(self.pedestrian.is_inside_target())
+
+ def test_getter_methods(self):
+ """Tests the getter methods."""
+ self.assertEqual(self.pedestrian.get_spawner(), self.spawner)
+ self.assertEqual(self.pedestrian.get_target(), self.target)
+ self.assertIsNotNone(self.pedestrian.get_id())
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/core_tests/test_position.py b/tests/core_tests/test_position.py
new file mode 100644
index 0000000..2ec0026
--- /dev/null
+++ b/tests/core_tests/test_position.py
@@ -0,0 +1,41 @@
+import unittest
+from simulation.core.position import Position
+
+class ConcretePosition(Position):
+ pass
+
+class TestPosition(unittest.TestCase):
+ def setUp(self):
+ self.position = ConcretePosition(1, 2)
+ self.same_position = ConcretePosition(1, 2)
+ self.different_position = ConcretePosition(3, 4)
+
+ def test_initialization(self):
+ """Tests initialization and getters."""
+ self.assertEqual(self.position.get_x(), 1)
+ self.assertEqual(self.position.get_y(), 2)
+ self.assertEqual(self.position.as_tuple(), (1, 2))
+
+ def test_equality(self):
+ """Tests the __eq__ and __ne__ methods."""
+ self.assertEqual(self.position, self.same_position)
+ self.assertNotEqual(self.position, self.different_position)
+ self.assertFalse(self.position == (1, 2))
+
+ def test_equals_method(self):
+ """Tests the equals method."""
+ self.assertTrue(self.position.pos_equals(self.same_position))
+ self.assertFalse(self.position.pos_equals(self.different_position))
+ self.assertFalse(self.position.pos_equals((1, 2)))
+
+ def test_hash(self):
+ """Tests the __hash__ method."""
+ self.assertEqual(hash(self.position), hash(self.same_position))
+ self.assertNotEqual(hash(self.position), hash(self.different_position))
+
+ def test_str(self):
+ """Tests the __str__ method."""
+ self.assertEqual(str(self.position), "(1, 2)")
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/core_tests/test_simulation_grid.py b/tests/core_tests/test_simulation_grid.py
new file mode 100644
index 0000000..1fc4c1d
--- /dev/null
+++ b/tests/core_tests/test_simulation_grid.py
@@ -0,0 +1,85 @@
+import unittest
+from unittest.mock import Mock
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.core.cell import Cell
+from simulation.core.position import Position
+from simulation.neighbourhood.base_neighbourhood import NeighbourhoodBase
+from exceptions.simulation_error import SimulationError
+from exceptions.simulation_error_codes import SimulationErrorCode
+
+class MockNeighbourhood(NeighbourhoodBase):
+ def get_neighbours(self, x, y, width=1, height=1):
+ # Return a fixed set of neighbour coordinates for testing
+ neighbours = []
+ if x > 0:
+ neighbours.append((x - 1, y))
+ if x < self._width - 1:
+ neighbours.append((x + 1, y))
+ if y > 0:
+ neighbours.append((x, y - 1))
+ if y < self._height - 1:
+ neighbours.append((x, y + 1))
+ return neighbours
+
+class TestSimulationGrid(unittest.TestCase):
+ def setUp(self):
+ self.width = 5
+ self.height = 5
+ self.grid = SimulationGrid(self.width, self.height, MockNeighbourhood)
+
+ def test_initialization(self):
+ """Tests grid initialization."""
+ self.assertEqual(self.grid.get_width(), self.width)
+ self.assertEqual(self.grid.get_height(), self.height)
+ cells = self.grid.get_cells()
+ self.assertEqual(len(cells), self.width * self.height)
+ for cell in cells:
+ self.assertIsInstance(cell, Cell)
+
+ def test_get_cell_valid(self):
+ """Tests get_cell with valid coordinates."""
+ cell = self.grid.get_cell(2, 2)
+ self.assertEqual(cell.get_x(), 2)
+ self.assertEqual(cell.get_y(), 2)
+
+ def test_get_cell_invalid(self):
+ """Tests get_cell with invalid coordinates."""
+ with self.assertRaises(SimulationError) as context:
+ self.grid.get_cell(-1, 0)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.INVALID_COORDINATES.value[0])
+
+ def test_get_cell_at_pos_valid(self):
+ """Tests get_cell_at_pos with a valid position."""
+ pos = Position(1, 1)
+ cell = self.grid.get_cell_at_pos(pos)
+ self.assertEqual(cell.get_x(), 1)
+ self.assertEqual(cell.get_y(), 1)
+
+ def test_get_cell_at_pos_invalid(self):
+ """Tests get_cell_at_pos with an invalid position."""
+ pos = Position(5, 5)
+ with self.assertRaises(SimulationError) as context:
+ self.grid.get_cell_at_pos(pos)
+ self.assertEqual(context.exception.get_code(), SimulationErrorCode.INVALID_COORDINATES.value[0])
+
+ def test_get_neighbours(self):
+ """Tests get_neighbours method."""
+ neighbours = list(self.grid.get_neighbours(2, 2))
+ expected_positions = [(1, 2), (3, 2), (2, 1), (2, 3)]
+ self.assertEqual(len(neighbours), len(expected_positions))
+ for neighbour in neighbours:
+ self.assertIsInstance(neighbour, Cell)
+ self.assertIn((neighbour.get_x(), neighbour.get_y()), expected_positions)
+
+ def test_get_neighbours_at(self):
+ """Tests get_neighbours_at method."""
+ pos = Position(2, 2)
+ neighbours = list(self.grid.get_neighbours_at(pos))
+ expected_positions = [(1, 2), (3, 2), (2, 1), (2, 3)]
+ self.assertEqual(len(neighbours), len(expected_positions))
+ for neighbour in neighbours:
+ self.assertIsInstance(neighbour, Cell)
+ self.assertIn((neighbour.get_x(), neighbour.get_y()), expected_positions)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/core_tests/test_spawner.py b/tests/core_tests/test_spawner.py
new file mode 100644
index 0000000..eb8bbc8
--- /dev/null
+++ b/tests/core_tests/test_spawner.py
@@ -0,0 +1,100 @@
+import unittest
+from unittest.mock import Mock, patch
+from simulation.core.spawner import Spawner
+from simulation.core.cell import Cell
+from simulation.core.target import Target
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from utils.immutable_list import ImmutableList
+
+class TestSpawner(unittest.TestCase):
+ def setUp(self):
+ self.cells = [Mock(spec=Cell) for _ in range(5)]
+ self.targets = [Mock(spec=Target) for _ in range(2)]
+ self.distancing = Mock(spec=DistanceBase)
+ self.spawner = Spawner(
+ name="TestSpawner",
+ distancing=self.distancing,
+ cells=self.cells,
+ targets=self.targets,
+ total_spawns=10,
+ batch_size=2,
+ spawn_delay=1.0,
+ initial_delay=0.0
+ )
+
+ def test_initialization(self):
+ """Test spawner initialization."""
+ self.assertEqual(self.spawner.get_name(), "TestSpawner")
+ self.assertEqual(self.spawner.get_cells()._data, self.cells)
+ self.assertEqual(self.spawner._total_spawns, 10)
+ self.assertEqual(self.spawner._batch_size, 2)
+ self.assertEqual(self.spawner._spawn_delay, 1.0)
+ self.assertEqual(self.spawner._current_delay, 0.0)
+ self.assertEqual(self.spawner._distancing, self.distancing)
+ self.assertEqual(self.spawner._targets, self.targets)
+
+ def test_get_name(self):
+ """Test get_name method."""
+ self.assertEqual(self.spawner.get_name(), "TestSpawner")
+
+ def test_get_cells(self):
+ """Test get_cells method."""
+ self.assertEqual(list(self.spawner.get_cells()), self.cells)
+
+ def test_can_spawn_initial(self):
+ """Test can_spawn initially."""
+ self.assertTrue(self.spawner.can_spawn())
+
+ def test_is_done_initial(self):
+ """Test is_done initially."""
+ self.assertFalse(self.spawner.is_done())
+
+ def test_can_spawn_after_delay(self):
+ """Test can_spawn after increasing delay."""
+ self.spawner._current_delay = 1.0
+ self.assertFalse(self.spawner.can_spawn())
+ self.spawner._current_delay = 0.0
+ self.assertTrue(self.spawner.can_spawn())
+
+ def test_is_done_after_spawns(self):
+ """Test is_done after all spawns are done."""
+ self.spawner._total_spawns = 0
+ self.assertTrue(self.spawner.is_done())
+
+ def test_update_spawning(self):
+ """Test update method when spawning occurs."""
+ pedestrians = list(self.spawner.update(1.0))
+ self.assertEqual(len(pedestrians), 2)
+ self.assertEqual(self.spawner._current_delay, 1.0)
+ self.assertEqual(self.spawner._total_spawns, 8)
+
+ @patch.object(Spawner, 'spawn')
+ def test_update_no_spawn_due_to_delay(self, mock_spawn):
+ """Test update method when spawn delay hasn't elapsed."""
+ self.spawner._current_delay = 0.5
+ pedestrians = list(self.spawner.update(0.4))
+ mock_spawn.assert_not_called()
+ self.assertEqual(pedestrians, [])
+ # compare with .5 - .4 = because of floating point error (0.0999999999999998)
+ self.assertEqual(self.spawner._current_delay, 0.5 - 0.4)
+
+ @patch.object(Spawner, 'spawn')
+ def test_update_no_spawn_due_to_total_spawns(self, mock_spawn):
+ """Test update method when total_spawns reached zero."""
+ self.spawner._total_spawns = 0
+ pedestrians = list(self.spawner.update(1.0))
+ mock_spawn.assert_not_called()
+ self.assertEqual(pedestrians, [])
+ self.assertTrue(self.spawner.is_done())
+
+ def test_update_with_none_total_spawns(self):
+ """Test update when total_spawns is None (infinite spawns)."""
+ self.spawner._total_spawns = None
+ with patch.object(Spawner, 'spawn', return_value=[Mock(), Mock()]) as mock_spawn:
+ pedestrians = list(self.spawner.update(1.0))
+ mock_spawn.assert_called_once()
+ self.assertEqual(len(pedestrians), 2)
+ self.assertIsNone(self.spawner._total_spawns)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/core_tests/test_target.py b/tests/core_tests/test_target.py
new file mode 100644
index 0000000..1714903
--- /dev/null
+++ b/tests/core_tests/test_target.py
@@ -0,0 +1,90 @@
+import unittest
+from unittest.mock import Mock
+
+from simulation.core.cell_state import CellState
+from simulation.core.target import Target
+from simulation.core.cell import Cell
+from simulation.core.position import Position
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap_generator_base import HeatmapGeneratorBase
+from simulation.heatmaps.heatmap import Heatmap
+from utils.immutable_list import ImmutableList
+
+class TestTarget(unittest.TestCase):
+ def setUp(self):
+ self.name = "TestTarget"
+ self.cells = [Mock(spec=Cell) for _ in range(3)]
+ self.grid = Mock(spec=SimulationGrid)
+ self.heatmap_generator = Mock(spec=HeatmapGeneratorBase)
+ self.heatmap = Mock(spec=Heatmap)
+
+ self.heatmap_generator.generate_heatmap.return_value = self.heatmap
+ self.heatmap_generator.get_blocked.return_value = {CellState.OCCUPIED, CellState.OBSTACLE}
+
+ self.target = Target(
+ name=self.name,
+ cells=self.cells,
+ grid=self.grid,
+ heatmap_generator=self.heatmap_generator
+ )
+
+ def test_initialization(self):
+ """Tests the initialization of the Target."""
+ self.assertEqual(self.target.get_name(), self.name)
+ self.assertEqual(list(self.target.get_cells()), self.cells)
+ self.assertEqual(self.target._grid, self.grid)
+ self.assertEqual(self.target._heatmap_generator, self.heatmap_generator)
+ self.assertIsNone(self.target._heatmap)
+ self.assertEqual(self.target._exit_count, 0)
+
+ def test_get_name(self):
+ """Tests the get_name method."""
+ self.assertEqual(self.target.get_name(), self.name)
+
+ def test_get_cells(self):
+ """Tests the get_cells method."""
+ cells = self.target.get_cells()
+ self.assertIsInstance(cells, ImmutableList)
+ self.assertEqual(list(cells), self.cells)
+
+ def test_generate_heatmap(self):
+ """Tests the generate_heatmap method."""
+ self.target.update_heatmap()
+ self.heatmap_generator.generate_heatmap.assert_called_once_with(self.cells, self.grid)
+ self.assertEqual(self.target.get_heatmap(), self.heatmap)
+
+ def test_get_heatmap_without_generation(self):
+ """Tests get_heatmap method before heatmap is generated."""
+ with self.assertRaises(Exception):
+ self.target.get_heatmap()
+
+ def test_is_inside_target(self):
+ """Tests the is_inside_target method."""
+ positions = [Position(x, y) for x, y in [(1, 1), (2, 2), (3, 3)]]
+ for cell, pos in zip(self.cells, positions):
+ cell.get_x.return_value = pos.get_x()
+ cell.get_y.return_value = pos.get_y()
+ cell.as_tuple.return_value = (pos.get_x(), pos.get_y())
+
+ position_inside = positions[0]
+ position_outside = Position(4, 4)
+
+ self.assertTrue(self.target.is_inside_target(position_inside))
+ self.assertFalse(self.target.is_inside_target(position_outside))
+
+ def test_get_exit_count(self):
+ """Tests the get_exit_count method."""
+ self.assertEqual(self.target.get_exit_count(), 0)
+ self.target._exit_count = 5
+ self.assertEqual(self.target.get_exit_count(), 5)
+
+ def test_increment_exit_count(self):
+ """Tests the increment_exit_count method."""
+ self.assertEqual(self.target.get_exit_count(), 0)
+ self.target.increment_exit_count()
+ self.assertEqual(self.target.get_exit_count(), 1)
+ self.target.increment_exit_count()
+ self.assertEqual(self.target.get_exit_count(), 2)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/exceptions/__pycache__/__init__.cpython-312.pyc b/tests/exceptions/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 16d7bdf..0000000
Binary files a/tests/exceptions/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/tests/exceptions/__pycache__/simulationExceptionTest.cpython-312.pyc b/tests/exceptions/__pycache__/simulationExceptionTest.cpython-312.pyc
deleted file mode 100644
index 7923751..0000000
Binary files a/tests/exceptions/__pycache__/simulationExceptionTest.cpython-312.pyc and /dev/null differ
diff --git a/tests/exceptions/__pycache__/testModul.cpython-312.pyc b/tests/exceptions/__pycache__/testModul.cpython-312.pyc
deleted file mode 100644
index c0c9b33..0000000
Binary files a/tests/exceptions/__pycache__/testModul.cpython-312.pyc and /dev/null differ
diff --git a/tests/exceptions/simulationExceptionTest.py b/tests/exceptions/simulationExceptionTest.py
deleted file mode 100644
index 77f0b5d..0000000
--- a/tests/exceptions/simulationExceptionTest.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import unittest
-
-from src.exceptions.simulationException import SimulationException
-from src.exceptions.simulationErrorCodes import SimulationErrorCodes
-
-class SimulationExceptionTest(unittest.TestCase):
-
- def test_setsMessage(self):
- # ARRANGE
- simulationException = SimulationException(SimulationErrorCodes.ExampleError)
-
- # ACT / ASSERT
- self.assertEqual("Example ErrorCode", simulationException.getMessage())
- self.assertEqual(1, simulationException.getCode())
-
-if __name__ == "__main__":
- unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/distancing_tests/test_euclidean_distance.py b/tests/heatmaps_tests/distancing_tests/test_euclidean_distance.py
new file mode 100644
index 0000000..d881418
--- /dev/null
+++ b/tests/heatmaps_tests/distancing_tests/test_euclidean_distance.py
@@ -0,0 +1,21 @@
+import unittest
+from simulation.heatmaps.distancing.euclidean_distance import EuclideanDistance
+from simulation.core.position import Position
+
+class TestEuclideanDistance(unittest.TestCase):
+ def setUp(self):
+ self.distance = EuclideanDistance(scale=2.0)
+ self.pos1 = Position(0, 0)
+ self.pos2 = Position(3, 4)
+
+ def test_initialization(self):
+ """Tests initialization and get_scale method."""
+ self.assertEqual(self.distance.get_scale(), 2.0)
+
+ def test_calculate_distance(self):
+ """Tests the calculate_distance method."""
+ expected_distance = 5.0 * 2.0
+ self.assertEqual(self.distance.calculate_distance(self.pos1, self.pos2), expected_distance)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/distancing_tests/test_taxi_distance.py b/tests/heatmaps_tests/distancing_tests/test_taxi_distance.py
new file mode 100644
index 0000000..f13fc08
--- /dev/null
+++ b/tests/heatmaps_tests/distancing_tests/test_taxi_distance.py
@@ -0,0 +1,21 @@
+import unittest
+from simulation.heatmaps.distancing.taxi_distance import TaxiDistance
+from simulation.core.position import Position
+
+class TestTaxiDistance(unittest.TestCase):
+ def setUp(self):
+ self.distance = TaxiDistance(scale=2.0)
+ self.pos1 = Position(0, 0)
+ self.pos2 = Position(3, 4)
+
+ def test_initialization(self):
+ """Tests initialization and get_scale method."""
+ self.assertEqual(self.distance.get_scale(), 2.0)
+
+ def test_calculate_distance(self):
+ """Tests the calculate_distance method."""
+ expected_distance = (3 + 4) * 2.0
+ self.assertEqual(self.distance.calculate_distance(self.pos1, self.pos2), expected_distance)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/neighbourhood_tests/test_moore_neighbourhood.py b/tests/heatmaps_tests/neighbourhood_tests/test_moore_neighbourhood.py
new file mode 100644
index 0000000..04e7bc9
--- /dev/null
+++ b/tests/heatmaps_tests/neighbourhood_tests/test_moore_neighbourhood.py
@@ -0,0 +1,45 @@
+import unittest
+from simulation.neighbourhood.moore_neighbourhood import MooreNeighbourhood
+
+class TestMooreNeighbourhood(unittest.TestCase):
+ def setUp(self):
+ self.width = 5
+ self.height = 5
+ self.neighbourhood = MooreNeighbourhood(self.width, self.height)
+
+ def test_initialization(self):
+ """Tests initialization of the MooreNeighbourhood."""
+ self.assertEqual(self.neighbourhood._width, self.width)
+ self.assertEqual(self.neighbourhood._height, self.height)
+
+ def test_get_neighbours(self):
+ """Tests the get_neighbours method."""
+ x, y = 2, 2
+ neighbours = list(self.neighbourhood.get_neighbours(x, y, 1, 1))
+ expected_neighbours = [
+ (1, 1), (1, 2), (1, 3),
+ (2, 1), (2, 3),
+ (3, 1), (3, 2), (3, 3)
+ ]
+ self.assertEqual(sorted(neighbours), sorted(expected_neighbours))
+
+ def test_get_neighbours_edge(self):
+ """Tests the get_neighbours method at the edge of the grid."""
+ x, y = 0, 0
+ neighbours = list(self.neighbourhood.get_neighbours(x, y, 1, 1))
+ expected_neighbours = [
+ (0, 1), (1, 0), (1, 1)
+ ]
+ self.assertEqual(sorted(neighbours), sorted(expected_neighbours))
+
+ def test_get_neighbours_corner(self):
+ """Tests the get_neighbours method at the corner of the grid."""
+ x, y = 4, 4
+ neighbours = list(self.neighbourhood.get_neighbours(x, y, 1, 1))
+ expected_neighbours = [
+ (3, 3), (3, 4), (4, 3)
+ ]
+ self.assertEqual(sorted(neighbours), sorted(expected_neighbours))
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/neighbourhood_tests/test_neumann_neighbourhood.py b/tests/heatmaps_tests/neighbourhood_tests/test_neumann_neighbourhood.py
new file mode 100644
index 0000000..539bfcd
--- /dev/null
+++ b/tests/heatmaps_tests/neighbourhood_tests/test_neumann_neighbourhood.py
@@ -0,0 +1,40 @@
+import unittest
+from simulation.neighbourhood.neumann_neighbourhood import NeumannNeighbourhood
+
+class TestNeumannNeighbourhood(unittest.TestCase):
+ def setUp(self):
+ self.width = 5
+ self.height = 5
+ self.neighbourhood = NeumannNeighbourhood(self.width, self.height)
+
+ def test_get_neighbours(self):
+ """Tests the get_neighbours method."""
+ x, y = 2, 2
+ neighbours = list(self.neighbourhood.get_neighbours(x, y, 1, 1))
+ expected_neighbours = [
+ (1, 2), (3, 2),
+ (2, 1), (2, 3)
+ ]
+ self.assertEqual(sorted(neighbours), sorted(expected_neighbours))
+
+ # Grid bound checking only appears in SimulationGrid, Neighbourhood is grid size agnostic
+ # def test_get_neighbours_edge(self):
+ # """Tests the get_neighbours method at the edge of the grid."""
+ # x, y = 0, 0
+ # neighbours = list(self.neighbourhood.get_neighbours(x, y, 1, 1))
+ # expected_neighbours = [
+ # (1, 0), (0, 1)
+ # ]
+ # self.assertEqual(sorted(neighbours), sorted(expected_neighbours))
+ #
+ # def test_get_neighbours_corner(self):
+ # """Tests the get_neighbours method at the corner of the grid."""
+ # x, y = 4, 4
+ # neighbours = list(self.neighbourhood.get_neighbours(x, y, 1, 1))
+ # expected_neighbours = [
+ # (3, 4), (4, 3)
+ # ]
+ # self.assertEqual(sorted(neighbours), sorted(expected_neighbours))
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/test_djisktra_heatmap_generator.py b/tests/heatmaps_tests/test_djisktra_heatmap_generator.py
new file mode 100644
index 0000000..1a5b49c
--- /dev/null
+++ b/tests/heatmaps_tests/test_djisktra_heatmap_generator.py
@@ -0,0 +1,111 @@
+import unittest
+from unittest.mock import Mock
+
+from simulation.core.cell_state import CellState
+from simulation.heatmaps.distancing.euclidean_distance import EuclideanDistance
+from simulation.heatmaps.djisktra_heatmap_generator import DijkstraHeatmapGenerator
+from simulation.core.cell import Cell
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.neighbourhood.moore_neighbourhood import MooreNeighbourhood
+
+
+class TestDijkstraHeatmapGenerator(unittest.TestCase):
+ def setUp(self):
+ self.distancing = Mock(spec=DistanceBase)
+ self.grid = Mock(spec=SimulationGrid)
+ self.cells = [Mock(spec=Cell) for _ in range(3)]
+ self.heatmap_generator = DijkstraHeatmapGenerator(self.distancing)
+
+ for i, cell in enumerate(self.cells):
+ cell.get_x.return_value = i
+ cell.get_y.return_value = i
+
+ def test_initialization(self):
+ """Tests initialization of the DijkstraHeatmapGenerator."""
+ self.assertEqual(self.heatmap_generator._distancing, self.distancing)
+
+ def test_generate_heatmap(self):
+ """Tests the generate_heatmap method."""
+ self.grid.get_width.return_value = 5
+ self.grid.get_height.return_value = 5
+ self.grid.get_cells.return_value = self.cells
+ self.grid.get_neighbours_at.return_value = []
+
+ heatmap = self.heatmap_generator.generate_heatmap(self.cells, self.grid)
+
+ self.assertIsInstance(heatmap, Heatmap)
+ self.assertEqual(heatmap.get_width(), 5)
+ self.assertEqual(heatmap.get_height(), 5)
+ for cell in self.cells:
+ heatmap.set_cell_at_pos(cell, 0)
+ self.assertEqual(heatmap.get_cell_at_pos(cell), 0)
+
+ def test_call_generate_heatmap__generates__expected_values_of_dijkstra(self):
+ """Tests the generate_heatmap method to see if dijkstra algorithm is correctly implemented."""
+ # Arrange
+ grid = SimulationGrid(3, 3, MooreNeighbourhood)
+ distance = EuclideanDistance(1)
+ generator = DijkstraHeatmapGenerator(distance)
+
+ # Act
+ heatmap = generator.generate_heatmap([grid.get_cell(1, 1)], grid)
+
+ # Assert
+ expected: list[float] = [
+ 1.4, 1.0, 1.4,
+ 1.0, 0.0, 1.0,
+ 1.4, 1.0, 1.4
+ ]
+
+ for i, value in enumerate(heatmap.get_cells()):
+ self.assertAlmostEqual(value, expected[i], places=1)
+
+ def test_call_generate_heatmap__generates__expected_values_of_dijkstra__with_obstacle(self):
+ """Tests the generate_heatmap method to see if dijkstra algorithm is correctly implemented."""
+ # Arrange
+ grid = SimulationGrid(3, 3, MooreNeighbourhood)
+ distance = EuclideanDistance(1)
+ generator = DijkstraHeatmapGenerator(distance, {CellState.OBSTACLE})
+
+ grid.get_cell(1, 0).set_osbtacle()
+
+ # Act
+ heatmap = generator.generate_heatmap([grid.get_cell(1, 1)], grid)
+
+ # Assert
+ expected: list[float] = [
+ 1.4, float('inf'), 1.4,
+ 1.0, 0.0, 1.0,
+ 1.4, 1.0, 1.4
+ ]
+
+ for i, value in enumerate(heatmap.get_cells()):
+ self.assertAlmostEqual(value, expected[i], places=1)
+
+ def test_call_generate_heatmap__ignores_obstacle__when_not_in_block_list(self):
+ """Tests the generate_heatmap method to see if dijkstra algorithm is correctly implemented."""
+ # Arrange
+ grid = SimulationGrid(3, 3, MooreNeighbourhood)
+ distance = EuclideanDistance(1)
+ generator = DijkstraHeatmapGenerator(distance, set())
+
+ grid.get_cell(1, 0).set_osbtacle()
+
+ # Act
+ heatmap = generator.generate_heatmap([grid.get_cell(1, 1)], grid)
+
+ # Assert
+ expected: list[float] = [
+ 1.4, 1.0, 1.4,
+ 1.0, 0.0, 1.0,
+ 1.4, 1.0, 1.4
+ ]
+
+ for i, value in enumerate(heatmap.get_cells()):
+ self.assertAlmostEqual(value, expected[i], places=1)
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/test_fast_marching_heatmap_generator.py b/tests/heatmaps_tests/test_fast_marching_heatmap_generator.py
new file mode 100644
index 0000000..22cfde4
--- /dev/null
+++ b/tests/heatmaps_tests/test_fast_marching_heatmap_generator.py
@@ -0,0 +1,53 @@
+import unittest
+from unittest.mock import Mock
+
+from simulation.heatmaps.distancing.euclidean_distance import EuclideanDistance
+from simulation.heatmaps.fast_marching_heatmap_generator import FastMarchingHeatmapGenerator
+from simulation.core.cell import Cell
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.core.cell_state import CellState
+from simulation.neighbourhood.moore_neighbourhood import MooreNeighbourhood
+
+
+class TestFastMarchingHeatmapGenerator(unittest.TestCase):
+ def setUp(self):
+ self.distancing = Mock(spec=DistanceBase)
+ self.grid = Mock(spec=SimulationGrid)
+ self.cells = [Mock(spec=Cell) for _ in range(3)]
+ self.heatmap_generator = FastMarchingHeatmapGenerator(self.distancing)
+
+ for i, cell in enumerate(self.cells):
+ cell.get_x.return_value = i
+ cell.get_y.return_value = i
+ cell.get_state.return_value = CellState.FREE
+
+ def test_initialization(self):
+ """Tests initialization of the FastMarchingHeatmapGenerator."""
+ self.assertEqual(self.heatmap_generator._distancing, self.distancing)
+ self.assertEqual(self.heatmap_generator._blocked, {CellState.OBSTACLE})
+
+ def test_generate_heatmap__generates__expected_values_of_fast_marching(self):
+ """Tests the implementation of the Fast Marching Method. Comparing it to the values of CDS307 Slides of day 4 page 19"""
+ # Arrange
+ grid = SimulationGrid(3, 3, MooreNeighbourhood)
+ distance = EuclideanDistance(1.0)
+ generator = FastMarchingHeatmapGenerator(distance)
+
+ # Act
+ heatmap = generator.generate_heatmap([grid.get_cell(0, 2)], grid)
+
+ # Assert
+ expected = [
+ 2.0, 2.2, 2.9,
+ 1.0, 1.7, 2.2,
+ 0.0, 1.0, 2.0
+ ]
+
+ for i, value in enumerate(heatmap.get_cells()):
+ self.assertAlmostEqual(value, expected[i], places=1)
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/heatmaps_tests/test_social_distancing_heatmap_generator.py b/tests/heatmaps_tests/test_social_distancing_heatmap_generator.py
new file mode 100644
index 0000000..936d419
--- /dev/null
+++ b/tests/heatmaps_tests/test_social_distancing_heatmap_generator.py
@@ -0,0 +1,94 @@
+import unittest
+from collections.abc import generator
+from unittest.mock import Mock
+
+from simulation.core.pedestrian import Pedestrian
+from simulation.heatmaps.distancing.euclidean_distance import EuclideanDistance
+from simulation.heatmaps.social_distancing_heatmap_generator import SocialDistancingHeatmapGenerator
+from simulation.core.cell import Cell
+from simulation.core.simulation_grid import SimulationGrid
+from simulation.heatmaps.heatmap import Heatmap
+from simulation.heatmaps.distancing.base_distance import DistanceBase
+from simulation.core.cell_state import CellState
+from simulation.neighbourhood.moore_neighbourhood import MooreNeighbourhood
+
+
+class TestSocialDistancingHeatmapGenerator(unittest.TestCase):
+ def setUp(self):
+ self.distancing = Mock(spec=DistanceBase)
+ self.grid = Mock(spec=SimulationGrid)
+ self.cells = [Mock(spec=Cell) for _ in range(3)]
+ self.heatmap_generator = SocialDistancingHeatmapGenerator(self.distancing, width=3.0, height=3.0)
+
+ for i, cell in enumerate(self.cells):
+ cell.get_x.return_value = i
+ cell.get_y.return_value = i
+ cell.get_state.return_value = CellState.FREE
+
+ def test_initialization(self):
+ """Tests initialization of the SocialDistancingHeatmapGenerator."""
+ self.assertEqual(self.heatmap_generator._distancing, self.distancing)
+ self.assertEqual(self.heatmap_generator._width, 3.0)
+ self.assertEqual(self.heatmap_generator._height, 3.0)
+ self.assertEqual(self.heatmap_generator._blocked, {CellState.OCCUPIED})
+
+ def test_generate_heatmap(self):
+ """Tests the generate_heatmap method."""
+ self.grid.get_width.return_value = 5
+ self.grid.get_height.return_value = 5
+ self.grid.get_cells.return_value = self.cells
+ self.grid.get_neighbours_at.side_effect = lambda pos: [
+ cell for cell in self.cells if cell.get_x() != pos.get_x() or cell.get_y() != pos.get_y()
+ ]
+
+ heatmap = self.heatmap_generator.generate_heatmap(self.cells, self.grid)
+
+ self.assertIsInstance(heatmap, Heatmap)
+ self.assertEqual(heatmap.get_width(), 5)
+ self.assertEqual(heatmap.get_height(), 5)
+ for cell in self.cells:
+ self.assertEqual(heatmap.get_cell_at_pos(cell), 0)
+
+ def test_generate_heatmap__generates__expected_heatmap_values(self):
+ """Tests if social distancing algorithm works as expected."""
+ # Arrange
+ distancing = EuclideanDistance(1.0)
+ grid = SimulationGrid(3, 3, MooreNeighbourhood)
+ pedestrian = Mock(spec=Pedestrian)
+ grid.get_cell(0,0).set_pedestrian(pedestrian)
+ generator = SocialDistancingHeatmapGenerator(distancing, 3, 3, {CellState.OCCUPIED})
+
+ # Act
+ heatmap = generator.generate_heatmap([grid.get_cell(0,0)], grid)
+
+ # Assert
+ # Setup expected with manually calculating the formula from the document CDS307-Aufgabe-Zellularautomat.pdf page 2
+ # w = h = 3
+ # up_d = h * exp(1/((d/w)^2 - 1)) if abs(d) < w else 0 (minus got omitted since a positive heatmap value means higher repulsion)
+ expected = [
+ 1.103, 0.973, 0.495,
+ 0.973, 0.829, 0.316,
+ 0.495, 0.316, 0.000
+ ]
+
+ for i, value in enumerate(heatmap.get_cells()):
+ self.assertAlmostEqual(value, expected[i], 2)
+
+ def test_generate_heatmap__only_ignores_cells__not_in_blocked_list(self):
+ """Tests if the heatmap generator ignores cells that are not in the blocked list."""
+ # Arrange
+ distancing = EuclideanDistance(1.0)
+ grid = SimulationGrid(3, 3, MooreNeighbourhood)
+ pedestrian = Mock(spec=Pedestrian)
+ grid.get_cell(0,0).set_pedestrian(pedestrian)
+ generator = SocialDistancingHeatmapGenerator(distancing, 3, 3, {})
+
+ # Act
+ heatmap = generator.generate_heatmap([grid.get_cell(0,0)], grid)
+
+ # Assert
+ for value in heatmap.get_cells():
+ self.assertGreaterEqual(value, 0.0)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/simulation/__pycache__/__init__.cpython-312.pyc b/tests/simulation/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 990b9dc..0000000
Binary files a/tests/simulation/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/tests/simulation/__pycache__/baseSimulationTest.cpython-312.pyc b/tests/simulation/__pycache__/baseSimulationTest.cpython-312.pyc
deleted file mode 100644
index 2f3af0e..0000000
Binary files a/tests/simulation/__pycache__/baseSimulationTest.cpython-312.pyc and /dev/null differ
diff --git a/tests/simulation/__pycache__/testGrid.cpython-312.pyc b/tests/simulation/__pycache__/testGrid.cpython-312.pyc
deleted file mode 100644
index c60f359..0000000
Binary files a/tests/simulation/__pycache__/testGrid.cpython-312.pyc and /dev/null differ
diff --git a/tests/simulation/baseSimulationTest.py b/tests/simulation/baseSimulationTest.py
deleted file mode 100644
index 9e9d176..0000000
--- a/tests/simulation/baseSimulationTest.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import unittest
-from src.simulation.baseSimulation import BaseSimulation
-from src.exceptions.simulationException import SimulationException
-
-class SimulationExceptionTest(unittest.TestCase):
-
- def test_simulateStep_throws(self):
- # ARRANGE
- baseSilumlation = BaseSimulation(None, None)
-
- # ACT
- self.assertRaises(SimulationException, baseSilumlation.simulateStep)
-
-
-if __name__ == "__main__":
- unittest.main()
\ No newline at end of file
diff --git a/tests/simulation/locomotionAlgorithms/__init__.py b/tests/simulation/locomotionAlgorithms/__init__.py
deleted file mode 100644
index 5ad2b93..0000000
--- a/tests/simulation/locomotionAlgorithms/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from src.simulation.locomotionAlgorithms.dijkstra import Dijkstra
-
diff --git a/tests/simulation/testGrid.py b/tests/simulation/testGrid.py
deleted file mode 100644
index d3b278a..0000000
--- a/tests/simulation/testGrid.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import unittest
-from src.simulation.grid import Grid
-from src.enums.tileStatus import TileStatus
-from src.simulation.tile import Tile
-
-class SimulationExceptionTest(unittest.TestCase):
-
- def test_gridWith10x10Tile_setsTiles(self):
- # ARRANGE
- length: int = 10
- width: int = 10
-
- # ACT
- grid = Grid(length, width)
-
- # ASSERT
- self.assertEqual(length, len(grid.getTiles()))
- for row in grid.getTiles():
- self.assertEqual(width, len(row))
- for tile in row:
- self.assertEqual(TileStatus.FREE, tile.getTileDTO().getTileStatus())
-
-
-if __name__ == "__main__":
- unittest.main()
\ No newline at end of file
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/utils/clipped_normal_distribution.py b/utils/clipped_normal_distribution.py
new file mode 100644
index 0000000..657c5d7
--- /dev/null
+++ b/utils/clipped_normal_distribution.py
@@ -0,0 +1,25 @@
+import numpy as np
+
+class ClippedNormalDistribution:
+ """
+ Helper class which randomly samples an absolute value from a normal distribution and clips the value to a given range.
+ """
+ def __init__(self, mean: float, sigma:float, min:float, max:float):
+ """
+ :param mean: mean of the normal distribution
+ :param sigma: standard deviation of the normal distribution
+ :param min: minimum value of the clipped range
+ :param max: maximum value of the clipped range
+ """
+ self._mean: float = mean
+ self._sigma: float = sigma
+ self._min: float = min
+ self._max:float = max
+
+ def sample(self) -> float:
+ """
+ Samples a value from the normal distribution and clips it to the given range
+ :return: positive value clipped to the given range
+ """
+ value = np.random.normal(self._mean, self._sigma)
+ return np.clip(abs(value), self._min, self._max)
\ No newline at end of file
diff --git a/utils/immutable_list.py b/utils/immutable_list.py
new file mode 100644
index 0000000..434a144
--- /dev/null
+++ b/utils/immutable_list.py
@@ -0,0 +1,24 @@
+class ImmutableList[T]:
+ """
+ List which cannot be modified, inspired by https://stackoverflow.com/questions/23474648/python-read-only-lists-using-the-property-decorator
+ """
+ def __init__(self, data: list[T]):
+ """
+ :param data: list to be wrapped
+ """
+ self._data = data
+
+ def __getitem__(self, index: int) -> T:
+ return self._data[index]
+
+ def __len__(self):
+ return len(self._data)
+
+ def __iter__(self):
+ return iter(self._data)
+
+ def __contains__(self, item):
+ return item in self._data
+
+ def index(self, target):
+ return self._data.index(target)
\ No newline at end of file
diff --git a/utils/utils.py b/utils/utils.py
new file mode 100644
index 0000000..98b35da
--- /dev/null
+++ b/utils/utils.py
@@ -0,0 +1,74 @@
+from typing import Tuple
+import base64
+import struct
+
+from simulation.heatmaps.heatmap import Heatmap
+
+
+def get_none_fields(**kwargs) -> list:
+ return [k for k, v in kwargs.items() if v is None]
+
+def if_all_or_none(*args) -> bool:
+ """
+ Check if all or none of the arguments are None
+ :param args:
+ :return:
+ """
+ return all(x is None for x in args) or all(x is not None for x in args)
+
+def none_check(**kwargs) -> Tuple[bool, list[str]]:
+ """
+ Check if all or none of the arguments are None
+ :param args: the arguments to check
+ :return: if all or none of the arguments are None, list of fields that are None
+ """
+ none_fields = get_none_fields(**kwargs)
+ return if_all_or_none(*kwargs.values()), none_fields
+
+
+def heatmap_to_bytes(heatmap: Heatmap) -> bytes:
+ """
+ Converts a heatmap to a byte array
+ :param heatmap: heatmap to convert
+ :return: byte array
+ """
+ cell_count = heatmap.get_width() * heatmap.get_height()
+ struct_fmt = 'II' + 'd' * cell_count
+ return struct.pack(struct_fmt, heatmap.get_width(), heatmap.get_height(), *heatmap.cells)
+
+def heatmap_to_base64(heatmap: Heatmap) -> str|None:
+ """
+ Converts a heatmap to a base64 string
+ :param heatmap: heatmap to convert
+ :return: base64 string
+ """
+ if heatmap is None:
+ return None
+
+ data = heatmap_to_bytes(heatmap)
+ return base64.b64encode(data).decode()
+
+def heatmap_from_bytes(data: bytes) -> Heatmap:
+ """
+ Converts a byte array to a heatmap
+ :param data: byte array
+ :return: heatmap
+ """
+ struct_fmt = ' Heatmap:
+ """
+ Converts a base64 string to a heatmap
+ :param data: base64 string
+ :return: heatmap
+ """
+ decoded = base64.b64decode(data)
+ return heatmap_from_bytes(decoded)
\ No newline at end of file
diff --git a/visualisation/__init__.py b/visualisation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/visualisation/button.py b/visualisation/button.py
new file mode 100644
index 0000000..11e9bc8
--- /dev/null
+++ b/visualisation/button.py
@@ -0,0 +1,78 @@
+from collections.abc import Callable
+from typing import Tuple
+
+import pygame
+from pygame import Rect, Surface
+
+from visualisation.theme import Theme, DEFAULT_THEME
+from visualisation.visualisation_helper import VisualisationHelper
+
+
+class Button:
+ def __init__(self, min_size: Rect, text: str, theme: Theme = DEFAULT_THEME, action: Callable = None):
+ self._min_size: Rect = min_size
+ self._bounds: Rect = min_size.copy()
+ self._theme: Theme = theme
+ self._millis_since_pressed = 0
+ self._action: Callable = action
+ self._is_hovered = False
+ self._text: str = None
+ self._rendered_text: Surface = None
+ self.set_text(text)
+
+ def update(self, delta_time: float) -> None:
+ if self._millis_since_pressed > 0:
+ self._millis_since_pressed -= delta_time
+
+ def is_pressed(self) -> bool:
+ return self._millis_since_pressed > 0
+
+ def press(self) -> None:
+ self._millis_since_pressed = 250
+ if self._action is not None:
+ self._action()
+
+ def is_hovered(self) -> bool:
+ return self._is_hovered
+
+ def set_hovered(self, hovered: bool) -> None:
+ self._is_hovered = hovered
+
+ def set_text(self, text: str) -> None:
+ if text != self._text:
+ self._rendered_text = VisualisationHelper.render_multiline_text_to_surface(self._theme.font, text, self._theme.foreground, True)
+ #self._rendered_text = self._theme.font.render(text, True, self._theme.foreground)
+ self._text = text
+ self._bounds.update(self._bounds.left, self._bounds.top, max(self._min_size.width, self._rendered_text.get_width()) + self._theme.padding.get_horizontal(), max(self._min_size.height, self._rendered_text.get_height()) + self._theme.padding.get_vertical())
+
+ def set_action(self, action) -> None:
+ self._action = action
+
+ def set_theme(self, theme: Theme) -> None:
+ self._theme = theme
+
+ def is_inside(self, x: int, y: int) -> bool:
+ return self._bounds.collidepoint(x, y)
+
+ def get_width(self) -> int:
+ return self._bounds.width
+
+ def get_height(self) -> int:
+ return self._bounds.height
+
+ def get_size(self) -> Tuple[int, int]:
+ return self._bounds.width, self._bounds.height
+
+ def set_position(self, x: int, y: int) -> None:
+ self._bounds.update(x, y, self._bounds.width, self._bounds.height)
+
+ def get_bounds(self) -> Rect:
+ return self._bounds
+
+ def render(self, surface) -> None:
+ color = self._theme.pressed_color if self.is_pressed() else self._theme.hover_color if self.is_hovered() else self._theme.color
+ pygame.draw.rect(surface, color, self._bounds)
+ center = self._bounds.center
+ text_pos = self._rendered_text.get_rect(center=center)
+ surface.blit(self._rendered_text, text_pos)
+
diff --git a/visualisation/features/__init__.py b/visualisation/features/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/visualisation/features/flowmeters_visualisation.py b/visualisation/features/flowmeters_visualisation.py
new file mode 100644
index 0000000..f456224
--- /dev/null
+++ b/visualisation/features/flowmeters_visualisation.py
@@ -0,0 +1,47 @@
+import pygame
+from pygame import Surface
+
+from typing import TYPE_CHECKING
+
+from pygame.font import SysFont
+
+from visualisation.visualisation_feature import VisualisationFeatureBase
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class FlowMetersVisualisation(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._small_font = SysFont(self._font_name, self._small_font_size)
+ self._render_names = self._helper.get_cell_size() > 40
+
+ def set_render_names(self, render_names: bool) -> None:
+ self._render_names = render_names
+
+ def get_render_names(self) -> bool:
+ return self._render_names
+
+
+ def _describe_state(self) -> str:
+ if self._visualisation.get_flow_meters() is None:
+ return "No Flow Meters"
+
+ return f"\nShow names: {self._render_names}\n".join([f"{flow_meter.get_name()}: {flow_meter.get_flow_rate()}" for flow_meter in self._visualisation.get_flow_meters()])
+
+ def _render(self, surface: Surface) -> None:
+ flow_meters = self._visualisation.get_flow_meters()
+ if flow_meters is not None:
+ for flow_meter in flow_meters:
+ for cell in flow_meter.get_cells():
+ cell_rect = self._helper.get_rect_at(cell)
+ pygame.draw.rect(surface, (200, 200, 200), cell_rect, 1)
+
+ if self._render_names:
+ name = self._small_font.render(f"{flow_meter.get_name()}: {flow_meter.get_flow_rate():.3f}", True, self._text_color)
+ text_pos = self._helper.get_x_center_pos_at(cell, name.get_height())
+ surface.blit(name, name.get_rect(center=text_pos))
+
diff --git a/visualisation/features/grid_visualisation_feature.py b/visualisation/features/grid_visualisation_feature.py
new file mode 100644
index 0000000..25eb5d3
--- /dev/null
+++ b/visualisation/features/grid_visualisation_feature.py
@@ -0,0 +1,37 @@
+import pygame
+from pygame import Surface
+
+from simulation.core.cell_state import CellState
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class GridVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._show_lines = True
+
+ def set_show_lines(self, show_lines: bool) -> None:
+ self._show_lines = show_lines
+
+ def get_show_lines(self) -> bool:
+ return self._show_lines
+
+ def _describe_state(self) -> str:
+ return None
+
+ def _render(self, surface: Surface) -> None:
+ for x in range(self._simulation.get_grid().get_width()):
+ for y in range(self._simulation.get_grid().get_height()):
+ cell = self._simulation.get_grid().get_cell(x, y)
+ cell_rect = self._helper.get_rect_at(cell)
+ if cell.get_state() == CellState.OBSTACLE:
+ pygame.draw.rect(surface, (0, 0, 0), cell_rect)
+
+ if self._show_lines:
+ pygame.draw.rect(surface, (0, 0, 0), cell_rect, 1)
diff --git a/visualisation/features/path_visualisation_feature.py b/visualisation/features/path_visualisation_feature.py
new file mode 100644
index 0000000..7d93f99
--- /dev/null
+++ b/visualisation/features/path_visualisation_feature.py
@@ -0,0 +1,89 @@
+import pygame.draw
+from pygame import Surface
+
+from simulation.core.position import Position
+from visualisation.visualisation_feature import VisualisationFeatureBase
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.pedestrian import Pedestrian
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class PathVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._selected_pedestrian: Pedestrian = None
+ self._selected_pedestrian_index = 0
+
+ def next_pedestrian(self):
+ pedestrians = self._simulation.get_pedestrians()
+ self._selected_pedestrian_index = (self._selected_pedestrian_index + 1) % (len(pedestrians) + 1)
+ self._selected_pedestrian = None if self._selected_pedestrian_index == 0 else pedestrians[self._selected_pedestrian_index - 1]
+
+ def previous_pedestrian(self):
+ pedestrians = self._simulation.get_pedestrians()
+ self._selected_pedestrian_index = (self._selected_pedestrian_index - 1) % (len(pedestrians) + 1)
+ self._selected_pedestrian = None if self._selected_pedestrian_index == 0 else pedestrians[self._selected_pedestrian_index - 1]
+
+ def set_pedestrian(self, pedestrian: 'Pedestrian|None'):
+ if pedestrian is None:
+ self._selected_pedestrian_index = 0
+ self._selected_pedestrian = None
+ else:
+ self._selected_pedestrian_index = self._simulation.get_pedestrians().index(pedestrian) + 1
+ self._selected_pedestrian = pedestrian
+
+ def _describe_state(self) -> str:
+ return f"Selected pedestrian: {'None' if self._selected_pedestrian is None else self._selected_pedestrian.get_id()}"
+
+ def _render_target(self, surface):
+ heatmap = self._selected_pedestrian.get_heatmap()
+ last_pos: Position = self._selected_pedestrian
+ visited: set[Position] = set()
+ visited.add(last_pos)
+ while last_pos is not None:
+ next_pos: Position = self._simulation._get_next_target_cell(heatmap, last_pos, last_pos)
+ if next_pos is None or next_pos in visited:
+ break
+
+ pygame.draw.line(surface, (255, 255, 255), self._helper.get_centered_pos_at(last_pos), self._helper.get_centered_pos_at(next_pos), 2)
+ if self._selected_pedestrian.get_target().is_inside_target(next_pos):
+ break
+
+ visited.add(next_pos)
+ last_pos = next_pos
+
+ def _render_waypoint(self, surface):
+ waypoint = self._selected_pedestrian.get_waypoint()
+ heatmap = waypoint.get_heatmap()
+ last_pos: Position = self._selected_pedestrian
+ visited: set[Position] = set()
+ visited.add(last_pos)
+ while last_pos is not None:
+ next_pos: Position = waypoint.next_cell(last_pos)
+ if next_pos is None or next_pos in visited:
+ break
+
+ pygame.draw.line(surface, (255, 255, 255), self._helper.get_centered_pos_at(last_pos), self._helper.get_centered_pos_at(next_pos), 2)
+ if waypoint.is_inside_waypoint(next_pos):
+ break
+
+ visited.add(next_pos)
+ last_pos = next_pos
+
+ def _render(self, surface: Surface) -> None:
+ if self._selected_pedestrian is not None:
+ if self._selected_pedestrian.has_reached_target():
+ self.set_pedestrian(None)
+ return
+
+ origin = self._helper.get_centered_pos_at(self._selected_pedestrian)
+ pygame.draw.circle(surface, (255, 255, 255), origin, self._helper.get_cell_size() // 2, 2)
+ if self._selected_pedestrian.has_waypoint():
+ self._render_waypoint(surface)
+ else:
+ self._render_target(surface)
diff --git a/visualisation/features/pedestrian_visualisation_feature.py b/visualisation/features/pedestrian_visualisation_feature.py
new file mode 100644
index 0000000..6b63c07
--- /dev/null
+++ b/visualisation/features/pedestrian_visualisation_feature.py
@@ -0,0 +1,61 @@
+import math
+
+import pygame.font
+from pygame import Surface
+
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class PedestrianVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._render_details = self._helper.get_cell_size() > 20
+ self._render_target_line = self._helper.get_cell_size() > 30
+ self._font = pygame.font.SysFont(self._font_name, self._font_size)
+
+ def set_render_details(self, state: bool) -> None:
+ self._render_details = state
+
+ def set_render_target_line(self, state: bool) -> None:
+ self._render_target_line = state
+
+ def get_render_details(self) -> bool:
+ return self._render_details
+
+ def get_render_target_line(self) -> bool:
+ return self._render_target_line
+
+ def _describe_state(self) -> str:
+ return f"Show details: {self._render_details}\nShow target line: {self._render_target_line}"
+
+ def _render(self, surface: Surface) -> None:
+ for pedestrian in self._simulation.get_pedestrians():
+ spawner_color = self._visualisation.get_spawner_color(pedestrian.get_spawner())
+
+ if pedestrian.has_targeted_cell():
+ target = pedestrian.get_targeted_cell()
+ target_color = self._visualisation.get_target_color(pedestrian.get_target())
+ if self._render_target_line:
+ from_pos = self._helper.get_centered_pos_at(pedestrian)
+ to_pos = self._helper.get_centered_pos_at(target)
+ pygame.draw.line(surface, target_color, from_pos, to_pos, 2)
+
+ # calculate the angle between the pedestrian and the target
+ angle = math.atan2(target.get_y() - pedestrian.get_y(), target.get_x() - pedestrian.get_x())
+ triangle = self._helper.get_small_triangle_at(pedestrian, angle)
+ pygame.draw.polygon(surface, spawner_color, triangle)
+ pygame.draw.lines(surface, target_color, True, triangle, 2)
+ else:
+ rect = self._helper.get_small_rect_at(pedestrian)
+ pygame.draw.rect(surface, spawner_color, rect)
+
+ if self._render_details:
+ speed_info = self._font.render(f"[{pedestrian.get_id()}] {pedestrian.get_average_speed():.2f}m/s, {pedestrian.get_optimal_speed():.2f}m/s, {pedestrian.get_current_distance():.2f}m", True, self._text_color)
+ text_pos = self._helper.get_x_center_pos_at(pedestrian, speed_info.get_height())
+ surface.blit(speed_info, speed_info.get_rect(center=text_pos))
diff --git a/visualisation/features/simulation_info_visualisation_feature.py b/visualisation/features/simulation_info_visualisation_feature.py
new file mode 100644
index 0000000..17a4959
--- /dev/null
+++ b/visualisation/features/simulation_info_visualisation_feature.py
@@ -0,0 +1,39 @@
+from os import supports_fd
+
+import pygame.font
+from pygame import Surface
+
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class SimulationInfoVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._font_size = 20
+ self._font = pygame.font.SysFont(self._font_name, self._helper.get_cell_size(), bold=True)
+
+ def _describe_state(self) -> str:
+ return f"FPS: {self._visualisation._clock.get_fps()}\nStep: {self._simulation.get_steps()}\nTime: {self._simulation.get_run_time():.2f}s\nPedestrians: {len(self._simulation.get_pedestrians())}\nIs paused: {self._visualisation.is_paused()}"
+
+ def _translate(self, pos: tuple[int, int], x: int, y: int) -> tuple[int, int]:
+ return pos[0] + x, pos[1] + y
+
+ def _next_line(self, pos: tuple[int, int]) -> tuple[int, int]:
+ return pos[0], pos[1] + self._font_size
+
+ def _render(self, surface: Surface) -> None:
+ pos = self._translate(self._helper.get_top_left(), 10, 10)
+ steps = self._font.render(f"Step: {self._simulation.get_steps()}", True, self._text_color)
+ surface.blit(steps, pos)
+ run_time = self._font.render(f"Time: {self._simulation.get_run_time():.2f}s", True, self._text_color)
+ surface.blit(run_time, pos := self._translate(pos, 0, steps.get_height()))
+ ped_count = self._font.render(f"Pedestrians: {len(self._simulation.get_pedestrians())}", True, self._text_color)
+ surface.blit(ped_count, pos := self._translate(pos, 0, run_time.get_height()))
+ paused = self._font.render(f"Is paused: {self._visualisation.is_paused()}", True, self._text_color)
+ surface.blit(paused, pos := self._translate(pos, 0, ped_count.get_height()))
diff --git a/visualisation/features/spawner_visualisation_feature.py b/visualisation/features/spawner_visualisation_feature.py
new file mode 100644
index 0000000..3e44967
--- /dev/null
+++ b/visualisation/features/spawner_visualisation_feature.py
@@ -0,0 +1,37 @@
+import pygame
+from pygame import Surface
+
+
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class SpawnerVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._font = pygame.font.SysFont(self._font_name, self._font_size)
+ self._render_names = self._helper.get_cell_size() > 40
+
+ def get_render_names(self) -> bool:
+ return self._render_names
+
+ def set_render_names(self, state: bool) -> None:
+ self._render_names = state
+
+ def _describe_state(self) -> str:
+ return f"Show names: {self._render_names}"
+
+ def _render(self, surface: Surface) -> None:
+ for spawner in self._simulation.get_spawners():
+ color = self._visualisation.get_spawner_color(spawner)
+ for cell in spawner.get_cells():
+ cell_rect = self._helper.get_small_rect_at(cell)
+ pygame.draw.rect(surface, color, cell_rect)
+ if self._render_names:
+ name = self._font.render(spawner.get_name(), True, self._text_color)
+ text_pos = self._helper.get_x_center_pos_at(cell, name.get_height())
+ surface.blit(name, name.get_rect(center=text_pos))
diff --git a/visualisation/features/target_heatmap_visualisation_feature.py b/visualisation/features/target_heatmap_visualisation_feature.py
new file mode 100644
index 0000000..b43dad6
--- /dev/null
+++ b/visualisation/features/target_heatmap_visualisation_feature.py
@@ -0,0 +1,81 @@
+import pygame
+from pygame import Surface
+
+from simulation.core.target import Target
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class TargetHeatmapVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._selected_target_index = 0
+ self._selected_target = None
+ self._font = pygame.font.SysFont(self._font_name, self._small_font_size)
+ self._include_social_distancing = True
+ self._max_social_distancing_value = sim._social_distancing_generator.get_max_value()
+ self._max_heatmap_value = sim.get_max_grid_distance() + self._max_social_distancing_value
+ self._render_details = self._helper.get_cell_size() > 10
+
+ def next_target(self):
+ self._selected_target_index = (self._selected_target_index + 1) % (len(self._simulation.get_targets()) + 1)
+ self._selected_target = None if self._selected_target_index == 0 else self._simulation.get_targets()[self._selected_target_index-1]
+
+ def previous_target(self):
+ self._selected_target_index = (self._selected_target_index - 1) % (len(self._simulation.get_targets()) + 1)
+ self._selected_target = None if self._selected_target_index == 0 else self._simulation.get_targets()[self._selected_target_index-1]
+
+ def set_target(self, target: 'Target'):
+ if target is None:
+ self._selected_target_index = 0
+ self._selected_target = None
+ else:
+ self._selected_target_index = self._simulation.get_targets().index(target) + 1
+ self._selected_target = target
+
+ def set_social_distancing(self, value: bool):
+ self._include_social_distancing = value
+
+ def get_social_distancing(self) -> bool:
+ return self._include_social_distancing
+
+ def _describe_state(self) -> str:
+ return f"Selected target: {'None' if self._selected_target is None else self._selected_target.get_name()}\nShow social distancing: {self._include_social_distancing}"
+
+ def _render(self, surface: Surface) -> None:
+ heatmap = self._selected_target.get_heatmap() if self._selected_target is not None else None
+ social_distancing_heatmap = self._simulation.get_distancing_heatmap()
+
+ for x in range(self._simulation.get_grid().get_width()):
+ for y in range(self._simulation.get_grid().get_height()):
+ value = heatmap.get_cell(x, y) if heatmap is not None else 0
+
+ min_r = 0
+ if self._include_social_distancing:
+ social_distance = social_distancing_heatmap.get_cell(x, y)
+ min_r = min(200, int(social_distance/(self._max_social_distancing_value // 2) * 200))
+ value += social_distance
+
+ ratio = min(value, self._max_heatmap_value) / self._max_heatmap_value
+ color = self._helper.mix_colors((255, 200, 0), (min_r, 100, 255), ratio)
+ rect = self._helper.get_rect(x, y)
+ pygame.draw.rect(surface, color, rect)
+ if self._render_details:
+ value_text = self._font.render(f"{value:.2f}", True, (40, 40, 40))
+ text_pos = self._helper.get_centered_pos(x, y)
+ surface.blit(value_text, value_text.get_rect(center=text_pos))
+
+ if self._selected_target is not None:
+ min_x, min_y, max_x, max_y = float('inf'), float('inf'), 0, 0
+ for cell in self._selected_target.get_cells():
+ min_x = min(min_x, cell.get_x())
+ min_y = min(min_y, cell.get_y())
+ max_x = max(max_x, cell.get_x())
+ max_y = max(max_y, cell.get_y())
+
+ pygame.draw.rect(surface, (255, 255, 255), self._helper.get_rect_with_size(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1), 2)
\ No newline at end of file
diff --git a/visualisation/features/target_visualisation_feature.py b/visualisation/features/target_visualisation_feature.py
new file mode 100644
index 0000000..1480f36
--- /dev/null
+++ b/visualisation/features/target_visualisation_feature.py
@@ -0,0 +1,38 @@
+import pygame
+from pygame import Surface
+
+from visualisation.visualisation_feature import VisualisationFeatureBase
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class TargetVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._font = pygame.font.SysFont(self._font_name, self._font_size)
+ self._render_names = self._helper.get_cell_size() > 40
+
+ def set_render_names(self, render_names: bool) -> None:
+ self._render_names = render_names
+
+ def get_render_names(self) -> bool:
+ return self._render_names
+
+ def _describe_state(self) -> str:
+ return f"Show names: {self._render_names}"
+
+ def _render(self, surface: Surface) -> None:
+ radius = self._helper.get_cell_size() // 4
+ for target in self._simulation.get_targets():
+ color = self._visualisation.get_target_color(target)
+ for cell in target.get_cells():
+ pygame.draw.circle(surface, color, self._helper.get_centered_pos_at(cell), radius)
+ if self._render_names:
+ name = self._font.render(target.get_name(), True, self._text_color)
+ text_pos = self._helper.get_x_center_pos_at(cell, name.get_height())
+ surface.blit(name, name.get_rect(center=text_pos))
\ No newline at end of file
diff --git a/visualisation/features/waypoint_visualisation_feature.py b/visualisation/features/waypoint_visualisation_feature.py
new file mode 100644
index 0000000..03e52ba
--- /dev/null
+++ b/visualisation/features/waypoint_visualisation_feature.py
@@ -0,0 +1,39 @@
+import pygame
+from pygame import Surface
+
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+ from visualisation.visualisation_helper import VisualisationHelper
+
+
+class WaypointVisualisationFeature(VisualisationFeatureBase):
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: 'VisualisationHelper'):
+ super().__init__(sim, vis, vis_helper)
+ self._show_pedestrian_line = True
+
+ def set_show_pedestrian_line(self, show_pedestrian_line: bool) -> None:
+ self._show_pedestrian_line = show_pedestrian_line
+
+ def get_show_pedestrian_line(self) -> bool:
+ return self._show_pedestrian_line
+
+ def _describe_state(self) -> str:
+ pass
+
+ def _render(self, surface: Surface) -> None:
+ for waypoint in self._simulation.get_waypoints():
+ color = (120, 30, 220)
+ center_pos = self._helper.get_centered_pos_at(waypoint.get_cell())
+ pygame.draw.circle(surface, color, center_pos, self._helper.get_cell_size() // 6)
+ pygame.draw.circle(surface, color, center_pos, self._helper.get_cell_size() // 4, 3)
+
+ if self._show_pedestrian_line:
+ pedestrian = waypoint.get_pedestrian()
+ if pedestrian is not None:
+ pygame.draw.line(surface, color, center_pos, self._helper.get_centered_pos_at(pedestrian), 3)
+
+
diff --git a/visualisation/flow_meter.py b/visualisation/flow_meter.py
new file mode 100644
index 0000000..2e01305
--- /dev/null
+++ b/visualisation/flow_meter.py
@@ -0,0 +1,63 @@
+from typing import TYPE_CHECKING
+
+from serialization.serializable import Serializable
+import csv
+
+if TYPE_CHECKING:
+ from simulation.core.cell import Cell
+ from simulation.core.simulation import Simulation
+
+
+class FlowMeter:
+ HEADERS = ['runTime', 'flowRate', 'pedestrianDensity']
+
+ def __init__(self, name: str, time_span: float, cells: 'list[Cell]', logfile: str = None, log_interval: float = float("inf")):
+ self._name = name
+ self._flow_rate = 0
+ self._time_span: float = time_span
+ self._cells: 'list[Cell]' = cells
+ self._seen_pedestrians: dict[int, float] = {}
+ self._logfile = logfile
+ self._log_interval = log_interval
+ self._delta_time = log_interval
+
+ def get_initial_data(self) -> dict[str, any]:
+ return {
+ "name": self._name,
+ "time_span": self._time_span,
+ "cells": [[cell.get_x(), cell.get_y()] for cell in self._cells],
+ }
+
+ def get_name(self) -> str:
+ return self._name
+
+ def get_flow_rate(self) -> float:
+ return self._flow_rate
+
+ def update(self, simulation: 'Simulation') -> None:
+ for cell in self._cells:
+ pedestrian = cell.get_pedestrian()
+ if pedestrian is not None and pedestrian.get_id() not in self._seen_pedestrians:
+ self._seen_pedestrians[pedestrian.get_id()] = simulation.get_run_time()
+
+ pedestrians_in_timespan = sum(1 for time in self._seen_pedestrians.values() if simulation.get_run_time() - time <= self._time_span)
+ self._flow_rate = pedestrians_in_timespan / self._time_span
+
+ def get_cells(self) -> 'list[Cell]':
+ return self._cells
+
+ def create_log(self) -> None:
+ with open(self._logfile, mode="w", newline="") as file:
+ writer = csv.writer(file)
+ writer.writerow(self.HEADERS)
+
+ def log(self, delta_time: float, runtime: float, pedestrian_density: float) -> None:
+ self._delta_time -= delta_time
+ if self._delta_time < 0:
+ self._log(runtime, pedestrian_density)
+ self._delta_time = self._log_interval
+
+ def _log(self, runtime, pedestrian_density: float) -> None:
+ with open(self._logfile, "a", newline="") as file:
+ writer = csv.writer(file)
+ writer.writerow([runtime, self.get_flow_rate(), pedestrian_density])
\ No newline at end of file
diff --git a/visualisation/shortcut.py b/visualisation/shortcut.py
new file mode 100644
index 0000000..1c6df28
--- /dev/null
+++ b/visualisation/shortcut.py
@@ -0,0 +1,38 @@
+class Shortcut:
+ def __init__(self, name: str, key: int, modifiers: int, action, toggable: bool = False, initial_state: bool = True):
+ self.name = name
+ self.key = key
+ self.modifiers = modifiers
+ self.action = action
+ self.code = Shortcut.calculate_code(key, modifiers)
+ self._is_toggle = toggable
+ self._state = initial_state
+
+ @staticmethod
+ def calculate_code(key: int, modifiers: int):
+ return (modifiers & 0xFF) | (key << 8)
+
+ def get_code(self):
+ return self.code
+
+ def execute(self):
+ if self._is_toggle:
+ self._state = not self._state
+ self.action(self._state)
+ else:
+ self.action()
+
+ def is_toggle(self):
+ return self._is_toggle
+
+ def get_state(self):
+ return self._state
+
+ def get_action(self):
+ return self.action
+
+ def set_action(self, action):
+ self.action = action
+
+ def get_text(self):
+ return self.name
\ No newline at end of file
diff --git a/visualisation/theme.py b/visualisation/theme.py
new file mode 100644
index 0000000..0fa1a99
--- /dev/null
+++ b/visualisation/theme.py
@@ -0,0 +1,59 @@
+from typing import Tuple
+
+import pygame
+from pygame.font import Font
+
+from visualisation.visualisation_helper import VisualisationHelper
+
+
+class Padding(Tuple[int, int, int, int]):
+ def __new__(cls, top: int, right: int, bottom: int, left: int):
+ return super().__new__(cls, (top, right, bottom, left))
+
+ def get_left(self) -> int:
+ return self[3]
+
+ def get_right(self) -> int:
+ return self[1]
+
+ def get_top(self) -> int:
+ return self[0]
+
+ def get_bottom(self) -> int:
+ return self[2]
+
+ def get_horizontal(self) -> int:
+ return self[1] + self[3]
+
+ def get_vertical(self) -> int:
+ return self[0] + self[2]
+
+
+class Theme:
+ def __init__(self, color: Tuple[int, int, int], hover_color: Tuple[int, int, int], active_color: Tuple[int, int, int], pressed_color: Tuple[int, int, int], foreground: Tuple[int, int, int], background: Tuple[int, int, int], font: Font = None, padding: Padding = None):
+ self.color: Tuple[int, int, int] = color
+ self.hover_color: Tuple[int, int, int] = hover_color
+ self.active_color: Tuple[int, int, int] = active_color
+ self.pressed_color: Tuple[int, int, int] = pressed_color
+ self.foreground: Tuple[int, int, int] = foreground
+ self.background: Tuple[int, int, int] = background
+ pygame.font.init()
+ self.font: Font = font or pygame.font.SysFont("Arial", 12)
+ self.padding: Padding = padding or Padding(5, 5, 5, 5)
+
+ @classmethod
+ def create_theme(cls, base_color: Tuple[int, int, int], font: Font = None, padding: Padding = None) -> 'Theme':
+ hsl = VisualisationHelper.rgb_to_hsl(base_color)
+ if hsl[2] < 0.5:
+ hover_color = VisualisationHelper.hsl_to_rgb((hsl[0], hsl[1] * 0.8, hsl[2] * 1.2))
+ active_color = VisualisationHelper.hsl_to_rgb((hsl[0], hsl[1] * 0.7, hsl[2] * 1.3))
+ pressed_color = VisualisationHelper.hsl_to_rgb((hsl[0], hsl[1] * 0.6, hsl[2] * 1.4))
+ return cls(base_color, hover_color, active_color, pressed_color, (250, 250, 250), (20, 20, 20), font, padding)
+ else:
+ hover_color = VisualisationHelper.hsl_to_rgb((hsl[0], hsl[1] * 0.8, hsl[2] * 0.8))
+ active_color = VisualisationHelper.hsl_to_rgb((hsl[0], hsl[1] * 0.7, hsl[2] * 0.7))
+ pressed_color = VisualisationHelper.hsl_to_rgb((hsl[0], hsl[1] * 0.6, hsl[2] * 0.6))
+ return cls(base_color, hover_color, active_color, pressed_color, (20, 20, 20), (250, 250, 250), font, padding)
+
+
+DEFAULT_THEME = Theme.create_theme((100, 149, 237))
diff --git a/visualisation/toggle_button.py b/visualisation/toggle_button.py
new file mode 100644
index 0000000..61421d6
--- /dev/null
+++ b/visualisation/toggle_button.py
@@ -0,0 +1,33 @@
+from collections.abc import Callable
+
+from pygame import Rect
+
+from visualisation.button import Button
+from visualisation.theme import Theme
+
+
+class ToggleButton(Button):
+ def __init__(self, min_size: Rect, on_format: str, off_format: str, action: Callable, on_theme: Theme, off_theme: Theme, state_getter: Callable[[],bool]):
+ super().__init__(min_size, "", on_theme, action)
+ self._on_theme = on_theme
+ self._off_theme = off_theme
+ self._state_getter: Callable[[], bool] = state_getter
+ self._on_format = on_format
+ self._off_format = off_format
+ self._last_state = None
+ self._update_state()
+
+ def _update_state(self) -> None:
+ state = self._state_getter()
+ if state != self._last_state:
+ self._last_state = state
+ self._theme = self._on_theme if state else self._off_theme
+ self.set_text((self._on_format if state else self._off_format).format("On" if state else "Off"))
+
+ def update(self, delta: float) -> None:
+ super().update(delta)
+ self._update_state()
+
+ def press(self) -> None:
+ super().press()
+ self._update_state()
\ No newline at end of file
diff --git a/visualisation/visualisation.py b/visualisation/visualisation.py
new file mode 100644
index 0000000..e4d6350
--- /dev/null
+++ b/visualisation/visualisation.py
@@ -0,0 +1,327 @@
+import datetime
+import json
+import math
+from random import random
+from typing import Tuple, Type, TypeVar
+
+import pygame
+from pygame import Color, Rect, Surface
+from pygame.font import Font
+
+from serialization.serializer import Serializer
+from simulation.core.cell_state import CellState
+from simulation.core.position import Position
+from simulation.core.simulation import Simulation
+from visualisation.button import Button
+from visualisation.features.flowmeters_visualisation import FlowMetersVisualisation
+from visualisation.features.grid_visualisation_feature import GridVisualisationFeature
+from visualisation.features.path_visualisation_feature import PathVisualisationFeature
+from visualisation.features.pedestrian_visualisation_feature import PedestrianVisualisationFeature
+from visualisation.features.simulation_info_visualisation_feature import SimulationInfoVisualisationFeature
+from visualisation.features.spawner_visualisation_feature import SpawnerVisualisationFeature
+from visualisation.features.target_heatmap_visualisation_feature import TargetHeatmapVisualisationFeature
+from visualisation.features.target_visualisation_feature import TargetVisualisationFeature
+from visualisation.features.waypoint_visualisation_feature import WaypointVisualisationFeature
+from visualisation.flow_meter import FlowMeter
+from visualisation.shortcut import Shortcut
+from visualisation.theme import Theme, DEFAULT_THEME
+from visualisation.toggle_button import ToggleButton
+from visualisation.visualisation_feature import VisualisationFeatureBase
+from visualisation.visualisation_helper import VisualisationHelper
+
+
+class Visualisation:
+ GRID_OFFSET = 200
+
+ def __init__(self, simulation: Simulation, cell_size: int | None = None, fps: float = 30, log_file: str = None, flow_meters: list[FlowMeter] = None):
+ pygame.init()
+ simulation.update(0) # Update simulation once to get the initial state
+ self.simulation = simulation
+ if cell_size is None:
+ info = pygame.display.Info()
+ w, h = info.current_w, info.current_h
+ cell_size = math.ceil(self.calculate_cell_size(w, h) * 0.9)
+
+ self._cell_size: int = cell_size
+ self._screen = pygame.display.set_mode((simulation.get_grid().get_width() * cell_size, Visualisation.GRID_OFFSET + simulation.get_grid().get_height() * cell_size), pygame.RESIZABLE)
+ pygame.display.set_caption("Simulation Visualisation")
+ self._clock = pygame.time.Clock()
+ self._is_paused = False
+ self._running = True
+ self._fps = fps
+ self._spawner_colors = {spawner: VisualisationHelper.get_random_color() for spawner in simulation.get_spawners()}
+ self._target_colors = {target: VisualisationHelper.get_random_color() for target in simulation.get_targets()}
+ self._helper = VisualisationHelper(self)
+ self._shortcuts: dict[int, Shortcut] = {}
+ self._features: list[VisualisationFeatureBase] = []
+ self._font = pygame.font.SysFont("Arial", min(int(self._cell_size * 0.5), 16))
+ self._buttons: list[Button] = []
+ self._click_theme = DEFAULT_THEME
+ self._on_theme = Theme.create_theme((100, 250, 30))
+ self._off_theme = Theme.create_theme((250, 30, 30))
+ self._max_feature_width = 0
+ self._simulation_delta = 0
+ self._show_feature_details = self._screen.get_width() > 800
+ self._show_buttons = True
+ self._init_features()
+ self._flow_meters: list[FlowMeter] = flow_meters or None
+ self._flow_log = open(datetime.datetime.now().strftime("%d%m%y_%H%M%S") + "_flow_log.json", "w") if self._flow_meters is not None else None
+ self._is_first_flow_log = True
+ self._serializer = Serializer(simulation, log_file.format(datetime.datetime.now().strftime("%d%m%y_%H%M%S"))) if log_file is not None else None
+ self._init_flow_log()
+
+ TFeature = TypeVar('TFeature', bound=VisualisationFeatureBase)
+
+
+ def get_flow_meters(self) -> list[FlowMeter]:
+ return self._flow_meters
+
+ def get_cell_size(self) -> int:
+ return self._cell_size
+
+ def is_paused(self) -> bool:
+ return self._is_paused
+
+ def set_pause(self, pause: bool) -> None:
+ self._is_paused = pause
+
+ def calculate_cell_size(self, width: int, height: int) -> int:
+ return min(width // self.simulation.get_grid().get_width(), (height - Visualisation.GRID_OFFSET) // self.simulation.get_grid().get_height())
+
+ def get_feature(self, type: Type[TFeature]) -> TFeature | None:
+ for feature in self._features:
+ if isinstance(feature, type):
+ return feature
+
+ return None
+
+ def add_feature[T](self, feature: T, enabled: bool = True) -> T:
+ self._features.append(feature)
+ feature.set_enabled(enabled)
+ return feature
+
+ def add_shortcut(self, shortcut: Shortcut, add_button: bool = False, button_text: str = None, off_text: str = None):
+ button_text = button_text or shortcut.get_text()
+ self._shortcuts[shortcut.get_code()] = shortcut
+ if add_button:
+ if shortcut.is_toggle():
+ button = ToggleButton(Rect(0, 0, 100, 35), button_text, off_text or button_text, shortcut.execute, self._on_theme, self._off_theme, shortcut.get_state)
+ self._buttons.append(button)
+ else:
+ button = Button(Rect(0, 0, 100, 35), button_text, self._click_theme, shortcut.execute)
+ self._buttons.append(button)
+
+ def _init_features(self):
+ heatmap = self.add_feature(TargetHeatmapVisualisationFeature(self.simulation, self, self._helper))
+ spawner = self.add_feature(SpawnerVisualisationFeature(self.simulation, self, self._helper))
+ target = self.add_feature(TargetVisualisationFeature(self.simulation, self, self._helper))
+ grid = self.add_feature(GridVisualisationFeature(self.simulation, self, self._helper))
+ path = self.add_feature(PathVisualisationFeature(self.simulation, self, self._helper))
+ flow_meter = self.add_feature(FlowMetersVisualisation(self.simulation, self, self._helper), True)
+ waypoint = self.add_feature(WaypointVisualisationFeature(self.simulation, self, self._helper))
+ pedestrian = self.add_feature(PedestrianVisualisationFeature(self.simulation, self, self._helper))
+ info = self.add_feature(SimulationInfoVisualisationFeature(self.simulation, self, self._helper), False)
+ self.add_shortcut(Shortcut("Toggle heatmap", pygame.K_h, 0, heatmap.set_enabled, True, heatmap.is_enabled()), True, "Heatmap {0}")
+ self.add_shortcut(Shortcut("Toggle spawner", pygame.K_s, 0, spawner.set_enabled, True, spawner.is_enabled()), True, "Spawners {0}")
+ self.add_shortcut(Shortcut("Toggle target", pygame.K_t, 0, target.set_enabled, True, target.is_enabled()), True, "Targets {0}")
+ self.add_shortcut(Shortcut("Toggle grid", pygame.K_g, 0, grid.set_enabled, True, grid.is_enabled()), True, "Grid {0}")
+ self.add_shortcut(Shortcut("Toggle pedestrian", pygame.K_p, 0, pedestrian.set_enabled, True, pedestrian.is_enabled()), True, "Pedestrians {0}")
+ self.add_shortcut(Shortcut("Toggle info", pygame.K_i, 0, info.set_enabled, True, info.is_enabled()), True, "Info {0}")
+ self.add_shortcut(Shortcut("Toggle social distancing", pygame.K_d, 0, heatmap.set_social_distancing, True, heatmap.get_social_distancing()), True, "Social distancing\n{0}")
+ self.add_shortcut(Shortcut("Toggle route", pygame.K_r, 0, path.set_enabled, True, path.is_enabled()), True, "Pathing {0}")
+ self.add_shortcut(Shortcut("Toggle flow meters", pygame.K_f, 0, flow_meter.set_enabled, True, flow_meter.is_enabled()), True, "Flow meters {0}")
+ self.add_shortcut(Shortcut("Toggle grid lines", pygame.K_l, 0, grid.set_show_lines, True, grid.get_show_lines()), True, "Grid lines\n{0}")
+ self.add_shortcut(Shortcut("Toggle waypoint", pygame.K_w, 0, waypoint.set_enabled, True, waypoint.is_enabled()), True, "Waypoints {0}")
+ self.add_shortcut(Shortcut("Show object names", pygame.K_n, 0, self._set_show_names, True, False), True, "Show names\n{0}")
+ self.add_shortcut(Shortcut("Show pedestrian details", pygame.K_p, pygame.KMOD_LSHIFT, pedestrian.set_render_details, True, pedestrian.get_render_details()), True, "Pedestrian details\n{0}")
+ self.add_shortcut(Shortcut("Show pedestrian target cell", pygame.K_p, pygame.KMOD_LCTRL, pedestrian.set_render_target_line, True, pedestrian.get_render_target_line()), True, "Pedestrian target\n{0}")
+ self.add_shortcut(Shortcut("Show pedestrian line", pygame.K_w, pygame.KMOD_LSHIFT, waypoint.set_show_pedestrian_line, True, waypoint.get_show_pedestrian_line()), True, "Waypoint pedestrian\nline: {0}")
+ self.add_shortcut(Shortcut("Pause", pygame.K_SPACE, 0, self.set_pause, True, self.is_paused()), True, "Unpause", "Pause")
+ self.add_shortcut(Shortcut("Next target", pygame.K_RIGHT, 0, heatmap.next_target, False), True, "Next target\nheatmap")
+ self.add_shortcut(Shortcut("Previous target", pygame.K_LEFT, 0, heatmap.previous_target, False), True, "Previous target\nheatmap")
+ self.add_shortcut(Shortcut("Next pedestrian", pygame.K_DOWN, 0, path.next_pedestrian, False), True, "Next pedestrian")
+ self.add_shortcut(Shortcut("Previous pedestrian", pygame.K_UP, 0, path.previous_pedestrian, False), True, "Previous pedestrian")
+
+ def _set_show_names(self, state: bool) -> None:
+ for feature in self._features:
+ if hasattr(feature, "set_render_names"):
+ feature.set_render_names(state)
+
+ def get_target_color(self, target):
+ return self._target_colors[target]
+
+ def get_spawner_color(self, spawner):
+ return self._spawner_colors[spawner]
+
+ def handle_events(self):
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self._handle_quit()
+ if event.type == pygame.KEYDOWN:
+ self._handle_key_event(event.key, event.mod)
+ elif event.type == pygame.VIDEORESIZE:
+ w, h = event.dict['size']
+ self._handle_resize_event(w, h)
+ elif event.type == pygame.MOUSEMOTION:
+ self._handle_mouse_move_event(event.pos[0], event.pos[1])
+ elif event.type == pygame.MOUSEBUTTONDOWN:
+ self._handle_click_event(event.button, event.pos[0], event.pos[1])
+
+ def _handle_quit(self):
+ pygame.quit()
+ self._running = False
+
+ if self._serializer is not None:
+ self._serializer.close()
+
+ if self._flow_log is not None:
+ self._close_flow_log()
+
+
+ def _handle_key_event(self, key: int, mod: int) -> None:
+ # handle key press events, check for shortcuts
+ code = Shortcut.calculate_code(key, mod)
+ if code in self._shortcuts:
+ self._shortcuts[code].execute()
+
+ def _handle_resize_event(self, width: int, height: int) -> None:
+ # handle window resize, recalculate cell size and update grid
+ self._cell_size = self.calculate_cell_size(width, height)
+ self._max_feature_width = 0
+ pygame.display.update()
+ self._show_feature_details = self._screen.get_width() > 800
+
+ def _handle_mouse_move_event(self, x: int, y: int) -> None:
+ # handles hover logic for buttons
+ for button in self._buttons:
+ button.set_hovered(button.is_inside(x, y))
+
+ def _handle_click_event(self, button, click_x: int, click_y: int) -> None:
+ # handles click in visualisation
+ if button == 1:
+ if click_y < Visualisation.GRID_OFFSET:
+ if self._show_buttons:
+ self._handle_button_click(click_x, click_y)
+ else:
+ self._handle_grid_click(click_x, click_y)
+
+ def _handle_button_click(self, click_x: int, click_y: int) -> None:
+ # handle button click logic
+ if self._show_buttons:
+ for button in self._buttons:
+ if button.is_inside(click_x, click_y):
+ button.press()
+
+ def _handle_grid_click(self, click_x: int, click_y: int) -> None:
+ # check for clicked targets and update selected target
+ x, y = self._helper.screen_to_grid(click_x, click_y)
+ heatmap = self.get_feature(TargetHeatmapVisualisationFeature)
+ target = self.simulation.get_target(x, y)
+ if heatmap is not None and target is not None:
+ heatmap.set_target(target)
+ heatmap.set_enabled(True)
+
+ # check for clicked pedestrians and update selected pedestrian
+ path = self.get_feature(PathVisualisationFeature)
+ pedestrian = self.simulation.get_grid().get_cell(x, y).get_pedestrian()
+ if path is not None and pedestrian is not None:
+ path.set_pedestrian(pedestrian)
+ path.set_enabled(True)
+
+ def _init_flow_log(self):
+ if self._flow_log is not None:
+ setup_data = {
+ "time_resolution": self.simulation.get_time_resolution(),
+ "flow_meters": [meter.get_initial_data() for meter in self._flow_meters],
+ }
+ self._flow_log.write(f"{{\n \"setup\": {json.dumps(setup_data, indent=4)},\n \"flow_data\": [\n")
+
+ def _close_flow_log(self):
+ if self._flow_log is not None:
+ self._flow_log.write("\n]}")
+ self._flow_log.close()
+ self._flow_log = None
+
+ def _write_flow_log(self):
+ if self._flow_log is not None:
+ if self._is_first_flow_log:
+ self._is_first_flow_log = False
+ else:
+ self._flow_log.write(",\n")
+
+ for meter in self._flow_meters:
+ meter.update(self.simulation)
+
+ data = {
+ "step": self.simulation.get_steps(),
+ "time": self.simulation.get_run_time(),
+ "pedestrian_density": len(self.simulation.get_pedestrians()) / (self.simulation.get_grid().get_width() * self.simulation.get_grid().get_height()),
+ "flow": {meter.get_name(): meter.get_flow_rate() for meter in self._flow_meters}
+ }
+ self._flow_log.write(json.dumps(data, indent=4))
+
+ def update(self, delta) -> None:
+ # update simulation if not paused and bookkeep time delta
+ if self.is_paused() is False and self.simulation.is_done() is False:
+ self._simulation_delta += delta
+ if self._simulation_delta >= self.simulation.get_time_resolution():
+ self.simulation.update(self._simulation_delta)
+ self._simulation_delta = 0
+ self._write_flow_log()
+ if self._serializer is not None:
+ self._serializer.write_current_state()
+
+
+ def render_features(self, surface) -> None:
+ # render all visualisation features and their status messages
+ description_x = 0
+ description_y = 0
+ max_description_width = 0
+ pygame.draw.rect(surface, (255, 255, 255), Rect(0, 0, surface.get_width(), Visualisation.GRID_OFFSET))
+ for feature in self._features:
+ feature.render(surface)
+
+ if self._show_feature_details:
+ # only render if the feature is enabled if buttons are not shown since buttons already cover this information
+ texts, width, height = self._helper.get_multiline_texts(self._font, feature.describe_state(self._show_buttons == False), (0, 0, 0))
+ if description_y + height > Visualisation.GRID_OFFSET:
+ description_x += max_description_width + 20
+ description_y = 0
+ max_description_width = 0
+
+ for text in texts:
+ surface.blit(text, (description_x, description_y))
+ description_y += text.get_height()
+
+ max_description_width = max(max_description_width, width)
+ description_y += 20
+
+ self._max_feature_width = max(description_x + max_description_width, self._max_feature_width)
+
+ def render_buttons(self, dt, surface, x, y):
+ max_width = 0
+ for button in self._buttons:
+ button.update(dt)
+ if button.get_height() + y > Visualisation.GRID_OFFSET:
+ x += max_width + 10
+ max_width = 0
+ y = 10
+
+ button.set_position(x, y)
+ button.render(surface)
+ y += button.get_height() + 10
+ max_width = max(max_width, button.get_width())
+
+ def run(self):
+ dt = 0.0
+ while self._running:
+ self.handle_events()
+ self._screen.fill((40, 40, 40))
+ self.update(dt / 1000)
+ self.render_features(self._screen)
+ if self._show_buttons:
+ self.render_buttons(dt, self._screen, self._max_feature_width + 10, 10)
+ pygame.display.flip()
+ dt = self._clock.tick(self._fps)
+
+ pygame.quit()
diff --git a/visualisation/visualisation_feature.py b/visualisation/visualisation_feature.py
new file mode 100644
index 0000000..569bb1e
--- /dev/null
+++ b/visualisation/visualisation_feature.py
@@ -0,0 +1,89 @@
+from abc import ABC, abstractmethod
+from typing import Type
+
+from pygame import Surface
+
+from typing import TYPE_CHECKING
+from visualisation.visualisation_helper import VisualisationHelper
+
+if TYPE_CHECKING:
+ from simulation.core.simulation import Simulation
+ from visualisation.visualisation import Visualisation
+
+
+
+class VisualisationFeatureBase(ABC):
+ """
+ Base class for visualisation features. Visualisation features are used to generate visualisations based on the given grid
+ :param sim: the simulation the visualisation should be generated for
+ """
+ def __init__(self, sim: 'Simulation', vis: 'Visualisation', vis_helper: VisualisationHelper):
+ self._is_enabled = False
+ self._simulation = sim
+ self._visualisation = vis
+ self._helper: VisualisationHelper = vis_helper
+ self._text_color = (255, 255, 255)
+ self._font_name = "Arial"
+ self._mono_font_name = "Consolas"
+ self._small_font_size = 12 if self._helper is None else min(int(self._helper.get_cell_size() * 0.35), 12)
+ self._font_size = 18 if self._helper is None else min(int(self._helper.get_cell_size() * 0.75), 18)
+ pass
+
+ def enable(self) -> None:
+ """
+ Enables the visualisation feature
+ """
+ self._is_enabled = True
+
+ def disable(self) -> None:
+ """
+ Disables the visualisation feature
+ """
+ self._is_enabled = False
+
+ def set_enabled(self, enabled: bool) -> None:
+ """
+ Sets the state of the visualisation feature
+ :param enabled: the state of the visualisation feature
+ """
+ self._is_enabled = enabled
+
+ def is_enabled(self) -> bool:
+ """
+ Returns whether the visualisation feature is enabled
+ :return: whether the visualisation feature is enabled
+ """
+ return self._is_enabled
+
+ def render(self, surface: Surface) -> None:
+ """
+ Generates visualisation based on the given grid
+ :param surface: the surface the visualisation should be rendered onto
+ """
+ if self.is_enabled():
+ self._render(surface)
+
+ def describe_state(self, include_enabled: bool = True) -> str:
+ """
+ Describes the state of the visualisation feature
+ :param include_enabled: whether to include if the feature is enabled in the description as the first line
+ :return: a string describing the state of the visualisation feature
+ """
+ description = f"{self.__class__.__name__} is enabled: {self.is_enabled()}" if include_enabled else self.__class__.__name__
+ state_description = self._describe_state()
+ if state_description:
+ description += f"\n{state_description}"
+
+ return description
+
+ @abstractmethod
+ def _describe_state(self) -> str:
+ """
+ Describes the state of the visualisation feature
+ :return: a string describing the state of the visualisation feature
+ """
+ pass
+
+ @abstractmethod
+ def _render(self, surface: Surface) -> None:
+ pass
\ No newline at end of file
diff --git a/visualisation/visualisation_helper.py b/visualisation/visualisation_helper.py
new file mode 100644
index 0000000..6e54a58
--- /dev/null
+++ b/visualisation/visualisation_helper.py
@@ -0,0 +1,192 @@
+import math
+from typing import Tuple, List
+
+import numpy as np
+from pygame import Color, Surface, Rect
+from pygame.font import Font
+
+from simulation.core.position import Position
+import pygame
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from visualisation.visualisation import Visualisation
+
+
+class VisualisationHelper:
+ CURRENT_COLOR_ANGLE = 0
+
+ def __init__(self, vis: 'Visualisation'):
+ self._vis = vis
+
+ def get_cell_size(self):
+ return self._vis.get_cell_size()
+
+ def get_grid_top_offset(self):
+ return self._vis.GRID_OFFSET
+
+ def get_grid_left_offset(self):
+ return 0
+
+ def _transform_offset(self, pos: Tuple) -> Tuple:
+ return pos[0] + self.get_grid_left_offset(), pos[1] + self.get_grid_top_offset(), *pos[2:]
+
+ @staticmethod
+ def hsl_to_rgb(hsl: Tuple[float, float, float]) -> Tuple[int, int, int]:
+ color = Color(0)
+ color.hsla = np.clip(hsl[0], 0, 360), np.clip(hsl[1], 0, 100), np.clip(hsl[2], 0, 100), 100
+ return color.r, color.g, color.b
+
+ @staticmethod
+ def rgb_to_hsl(color: Tuple[int, int, int]) -> Tuple[float, float, float]:
+ color = Color(color)
+ return color.hsla[0], color.hsla[1], color.hsla[2]
+
+ @staticmethod
+ def translate_polygon_result(func):
+ def wrapper(*args):
+ self = args[0]
+ return [self._transform_offset(point) for point in func(*args)]
+
+ return wrapper
+
+ @staticmethod
+ def translate_rect_result(func):
+ def wrapper(*args):
+ self = args[0]
+ return self._transform_offset(func(*args))
+
+ return wrapper
+
+ @staticmethod
+ def get_random_color() -> (int, int, int):
+ angle = VisualisationHelper.CURRENT_COLOR_ANGLE
+ VisualisationHelper.CURRENT_COLOR_ANGLE += 23
+ color = Color(0)
+ color.hsla = angle, 100, 50, 100
+ return color.r, color.g, color.b
+
+ @staticmethod
+ def rotate(point: Tuple[float, float], origin: Tuple[float, float], angle: float) -> Tuple[float, float]:
+ ox, oy = origin
+ px, py = point
+ qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
+ qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
+ return qx, qy
+
+ @staticmethod
+ def mix_colors(color1, color2, ratio):
+ return tuple(int(c1 * ratio + c2 * (1 - ratio)) for c1, c2 in zip(color1, color2))
+
+ def screen_to_grid(self, x, y):
+ return x // self.get_cell_size(), (y - self.get_grid_top_offset()) // self.get_cell_size()
+
+ def get_rect_at(self, pos: Position) -> pygame.Rect:
+ return self.get_rect(pos.get_x(), pos.get_y())
+
+ @translate_rect_result
+ def get_rect(self, x: int, y: int) -> pygame.Rect:
+ cell_size = self.get_cell_size()
+ return pygame.Rect(x * cell_size, y * cell_size, cell_size, cell_size)
+
+ @translate_rect_result
+ def get_rect_with_size(self, x: int, y: int, width: int, height: int) -> pygame.Rect:
+ cell_size = self.get_cell_size()
+ return pygame.Rect(x * cell_size, y * cell_size, width * cell_size, height * cell_size)
+
+ def get_small_rect_at(self, pos: Position) -> pygame.Rect:
+ return self.get_small_rect(pos.get_x(), pos.get_y())
+
+ @translate_rect_result
+ def get_small_rect(self, x: int, y: int) -> pygame.Rect:
+ cell_size = self.get_cell_size()
+ cell_size_half = self.get_cell_size() // 2
+ cell_size_fourth = self.get_cell_size() // 4
+ return pygame.Rect(x * cell_size + cell_size_fourth, y * cell_size + cell_size_fourth, cell_size_half,
+ cell_size_half)
+
+ def get_centered_pos_at(self, pos: Position) -> Tuple[int, int]:
+ return self.get_centered_pos(pos.get_x(), pos.get_y())
+
+ @translate_rect_result
+ def get_centered_pos(self, x: int, y: int) -> Tuple[int, int]:
+ cell_size = self.get_cell_size()
+ cell_size_half = self.get_cell_size() // 2
+ return x * cell_size + cell_size_half, y * cell_size + cell_size_half
+
+ def get_x_center_pos_at(self, pos: Position, y_offset=0) -> Tuple[int, int]:
+ return self.get_x_centered_pos(pos.get_x(), pos.get_y(), y_offset)
+
+ @translate_rect_result
+ def get_x_centered_pos(self, x: int, y: int, y_offset=0) -> Tuple[int, int]:
+ cell_size = self.get_cell_size()
+ cell_size_half = self.get_cell_size() // 2
+ return x * cell_size + cell_size_half, y * cell_size + y_offset
+
+ def get_y_centered_pos_at(self, pos: Position, x_offset=0):
+ return self.get_y_centered_pos(pos.get_x(), pos.get_y(), x_offset)
+
+ @translate_rect_result
+ def get_y_centered_pos(self, x: int, y: int, x_offset=0) -> Tuple[int, int]:
+ cell_size = self.get_cell_size()
+ cell_size_half = self.get_cell_size() // 2
+ return x * cell_size + x_offset, y * cell_size + cell_size_half
+
+ def get_small_triangle_at(self, pos: Position, rotation: float = 0.0) -> List[Tuple[float, float]]:
+ return self.get_small_triangle(pos.get_x(), pos.get_y(), rotation)
+
+ @translate_polygon_result
+ def get_small_triangle(self, x: int, y: int, rotation: float = 0.0) -> list[tuple[float, float]]:
+ cell_size = self.get_cell_size()
+ cell_half_size = cell_size // 2
+ cell_fourth_size = cell_size // 4
+ origin = (x * cell_size + cell_half_size, y * cell_size + cell_half_size)
+ left = x * cell_size
+ bottom = y * cell_size + cell_fourth_size + cell_half_size
+ top = y * cell_size
+ a = (left + cell_fourth_size, bottom)
+ b = (left + cell_fourth_size + cell_half_size, bottom)
+ c = (left + cell_half_size, top + cell_fourth_size)
+
+ triangle = [a, b, c]
+ return [VisualisationHelper.rotate(point, origin, rotation + math.pi / 2) for point in triangle]
+
+ @translate_rect_result
+ def get_top_left(self):
+ return (0, 0)
+
+ @staticmethod
+ def get_multiline_texts(font: Font, text: str, color: Tuple) -> Tuple[list[Surface], int, int]:
+ lines = text.split("\n")
+ max_width = 0
+ total_height = 0
+ surfaces = []
+ for line in lines:
+ text = font.render(line, True, color)
+ surfaces.append(text)
+ total_height += text.get_height()
+ max_width = max(max_width, text.get_width())
+
+ return surfaces, max_width, total_height
+
+ @staticmethod
+ def render_multiline_text(surface: Surface, rect: Rect, font: Font, text: str, color: Tuple, center_x: bool = False, center_y: bool = False):
+ lines, max_width, total_height = VisualisationHelper.get_multiline_texts(font, text, color)
+ off_y = (rect.height - total_height) // 2 if center_y else 0
+ for line in lines:
+ off_x = (max_width - line.get_width()) // 2 if center_x else 0
+ surface.blit(line, (rect.x + off_x, rect.y + off_y))
+ off_y += line.get_height()
+
+ @staticmethod
+ def render_multiline_text_to_surface(font: Font, text: str, color: Tuple, center_x: bool = False) -> Surface:
+ lines, max_width, total_height = VisualisationHelper.get_multiline_texts(font, text, color)
+ surface = Surface((max_width, total_height), pygame.SRCALPHA)
+ off_y = 0
+ for line in lines:
+ off_x = (max_width - line.get_width()) // 2 if center_x else 0
+ surface.blit(line, (off_x, off_y))
+ off_y += line.get_height()
+
+ return surface
\ No newline at end of file