diff --git a/Application/Cell.py b/Application/Cell.py index 6f6ebf6..1aae968 100644 --- a/Application/Cell.py +++ b/Application/Cell.py @@ -1,18 +1,19 @@ import random import math - +import numpy as np from decorator import log_decorator # one file per cell? class Cell: #Grundbaustein, jede Zelle kennt seine Position auf dem Grid und den entsprechenden state - def __init__(self, row, col,cell_size, state=0): + def __init__(self, row, col,cell_size, state=0, is_passable=True): self.row = row # Store the row position self.col = col # Store the column position self.state = state # 0 for dead/inactive, 1 for alive/active self.cell_size = cell_size + self.is_passable = is_passable #Methode welche die naheliegendste Target Zelle anhand einer target list (Tuples aus Koordinaten) sucht @log_decorator @@ -61,123 +62,114 @@ def get_neighbors(self, grid, radius=2): return neighbors @log_decorator + def valid_neighbors(self, neighbors): + valid_neighbors = [ + cell for layer in neighbors.values() + for cell in layer + if cell.is_passable + ] + return valid_neighbors + @log_decorator def euclidean_distance_to(self, other): #Euklidische Distanz zwischen Zwei Zellen return math.sqrt((self.row - other.row) ** 2 + (self.col - other.col) ** 2) *self.cell_size + def manhattan_difference_to(self, other_cell): + """Calculate the height and length difference between this cell and another cell.""" + height_diff = abs(self.row - other_cell.row) + length_diff = abs(self.col - other_cell.col) + return height_diff, length_diff + #Jedes Feld hat einen Potentialwert zu der naheliegendsten Target Zelle @log_decorator - + #Alte Funktion welche bein den ersten Versionen verwendet wurde. Jede zelle hatte als potenzial die negative euklidische Distanz zum nächstgelegenem Ziel def potential(self, grid, target_list): """Calculate potential based on the negative Euclidean distance to the target cell.""" target = self.find_target(target_list) - #print(f"TARGET:{target[0]},{target[1]}") - #print(f"SELF: {self.row}, {self.col}") # Euclidean distance calculation ( Check if row and col are right) distance = self.euclidean_distance_to(target) # Return the negative distance as potential return -distance - #is_passable (Kann ich von einem Agenten besucht werden). Evtl als Variable statt Methode? - # check naming - def is_passable(self): - return True # Most cells are passable by default + #Momentane Visualisierung noch in Konsole mit string repr. Für später dann Plots mit Daten aller Timesteps def __repr__(self): return "0" # Randzellen die das Feld umschliessen (etwa im Fall eines Raums mit Türen kann ein Border plaziert und danach Targets als Türen auf dem Border definiert werden) -class BorderCell(Cell): - def __init__(self,row, col, cell_size): - super().__init__(state=1, row=row, col=col, cell_size=cell_size) # Border cells are always active +#class BorderCell(Cell): + # def __init__(self,row, col, cell_size, is_passable=False): + # super().__init__(state=1, row=row, col=col, cell_size=cell_size, is_passable=is_passable) # Border cells are always active - def is_passable(self): - return False # Border cells are impassable - - def __repr__(self): - return 'B' + # def __repr__(self): + # return 'B' #Hindernisse auf dem Feld class ObstacleCell(Cell): - def __init__(self, row, col, cell_size): - super().__init__(state=4, row=row, col=col, cell_size=cell_size) - def is_passable(self): - return False #Obstacles are impassable + def __init__(self, row, col, cell_size, is_passable=False): + super().__init__(state=4, row=row, col=col, cell_size=cell_size, is_passable=False) + def __repr__(self): return '%' # Spawn Zelle. Generiert pro Zeitschritt eine vordefinierte Anzahl agenten auf seinen Moore Nachbar Zellen class SpawnCell(Cell): - def __init__(self, row, col, cell_size): - super().__init__(state=2, row=row, col=col, cell_size=cell_size) # Spawn cells are active + def __init__(self, row, col, cell_size, spawn_rate, is_passable=False): + super().__init__(state=2, row=row, col=col, cell_size=cell_size, is_passable=False) + self.spawn_rate = spawn_rate + # Spawn cells are active #Spawn eine definierte Anzahl Agenten auf deinen Moore Nachbarn und füge die neuen Agenten der grid.agents liste Hinzu def spawn_agents(self, grid, max_agents): - neighbors = self.get_neighbors(grid, radius=1) - valid_neighbors = [ - cell for layer in neighbors.values() - for cell in layer - if not grid.is_cell_occupied(cell.row, cell.col) and cell.is_passable() - ] - + valid_neighbors = self.valid_neighbors(self.get_neighbors(grid, radius=1)) random.shuffle(valid_neighbors) chance = random.randrange(0,1) agents_to_spawn = min(max_agents, len(valid_neighbors)) - if chance < 0.8: + #Check the random chance against a set rate in order to control how + if chance <= self.spawn_rate: for _ in range(agents_to_spawn): cell = valid_neighbors.pop(0) row, col = cell.row, cell.col agent = Agent(row, col, cell_size=self.cell_size) + agent.group = random.choice([0, 1]) # NEUE ZEILE grid.grid[row][col] = agent grid.agents.append(agent) - def is_passable(self): - return False #Agenten können nicht Spawnzellen laufen + def __repr__(self): return 'S' # Represent spawn cells with 'S' #Ziele: Agenten bewegen sich auf die Ziele class TargetCell(Cell): - def __init__(self, row, col, cell_size): - super().__init__(state=3, row=row, col=col, cell_size=cell_size) # Target cells are active + def __init__(self, row, col, cell_size, is_passable=True): + super().__init__(state=3, row=row, col=col, cell_size=cell_size, is_passable=is_passable) # Target cells are active + - def is_passable(self): - return True # Targets are passable to agents def __repr__(self): return 'T' # Represent target cells with 'T' class Agent(Cell): - def __init__(self, row, col, cell_size): - super().__init__(state=47, row=row, col=col, cell_size=cell_size) # Set the agent state as before + def __init__(self, row, col, cell_size,is_passable=False): + super().__init__(state=47, row=row, col=col, cell_size=cell_size, is_passable=is_passable) # Set the agent state as before self.arrived = False #velocity wird später verwendet um die Gehgeschwindigkeit der einzelnen Agenten zu verändern - self.velocity = random.uniform(0.75, 1.5) + #self.velocity = random.uniform(0.5, 1.5) self.id = self.__hash__() self.route = [] - self.movement_range = self.velocity - #Momentan nicht in Verwendung da wir Ziele als Liste führen und nicht immer als Grid Search finden müssen - # def find_nearest_target(self, grid): - # """Find the nearest TargetCell on the grid to this agent's current position.""" - # min_distance = float('inf') - # nearest_target = None - # - # for target_row in range(grid.rows): - # for target_col in range(grid.cols): - # cell = grid.grid[target_row][target_col] - # if isinstance(cell, TargetCell): - # # Calculate Euclidean distance using self.row and self.col - # distance = math.sqrt((target_row - self.row) ** 2 + (target_col - self.col) ** 2) - # if distance < min_distance: - # min_distance = distance - # nearest_target = (target_row, target_col) - # - # return nearest_target # Returns (target_row, target_col) or None if no TargetCell is found + self.target = None + self.group = None + self._original_velocity = random.uniform(0.5, 1.5) + self._original_movement_range = self.original_velocity + self.velocity = self._original_velocity + self.movement_range = self._original_movement_range + self.idle = None + def log_state(self, timestep, log_file="logs/agent_states.log"): """Log the agent's state to a file.""" @@ -186,13 +178,20 @@ def log_state(self, timestep, log_file="logs/agent_states.log"): f"Timestep: {timestep}, Agent ID: {self.id}, Position: (row: {self.row}, col: {self.col}), Route: {self.route}\n " f"State: {self.state}, Arrived: {self.arrived}, Velocity: {self.velocity}\n" ) + @property + def original_velocity(self): + """Access the original velocity.""" + return self._original_velocity + @property + def original_movement_range(self): + """Access the original movement range.""" + return self._original_movement_range + def restore_velocity(self): + self.velocity = self._original_velocity + def restore_movement_range(self): + self.movement_range = self._original_movement_range - - - def is_passable(self): - return False # Agents are impassable (to other agents, for instance) - #Momentan noch nicht in Verwendung aber wird für die Sichtlinie zum Ziel verwendet def line_of_sight(self, grid): target = self.find_nearest_target(grid) @@ -207,7 +206,7 @@ def line_of_sight(self, grid): return True # Line of sight is clear - #bresenham_line fürht eine Liste aller Zellen die auf der line_of_sight zum Ziel sind (um Sichtkontakt zum Ziel zu prüfen) + #bresenham_line fürht eine Liste aller Zellen die auf der line_of_sight zum Ziel sind (um Sichtkontakt zum Ziel zu prüfen). Könnte def bresenham_line(self, x1, y1, x2, y2): """Bresenham's Line Algorithm to calculate all cells between two points.""" cells = [] @@ -230,149 +229,126 @@ def bresenham_line(self, x1, y1, x2, y2): y1 += sy return cells - - @log_decorator - def social_force(self, grid): - print("entering social force") - neighbors = self.get_neighbors(grid, radius=2) # Get neighbors within the radius - total_neighbors = sum(len(cells) for cells in neighbors.values()) - penalty = 0 # Initialize the penalty accumulator - penalty_factor = 0.6 - agent_cells = [] # List to store agent cells - for distance, cells in neighbors.items(): # neighbors are grouped by distance layers - for cell in cells: - if isinstance(cell, Agent): # Check if the cell contains an agent - agent_cells.append(cell) - - # Calculate the percentage of agents among all neighbors - - - #Sollten wir wo anders abfangen, bei get_neighbors --> darf nicht 0 returnen - if total_neighbors == 0: # Avoid division by zero - return 0 # No penalty if no neighbors - agent_percentage = len(agent_cells) / total_neighbors - - # Apply penalty only if at least 40% of neighbors are agents - # if agent_percentage < 0.2: - # return 0 # No penalty applied if less than 40% are agents - - # Calculate penalty - penalty = 0 # Initialize the penalty accumulator - for agent in agent_cells: - euclidean_distance = self.euclidean_distance_to(agent) # Calculate distance - - if euclidean_distance > 0: # Avoid division by zero for self - penalty_contribution = 1 / euclidean_distance # Inverse distance penalty - penalty += penalty_contribution - print(f"Social force penalty for {self.__hash__()} is {penalty}") - return penalty + # Bewegungslogik --> Untestehend sind die 3 Methoden über welche die Bewegung gesteuert wird. + # adjust_movement_range --> Erhöht die movement_range um die momentane velocity (ich komme in t+1 nicht an, also warte ich und bei t+2 kann ich das Ziel erreichen) + # movement_decision --> Basierend auf den social penalties und der distance map wird das beste ziel gewählt. Falls der aktuelle standort bestes ziel ist wird None returned (bei None wird sich nicht bewegt) + # movement_towards_target --> nutzt die beiden oberen methoden um sich zum Ziel zu bewegen. Ist die euklidische Distanz zu gross erhähen wir die movement range für t+1 mit adjust_movement_range. Ist dies der Fall wird der Agent auf "idle" gestellt + # sobald der Agent sich bewegt hat wird der "idle" Modus beendet und die Movement range sowie geschwindigkeit sollten wieder zum standardwert zurückkehren + def adjust_movement_range(self): + # Methode wird aufgerufen wenn Ziel nicht in einem Zeitschritt erreicht werden kann --> + # print(self.idle) + if self.idle: + # print(f"Original Range{self._original_movement_range} and Original velocity{self._original_velocity}, adjusted velocity was {self.velocity} and movement was {self.movement_range}") + if self.velocity <= self.cell_size: + self.velocity += self.cell_size + + self.movement_range += self.velocity + + print("MOVEMENT INCREASE") + print(f"new range is{self.movement_range}") + elif not self.idle: + # We enter this part when the agent was able to move after being idle(for example stuck in crowd) --> We reset velocity and movement range to original states + self.movement_range = self.original_movement_range + print(f"MOVEMENT RESET: new range is {self.movement_range} and velocity is {self.velocity}") + self.velocity = self.original_velocity + + def movement_decision(self, grid, precomputed_penalties, agent_index): + """ + Decide the next move for the agent. Mark as arrived if reaching the target. + Returns the new position (row, col) or None if no movement. + """ + if self.arrived: + return None - @log_decorator - def increase_movement_range(self): - #Methode wird aufgerufen wenn Ziel nicht in einem Zeitschritt erreicht werden kann --> - self.movement_range = self.velocity + self.movement_range - return self.movement_range + if self.target is None: + self.target = self.find_target(grid.target_cells) + if not self.target: + return None - #Bewegungslogik - # sure this method does make sense here from a architectural point of view? - def movement_towards_target(self, grid): + target_key = (self.target[0], self.target[1]) + distance_map = grid.dijkstra_distance_maps.get(target_key) or grid.flood_fill_distance_maps.get(target_key) - """ - Use precomputed distance maps to move toward the target. - Supports flood-fill or Dijkstra-based maps. - """ + if not distance_map: + return None + + valid_neighbors = self.valid_neighbors(self.get_neighbors(grid, radius=1)) + valid_neighbors.append(self) # Include staying in place as an option + + best_move = self + smallest_cost = float('inf') + + for neighbor in valid_neighbors: + distance_to_target = distance_map[neighbor.row][neighbor.col] + social_penalty = precomputed_penalties[agent_index] + staying_penalty = 4 if neighbor == self else 0 + + # Reduce weight of social penalties near the target + if isinstance(grid.grid[neighbor.row][neighbor.col], TargetCell): + social_penalty *= 0.5 # Halve the effect of social penalties near the target + random_bias = random.uniform(-0.5, 0.5) + total_cost = distance_to_target + random_bias + social_penalty + staying_penalty + + if total_cost < smallest_cost: + smallest_cost = total_cost + best_move = neighbor + # Set lowest possible velocity before reducing it any further to 0.40, graceful penalty only + print(f"Agent{self.id} velocity before penalty is {self.velocity}. Social penalty is {social_penalty}") + if self.velocity >= 0.40: + if social_penalty is not None: + self.velocity = self.velocity - (social_penalty / 4) + if self.velocity <= 0: + # we dont want negative velocities + self.velocity = abs(self.velocity) + self.movement_range = self.velocity + + print(f"new velocity for {agent_index} is {self.velocity}") + + # Mark as arrived if moving onto the target + if isinstance(grid.grid[best_move.row][best_move.col], TargetCell): + self.arrived = True + return None + + return (best_move.row, best_move.col) if best_move != self else None + + def movement_towards_target(self, grid, precomputed_penalties, index, timestep): if self.arrived: return - # Determine the target and select the appropriate distance map target = self.find_target(grid.target_cells) - if not target: - return + # print(f"Agent {self.id} moving towards target {target}") - target_key = (target[0], target[1]) # Coordinates of the target - - if grid.movement_method=="dijkstra": - distance_map = grid.dijkstra_distance_maps.get(target_key) - - # Find the best move - neighbors = self.get_neighbors(grid, radius=1) - valid_neighbors = [ - cell for layer in neighbors.values() - for cell in layer - if not isinstance(cell, ObstacleCell) and not grid.is_cell_occupied(cell.row, cell.col) and not isinstance(cell, TargetCell) and cell.is_passable() - ] - - # Include the agent's current position as an option - valid_neighbors.append(self) - - # Determine the neighbor with the smallest distance to the target - best_move = self - smallest_distance = distance_map[self.row][self.col] - - for neighbor in valid_neighbors: - distance = distance_map[neighbor.row][neighbor.col] - if distance < smallest_distance: - smallest_distance = distance - best_move = neighbor - - # Move to the best neighbor - if best_move != self: - grid.grid[self.row][self.col] = Cell(self.row, self.col,cell_size=self.cell_size) # Clear current position - grid.grid[best_move.row][best_move.col] = self # Update agent position - self.row, self.col = best_move.row, best_move.col - - # Mark as arrived if adjacent to the target - if smallest_distance == grid.cell_size: - self.arrived = True - grid.agents.remove(self) - grid.grid[self.row][self.col] = Cell(self.row, self.col, cell_size=self.cell_size) - print(f"Agent at ({self.row}, {self.col}) has arrived at the target.") - elif grid.movement_method=="floodfill": - distance_map = grid.flood_fill_distance_maps.get(target_key) - #print(distance_map) - neighbors = self.get_neighbors(grid, radius=1) - valid_neighbors = [ - cell for layer in neighbors.values() - for cell in layer - if not isinstance(cell, ObstacleCell) and not isinstance(cell, TargetCell) and not grid.is_cell_occupied(cell.row, cell.col) - ] - - # Include the agent's current position as an option - valid_neighbors.append(self) - - # Determine the neighbor with the smallest distance to the target - best_move = self - smallest_distance = distance_map[self.row][self.col] - - - - for neighbor in valid_neighbors: - neighbor_distance = distance_map[neighbor.row][neighbor.col] - if neighbor_distance < smallest_distance: - smallest_distance = neighbor_distance - best_move = neighbor - - # Move to the best neighbor - if best_move != self: - # Clear current position - grid.grid[self.row][self.col] = Cell(self.row, self.col, cell_size=self.cell_size) - # Update agent position - grid.grid[best_move.row][best_move.col] = self - self.row, self.col = best_move.row, best_move.col - - # Check if the agent has reached the target - if smallest_distance == 0: # Reached the target - self.arrived = True - grid.agents.remove(self) - grid.grid[self.row][self.col] = Cell(self.row, self.col, cell_size=self.cell_size) - print(f"Agent at ({self.row}, {self.col}) has arrived at the target.") - if not distance_map: - return # No distance map available + valid_neighbors = self.valid_neighbors(self.get_neighbors(grid, radius=1)) + if not valid_neighbors: + # print(f"Agent {self.id} has no valid neighbors and is stuck.") + return + best_move = self + new_best_move = self.movement_decision(grid, precomputed_penalties, index) + if new_best_move is None: + print(f"Agent {self.id} at position {self.row, self.col} has no best move") + return + # print(new_best_move) + # print(self.euclidean_distance_to(grid.grid[new_best_move[0]][new_best_move[1]])) + if new_best_move != self and self.euclidean_distance_to(grid.grid[new_best_move[0]][new_best_move[1]]) <= self.movement_range: + grid.grid[self.row][self.col] = Cell(self.row, self.col, cell_size=self.cell_size) + grid.grid[new_best_move[0]][new_best_move[1]] = self + self.row, self.col = new_best_move[0], new_best_move[1] + self.route.append((new_best_move[0], new_best_move[1])) + # print(f"Agent {self.id} moved to ({self.row}, {self.col})") + self.idle = False + self.log_state(timestep) + self.adjust_movement_range() + + elif new_best_move != self and self.euclidean_distance_to(grid.grid[new_best_move[0]][new_best_move[1]]) > self.movement_range: + self.idle = True + self.log_state(timestep) + print( + f"Agent{self.id}: Range {self.movement_range} to short, adjusting. Distance is{self.euclidean_distance_to(grid.grid[new_best_move[0]][new_best_move[1]])}") + self.adjust_movement_range() def __repr__(self): return 'A' # Represent agent with 'A' diff --git a/Application/Grid.py b/Application/Grid.py index 81b8538..fc04aed 100644 --- a/Application/Grid.py +++ b/Application/Grid.py @@ -1,5 +1,5 @@ -from Cell import Cell, BorderCell, SpawnCell, Agent, TargetCell, ObstacleCell +from Cell import Cell, SpawnCell, Agent, TargetCell, ObstacleCell import random import os @@ -10,13 +10,17 @@ import matplotlib.colors as mcolors import math import numpy as np +from concurrent.futures import ThreadPoolExecutor +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns #Helper function for convert def clamp(value, min_value, max_value): return max(min(value, max_value), min_value) #Grid Klasse: Auf dem Grid befinden sich Zellobjekte und über das Grid wird das update() der Zellen durchgeführt class Grid: #Im Init wird das grid entsprechend aufgebaut, es können Listen mit Tuplen für die entsprechenden Zell Objekte mitgegeben werden - def __init__(self, length, height, spawn_cells, target_cells, obstacle_cells, cell_size=1.0, movement_method="dijkstra"): + def __init__(self, length, height, spawn_cells, target_cells, obstacle_cells, cell_size=1.0, movement_method="dijkstra", spawn_rate=1): self.length = length self.height = height self.cell_size = cell_size @@ -25,16 +29,20 @@ def __init__(self, length, height, spawn_cells, target_cells, obstacle_cells, ce self.grid = [ [Cell(row, col, cell_size=cell_size) for col in range(self.cols)] for row in range(self.rows) ] # Aufbau Grid - self.spawn_cells = spawn_cells # Listen für Spawns, Ziele und Hindernisse + self.spawn_cells = spawn_cells + self.spawn_rate = spawn_rate# Listen für Spawns, Ziele und Hindernisse self.target_cells = target_cells self.obstacle_cells = obstacle_cells self.agents = [] # Liste mit allen Agenten die sich auf dem Feld befinden self.movement_method = movement_method + self.density_data = [] + self.speed_data = [] + self.flow_data = [] # Aufbau von Spawn, Zielen und Hindernissen for row, col in spawn_cells: #print(row, col) - self.grid[row][col] = SpawnCell(row=row, col=col, cell_size=cell_size) + self.grid[row][col] = SpawnCell(row=row, col=col, cell_size=cell_size,spawn_rate=self.spawn_rate) if obstacle_cells is not None: for row, col in obstacle_cells: #print(obstacle_cells) @@ -69,10 +77,9 @@ def select_area_by_coordinates(self, start_x, start_y, end_x, end_y): List[Cell]: A list of Cell objects within the selected area. """ # Convert coordinates to grid indices using clamp - start_row = clamp(int(start_y / self.cell_size), 0, self.rows - 1) - start_col = clamp(int(start_x / self.cell_size), 0, self.cols - 1) - end_row = clamp(int(end_y / self.cell_size), 0, self.rows - 1) - end_col = clamp(int(end_x / self.cell_size), 0, self.cols - 1) + start_row, start_col = self.meter_to_rowcol(start_x, start_y) + end_row, end_col = self.meter_to_rowcol(end_x, end_y) + # Ensure start indices are less than or equal to end indices if start_row > end_row or start_col > end_col: @@ -86,18 +93,12 @@ def select_area_by_coordinates(self, start_x, start_y, end_x, end_y): ] return selected_cells - #Plaziere Wand um Feld - def place_border(self): - """Place a border around the grid""" - for r in range(self.rows): - for c in range(self.cols): - if r == 0 or r == self.rows - 1 or c == 0 or c == self.cols - 1: - self.grid[r][c] = BorderCell(cell_size=self.cell_size) + #Die Untenstehenden methoden erlauben eine Interaktion mit dem Grid ausserhalb der initialisierung def place_spawn_cell(self, row,col): """Place a spawn cell at a specific position on the grid""" #row, col = self.meter_to_rowcol(x, y) - self.grid[row][col] = SpawnCell(row=row, col=col, cell_size=self.cell_size) + self.grid[row][col] = SpawnCell(row=row, col=col, cell_size=self.cell_size, spawn_rate=self.spawn_rate) self.spawn_cells.append((row, col)) def place_empty_cell(self, row,col): @@ -115,6 +116,7 @@ def place_agent(self, row,col): """Place an agent at a specific position on the grid""" #row, col = self.meter_to_rowcol(x, y) agent = Agent(row, col, cell_size=self.cell_size) + agent.group = random.choice([0, 1]) if isinstance(self.grid[row][col], Cell) and not isinstance(self.grid[row][col], TargetCell): self.grid[row][col] = agent self.agents.append(agent) @@ -128,16 +130,40 @@ def is_cell_occupied(self,row,col): cell = self.grid[row][col] return isinstance(cell, Agent) + #Methoden für die Social Penalty berechnung (mit numpy für bessere leistung da anfänglich zu langsam) + def get_agent_positions(self): + """ + Extract the positions of all agents as a NumPy array. + Returns: + np.ndarray: Array of shape (num_agents, 2) with rows [row, col]. + """ + return np.array([[agent.row, agent.col] for agent in self.agents]) - #Helper Methode für Djkstra Algorithmus - def compute_distance_map(self, target): + def get_agent_arrival_status(self): """ - Compute the shortest path distances from the target to all cells using Dijkstra's algorithm. - This version calculates direct Euclidean distance for each cell to avoid cumulative errors. + Extract whether each agent has arrived. + Returns: + np.ndarray: Boolean array of shape (num_agents,) indicating arrival status. + """ + return np.array([agent.arrived for agent in self.agents]) + + # def get_agent_velocities(self): + # """ + # Extract the velocities of all agents. + # Returns: + # np.ndarray: Array of shape (num_agents,) with agent velocities. + # """ + # return np.array([agent.velocity for agent in self.agents]) + + + #Methode für Djkstra Algorithmus + def dijkstra_map(self, target): + """ + Compute the shortest path distances from the target to all cells using Dijkstra's algorithm. + This version allows setting distances on Agent cells but prohibits Obstacle cells. Parameters: target (tuple): Coordinates of the target cell as (row, col). - Returns: List[List[float]]: A 2D distance map where each cell contains the shortest distance to the target. """ @@ -161,42 +187,42 @@ def compute_distance_map(self, target): if current_distance > distance_map[current_row][current_col]: continue - # Get the current cell and its neighbors - current_cell = self.grid[current_row][current_col] - neighbors = current_cell.get_neighbors(self, radius=1) + # Get neighbors of the current cell + neighbors = self.grid[current_row][current_col].get_neighbors(self, radius=1) for layer in neighbors.values(): for neighbor in layer: neighbor_row, neighbor_col = neighbor.row, neighbor.col - # Skip impassable cells - if not neighbor.is_passable(): + # Skip obstacle cells + if isinstance(self.grid[neighbor_row][neighbor_col], ObstacleCell): continue - # Calculate the direct Euclidean distance from the target + # Allow distances on Agent cells + if isinstance(self.grid[neighbor_row][neighbor_col], Agent): + pass # Agents are treated as passable for distance purposes + + # Calculate the direct Euclidean distance direct_distance = math.sqrt((target_row - neighbor_row) ** 2 + (target_col - neighbor_col) ** 2) - #direct_distance_2 = self.grid[target_row][target_col].euclidean_distance(neighbor) - #print (direct_distance_2, direct_distance) - # If the calculated distance is shorter, update and enqueue the neighbor + # Update the neighbor's distance if this path is shorter if direct_distance < distance_map[neighbor_row][neighbor_col]: distance_map[neighbor_row][neighbor_col] = direct_distance heapq.heappush(queue, (direct_distance, neighbor_row, neighbor_col)) return distance_map - #Helper Methode für Flood Fill, returned Distance map - def flood_fill(self, target_row, target_col, target_state): + #Methode für Flood Fill, returned Distance map + + def flood_fill_map(self, target_row, target_col, target_state): """ Perform reverse flood-fill starting from the target (target_row, target_col). Computes a distance map where cells closer to the target have smaller values. Skips impassable cells (e.g., obstacles, borders). - Parameters: target_row (int): Row index of the target. target_col (int): Column index of the target. target_state (int): State of the target cell. - Returns: List[List[float]]: A 2D distance map with Manhattan distances to the target. """ @@ -207,7 +233,7 @@ def flood_fill(self, target_row, target_col, target_state): target_cell = self.grid[target_row][target_col] # Early exit if the target cell doesn't match the target state or is impassable - if target_cell.state != target_state or not target_cell.is_passable(): + if target_cell.state != target_state or not target_cell.is_passable: raise ValueError("Target cell is invalid or impassable.") # Initialize the distance map with infinity @@ -224,60 +250,171 @@ def flood_fill(self, target_row, target_col, target_state): current_row, current_col = queue.pop(0) current_distance = distance_map[current_row][current_col] - # Get neighbors of the current cell (4 directions: up, down, left, right) for dr, dc in directions: neighbor_row, neighbor_col = current_row + dr, current_col + dc - # Skip if out of bounds + # Skip out-of-bounds cells if not (0 <= neighbor_row < self.rows and 0 <= neighbor_col < self.cols): continue neighbor_cell = self.grid[neighbor_row][neighbor_col] - # Skip impassable cells or already visited cells - if not neighbor_cell.is_passable() or distance_map[neighbor_row][neighbor_col] != float('inf'): + # Skip obstacle cells + if isinstance(neighbor_cell, ObstacleCell): continue - # Update the distance for the neighbor - distance_map[neighbor_row][neighbor_col] = current_distance + 1 - queue.append((neighbor_row, neighbor_col)) + # Allow distances on Agent cells + if isinstance(neighbor_cell, Agent): + pass # Agents are treated as passable for flood-fill purposes + + # Update the distance for the neighbor if not visited + if distance_map[neighbor_row][neighbor_col] == float('inf'): + distance_map[neighbor_row][neighbor_col] = current_distance + 1 + queue.append((neighbor_row, neighbor_col)) return distance_map - # Display Methode: Momentan noch in Konsole, später mit Plots + # Konsolenausgabe def display(self): """Print the current state of the grid""" for row in self.grid: print(" ".join(str(cell) for cell in row)) print() - #Update funktion: Wir müssen nur die Agenten bewegen und die Spawns für den nächsten Zeitschritt durchführen + #Berechne Social Penalties für gesamtes Grid + def compute_social_penalties(grid, cutoff_distance=1.5, penalty_decay_factor=0.5): + """ + Compute social penalties for all agents using a vectorized approach. + Parameters: + grid (Grid): The simulation grid. + cutoff_distance (float): Maximum distance in meters to consider for social penalties. + penalty_decay_factor (float): Controls the steepness of the Gaussian decay. + Returns: + np.ndarray: Social penalties for all agents. + """ + positions = grid.get_agent_positions() # Shape: (num_agents, 2) + # If positions is 1D, reshape it --> Maybe this part here causes some of the Agents to get locked in place, not sure tho + if positions.ndim == 1: + positions = positions.reshape(-1, 2) + arrived = grid.get_agent_arrival_status() # Shape: (num_agents,) + num_agents = positions.shape[0] + + # Compute pairwise Euclidean distances (broadcasting) + deltas = positions[:, None, :] - positions[None, :, :] # Expected Shape: (num_agents, num_agents, 2) + distances = np.linalg.norm(deltas, axis=2) # Expected Shape: (num_agents, num_agents) + + # Apply cutoff distance (set penalties to 0 beyond this distance) + mask = (distances <= cutoff_distance) & ~np.eye(num_agents, dtype=bool) # Ignore self-distances + distances[~mask] = np.inf + + # Apply Gaussian decay penalty + penalties = np.exp(-(distances ** 2) / (2 * penalty_decay_factor ** 2)) # Shape: (num_agents, num_agents) + penalties[~mask] = 0 # Ensure penalties are 0 for distances > cutoff + + # Sum penalties for each agent + total_penalties = penalties.sum(axis=1) # Shape: (num_agents,) + + return total_penalties + + + + + def update(self, target_list, timestep): - - if timestep%4 == 0: + """ + Update the grid by moving agents and handling arrivals. + """ + if timestep == 0: self.update_distance_maps() - - #Bewege Agenten - for agent in self.agents: - if agent.arrived == True: - self.agents.remove(agent) - #print(agent) - if self.movement_method == "floodfill": - agent.movement_towards_target(self) - elif self.movement_method == "dijkstra": - agent.movement_towards_target(self) # Pass the grid instance - agent.log_state(timestep) + # Compute social penalties + precomputed_penalties = self.compute_social_penalties() + + # List to track agents to remove + agents_to_remove = [] - #Spawne Agenten ( + for i, agent in enumerate(self.agents[:]): # Iterate over a copy of the agents list + if agent.arrived: + agents_to_remove.append(agent) + continue + + # Call the agent's movement logic + #print(f"Agent {agent.id} moving from ({agent.row}, {agent.col})") + agent.movement_towards_target(self, precomputed_penalties, i, timestep=timestep) + + # Debugging + #print(f"Agent {agent.id} now at ({agent.row}, {agent.col})") + + # Remove agents that have arrived + for agent in agents_to_remove: + #print(f"Removing agent {agent} from ({agent.row}, {agent.col})") + self.agents.remove(agent) + + # Restore target cell explicitly (We Had alot of issues with Targets being overwritten, so we try to catch it at multiple levels) + if (agent.row, agent.col) in target_list: + self.grid[agent.row][agent.col] = TargetCell(agent.row, agent.col, self.cell_size) + else: + self.grid[agent.row][agent.col] = Cell(agent.row, agent.col, self.cell_size) + + # Spawn new agents for row, col in self.spawn_cells: cell = self.grid[row][col] - if isinstance(cell, SpawnCell): # Check if the cell at (row, col) is a SpawnCell - max_agents = 1 # Adjust the number of agents to spawn as needed + if isinstance(cell, SpawnCell): + max_agents = 1 cell.spawn_agents(self, max_agents) self.log_grid_state(timestep) + def update_no_penalties(self, target_list, timestep): + """ + Update the grid by moving agents and handling arrivals. + """ + if timestep == 0: + self.update_distance_maps() + # Compute social penalties + precomputed_penalties = self.compute_social_penalties() + # List to track agents to remove + agents_to_remove = [] + for i, agent in enumerate(self.agents[:]): # Iterate over a copy of the agents list + if agent.arrived: + agents_to_remove.append(agent) + continue + new_position = agent.movement_decision(self, precomputed_penalties, i) + if new_position: + current_row, current_col = agent.row, agent.col + new_row, new_col = new_position + # Update grid: Move the agent + if not isinstance(self.grid[new_row][new_col], TargetCell): + self.grid[new_row][new_col] = agent + # Restore the current cell + self.grid[current_row][current_col] = ( + TargetCell(current_row, current_col, self.cell_size) + if (current_row, current_col) in target_list + else Cell(current_row, current_col, self.cell_size) + ) + # Update agent position + agent.row, agent.col = new_row, new_col + # Remove agents that have arrived + for agent in agents_to_remove: + print(f"Removing agent {agent} from ({agent.row}, {agent.col})") + self.agents.remove(agent) + # Restore target cell explicitly + if (agent.row, agent.col) in target_list: + self.grid[agent.row][agent.col] = TargetCell(agent.row, agent.col, self.cell_size) + else: + self.grid[agent.row][agent.col] = Cell(agent.row, agent.col, self.cell_size) + # Spawn new agents + for row, col in self.spawn_cells: + cell = self.grid[row][col] + if isinstance(cell, SpawnCell): + max_agents = 1 + cell.spawn_agents(self, max_agents) + self.log_grid_state(timestep) + + + + + def update_distance_maps(self): """ Compute and store both flood-fill and Dijkstra-based distance maps for all targets. @@ -291,11 +428,11 @@ def update_distance_maps(self): # Flood-fill distance map if self.movement_method == "floodfill": - self.flood_fill_distance_maps[(row, col)] = self.flood_fill(row, col, target_state=3) + self.flood_fill_distance_maps[(row, col)] = self.flood_fill_map(row, col, target_state=3) elif self.movement_method == "dijkstra": # Dijkstra distance map - self.dijkstra_distance_maps[(row, col)] = self.compute_distance_map((row, col)) + self.dijkstra_distance_maps[(row, col)] = self.dijkstra_map((row, col)) def plot_distance_map(self, distance_map, title="Distance Map"): """ @@ -305,23 +442,39 @@ def plot_distance_map(self, distance_map, title="Distance Map"): distance_map (List[List[float]]): The 2D distance map to plot. title (str): Title for the heatmap. """ + + # Convert distance map to a numpy array for easier handling distance_array = np.array(distance_map) + # Determine color scale limits for better visualization + finite_values = distance_array[np.isfinite(distance_array)] # Exclude inf values + vmin = finite_values.min() if len(finite_values) > 0 else 0 + vmax = finite_values.max() if len(finite_values) > 0 else 1 + # Create a heatmap using seaborn - plt.figure(figsize=(8, 6)) - sns.heatmap(distance_array, annot=False, cmap="YlGnBu", cbar=True, square=True, linewidths=0.5) + plt.figure(figsize=(10, 8)) + sns.heatmap( + distance_array, + annot=False, # No annotations inside the cells + cmap="coolwarm", # Color map for gradient + cbar=True, # Display color bar + square=True, # Ensure square cells + linewidths=0, # Remove lines between cells + vmin=vmin, # Set minimum value for color scale + vmax=vmax # Set maximum value for color scale + ) - # Add a title and labels + # Add title and labels plt.title(title) plt.xlabel("Columns") plt.ylabel("Rows") plt.show() - def plot_grid_state(grid, timestep): - #Plot Ausgabe für klarere Visualisierung, momentan noch über States für Farbwahl: Evtl besser mit cell.color? + def plot_grid_state(self, timestep): + # Plot Ausgabe für klarere Visualisierung, momentan noch über States für Farbwahl: Evtl besser mit cell.color? # Convert grid to a DataFrame for easy visualization - data = [[cell.state for cell in row] for row in grid.grid] + data = [[cell.state for cell in row] for row in self.grid] # Define a custom color map for the cell states custom_colors = { 0: 'white', # Empty cells @@ -354,6 +507,95 @@ def plot_grid_state(grid, timestep): plt.ylabel("Rows") plt.show() + + + + + # def plot_fundamental_diagram(self): + # """ + # Plot the fundamental diagram with density vs speed and flow. + # """ + # + # + # densities = self.density_data + # speeds = self.speed_data + # flows = self.flow_data + # + # fig, ax1 = plt.subplots() + # + # ax1.set_xlabel("Density (agents/unit area)") + # ax1.set_ylabel("Speed (units/time)", color="blue") + # ax1.plot(densities, speeds, label="Speed", color="blue") + # ax1.tick_params(axis="y", labelcolor="blue") + # + # ax2 = ax1.twinx() # instantiate a second y-axis that shares the same x-axis + # ax2.set_ylabel("Flow (agents/unit time)", color="red") + # ax2.plot(densities, flows, label="Flow", color="red") + # ax2.tick_params(axis="y", labelcolor="red") + # + # fig.tight_layout() # ensure everything fits without overlap + # plt.title("Fundamental Diagram") + # plt.show() + + def calculate_fundamental_diagram(self, boundary, timestep, agents_crossed, area): + """ + Calculate and plot the Fundamental Diagram. + + Parameters: + grid (Grid): The simulation grid. + boundary (list): A list of (row, col) tuples representing the boundary. + timestep (int): The current simulation timestep. + agents_crossed (dict): A dictionary tracking agents that crossed the boundary. + + + Returns: + dict: Contains density, flow, and average speed for the timestep. + """ + # Step 1: Calculate Flow (Agents crossing the boundary in this timestep) + current_crossed_agents = [] + for coordinates in boundary: + cell = self.grid[coordinates.row][coordinates.col] + if isinstance(cell, Agent) and cell.id not in agents_crossed: + current_crossed_agents.append(cell) + agents_crossed[cell.id] = timestep # Track the agent's crossing + + flow = len(current_crossed_agents) # Number of agents crossing the boundary + + # Step 2: Calculate Density (Agents per square meter in the Room or Hallway) + # region_in_front = self.select_area_by_coordinates(*region_coordinates) + agent_count = 0 + agent_count = sum( + 1 for cell in area if isinstance(cell, Agent) + ) + print(f"agent count{agent_count}") + area_squared = sum( + self.cell_size**2 for cell in area if(cell.cell_size>0) + ) + # print(area_squared) + # Area of region in m^2 + + density = agent_count / area_squared if area_squared > 0 else 0 + print(f"density:{density}") + # Step 3: Calculate Average Speed (of agents crossing the boundary) + avg_speed = ( + sum(agent.velocity for agent in current_crossed_agents) / len(current_crossed_agents) + if current_crossed_agents else 0 + ) + + # Step 4: Return and log the results + results = { + "density": density, + "flow": flow, + "avg_speed": avg_speed, + "timestep": timestep, + } + + print(f"Timestep {timestep}: Density={density:.2f}, Flow={flow:.2f}, AvgSpeed={avg_speed:.2f}") + return results + + + + def create_logfile(self): path = f"gridlog-{self.__hash__()}.txt" if(os.path.isfile(path)): @@ -369,33 +611,101 @@ def __init__(self, grid): self.fig, self.ax = plt.subplots() def plot_grid_state(self, timestep): - data = [[cell.state for cell in row] for row in self.grid.grid] + data = [[cell.state for cell in row] for row in self.grid.grid] #2D Liste mit jedem Zellenstatus custom_colors = { - 0: 'white', # Empty cells - 1: 'yellow', # Border cells - 2: 'green', # Spawn cells - 3: 'red', # Target cells - 4: 'gray', # Obstacles - 47: 'blue' # Agents + 0: 'white', + 1: 'yellow', + 2: 'green', + 3: 'red', + 4: 'gray' } - cmap = mcolors.ListedColormap([custom_colors[key] for key in sorted(custom_colors.keys())]) - bounds = list(sorted(custom_colors.keys())) + [max(custom_colors.keys()) + 1] + agent_color_map = {0: 'purple', 1: 'darkblue'} + + cmap = mcolors.ListedColormap([custom_colors[key] for key in sorted(custom_colors.keys())]) #erstelle eigne Colormap durch obige Farbwahl + bounds = list(sorted(custom_colors.keys())) + [max(custom_colors.keys()) + 1] norm = mcolors.BoundaryNorm(bounds, cmap.N) - self.ax.clear() - self.ax.imshow(data, cmap=cmap, norm=norm) - - + self.ax.cla() #Lösche den vorherigen Plotinhalt. + self.ax.imshow(data, cmap=cmap, norm=norm) #Plotte die Zellen mit eigner Colormap + + for agent in self.grid.agents: #Plotte jeden Agenten auf dem Grid. + if isinstance(self.grid.grid[agent.row][agent.col], Agent): + self.ax.add_patch(plt.Rectangle((agent.col-0.5, agent.row-0.5), 1, 1, edgecolor='black', facecolor=agent_color_map[agent.group])) #Mit add_patch wird der Agent platziert. Wir ziehen -0.5 ab um den Agenten in der Mitte der ZELLE zu setzen. (Position) + + # Gitterlinien hinzufügen + for i in range(len(data)): + self.ax.axhline(i+0.5, color='black') #Wir addieren 0.5 dazu um den Agenten in der Mitte der GITTERLINIEN zu setzen. (Visuell) + for j in range(len(data[0])): + self.ax.axvline(j+0.5, color='black') + + #Genauer definierter Bereich + self.ax.set_xlim(-1, len(data[0])) #Von Links nach Rechts + self.ax.set_ylim(-1, len(data)) + self.ax.set_title(f"Grid State at Timestep {timestep}") self.ax.set_xlabel("Columns") self.ax.set_ylabel("Rows") - def animate_grid_states(self, timesteps): + plt.pause(0.01) + + def animate_grid_states(self, timesteps): #Für jeden Zeitschritt, rufe die Funktion Update auf, welche die Zellen im Grid updated und dann plottet. def update(frame): - self.grid.update(target_list=self.grid.target_cells, timestep=frame) + self.grid.update(target_list=self.grid.target_cells, timestep=frame) self.plot_grid_state(frame) - ani = animation.FuncAnimation(self.fig, update, frames=timesteps, interval=10) + ani = animation.FuncAnimation(self.fig, update, frames=timesteps, interval=10) #FuncAnimation regelt die Synchronisation, in diesem Fall 10 Milisekunden plt.show() + + def plot_fundamental_diagram(self,data): + """Generate two separate plots from fundamental diagram data.""" + + # Extract data, remove entries where flow was 0 (no agents crossing yet) + timesteps = [entry["timestep"] for entry in data if entry["flow"]>0] + densities = [entry["density"] for entry in data if entry["flow"]>0] + avg_speeds = [entry["avg_speed"] for entry in data if entry["flow"]>0] + flows = [entry["flow"] for entry in data if entry["flow"]>0] + + # Plot 1: Density over time + plt.figure(figsize=(10, 6)) + plt.plot(timesteps, densities, marker="o", label="Density over time") + plt.title("Density Over Time") + plt.xlabel("Timestep") + plt.ylabel("Density (agents/m^2)") + plt.grid(True) + plt.legend() + plt.savefig("Density_over_time") + plt.show() + + # Plot 2: Flow over time + plt.figure(figsize=(10, 6)) + plt.plot(timesteps, flows, marker="o", label="Density over time") + plt.title("Flow Over Time") + plt.xlabel("Timestep") + plt.ylabel("Flow (agents/m/s)") + plt.grid(True) + plt.legend() + plt.savefig("Flow_over_time") + plt.show() + # Plot 3: Flow over time + plt.figure(figsize=(10, 6)) + plt.plot(densities, flows, marker="o", label="Density over time") + plt.title("Flows over Densities") + plt.xlabel("Densities") + plt.ylabel("Flow (agents/m/s)") + plt.grid(True) + plt.legend() + plt.savefig("Flow_over_densities") + plt.show() + + # Plot 2: Average speed relative to density + plt.figure(figsize=(10, 6)) + plt.plot(densities, avg_speeds, marker="s", label="Avg Speed vs. Density", color="red") + plt.title("Average Speed Relative to Density") + plt.xlabel("Density (agents/m^2)") + plt.ylabel("Average Speed (m/s)") + plt.grid(True) + plt.legend() + plt.savefig("Speed_relative_to_density") + plt.show() \ No newline at end of file diff --git a/Application/calculations.py b/Application/calculations.py deleted file mode 100644 index 8db66db..0000000 --- a/Application/calculations.py +++ /dev/null @@ -1,11 +0,0 @@ -import numpy as np - -def euclidian_distance(cellAgent, cellTarget): - x1, y1 = cellAgent - x2, y2 = cellTarget - sum = (y1-x1)^2+(y2-x2)^2 - if sum >= 0: - result = np.sqrt(sum) - return result - else: - return print("Error - negative value under square root") diff --git a/Application/packages.txt b/Application/packages.txt new file mode 100644 index 0000000..b584856 --- /dev/null +++ b/Application/packages.txt @@ -0,0 +1,110 @@ +# Name Version Build Channel +asttokens 2.4.1 pyhd8ed1ab_0 conda-forge +blas 1.0 mkl +bottleneck 1.4.2 py312h4b0e54e_0 +brotli 1.0.9 h2bbff1b_8 +brotli-bin 1.0.9 h2bbff1b_8 +bzip2 1.0.8 h2bbff1b_6 +ca-certificates 2024.9.24 haa95532_0 +colorama 0.4.6 pyhd8ed1ab_0 conda-forge +comm 0.2.2 pyhd8ed1ab_0 conda-forge +contourpy 1.2.0 py312h59b6b97_0 +cycler 0.11.0 pyhd3eb1b0_0 +debugpy 1.6.7 py312hd77b12b_0 +decorator 5.1.1 pyhd8ed1ab_0 conda-forge +exceptiongroup 1.2.2 pyhd8ed1ab_0 conda-forge +executing 2.1.0 pyhd8ed1ab_0 conda-forge +expat 2.6.3 h5da7b33_0 +fonttools 4.51.0 py312h2bbff1b_0 +freetype 2.12.1 ha860e81_0 +icc_rt 2022.1.0 h6049295_2 +icu 73.1 h6c2663c_0 +importlib-metadata 8.5.0 pyha770c72_0 conda-forge +intel-openmp 2023.1.0 h59b6b97_46320 +ipykernel 6.29.5 pyh4bbf305_0 conda-forge +ipython 8.29.0 pyh7428d3b_0 conda-forge +jedi 0.19.2 pyhff2d567_0 conda-forge +joblib 1.4.2 py312haa95532_0 +jpeg 9e h827c3e9_3 +jupyter_client 8.6.3 pyhd8ed1ab_0 conda-forge +jupyter_core 5.7.2 py312haa95532_0 +kiwisolver 1.4.4 py312hd77b12b_0 +krb5 1.20.1 h5b6d351_0 +lcms2 2.12 h83e58a3_0 +lerc 3.0 hd77b12b_0 +libbrotlicommon 1.0.9 h2bbff1b_8 +libbrotlidec 1.0.9 h2bbff1b_8 +libbrotlienc 1.0.9 h2bbff1b_8 +libclang 14.0.6 default_hb5a9fac_1 +libclang13 14.0.6 default_h8e68704_1 +libdeflate 1.17 h2bbff1b_1 +libffi 3.4.4 hd77b12b_1 +libpng 1.6.39 h8cc25b3_0 +libpq 12.20 h70ee33d_0 +libsodium 1.0.18 h8d14728_1 conda-forge +libtiff 4.5.1 hd77b12b_0 +libwebp-base 1.3.2 h3d04722_1 +lz4-c 1.9.4 h2bbff1b_1 +matplotlib 3.9.2 py312haa95532_0 +matplotlib-base 3.9.2 py312hbdc63d0_0 +matplotlib-inline 0.1.7 pyhd8ed1ab_0 conda-forge +mkl 2023.1.0 h6b88ed4_46358 +mkl-service 2.4.0 py312h2bbff1b_1 +mkl_fft 1.3.11 py312h827c3e9_0 +mkl_random 1.2.8 py312h0158946_0 +nest-asyncio 1.6.0 pyhd8ed1ab_0 conda-forge +numexpr 2.10.1 py312h4cd664f_0 +numpy 1.26.4 py312hfd52020_0 +numpy-base 1.26.4 py312h4dde369_0 +openjpeg 2.5.2 hae555c5_0 +openssl 3.0.15 h827c3e9_0 +packaging 24.1 py312haa95532_0 +pandas 2.2.2 py312h0158946_0 +parso 0.8.4 pyhd8ed1ab_0 conda-forge +patsy 0.5.6 py312haa95532_0 +pickleshare 0.7.5 py_1003 conda-forge +pillow 10.4.0 py312h827c3e9_0 +pip 24.2 py312haa95532_0 +platformdirs 4.3.6 pyhd8ed1ab_0 conda-forge +ply 3.11 py312haa95532_1 +prompt-toolkit 3.0.48 pyha770c72_0 conda-forge +psutil 5.9.0 py312h2bbff1b_0 +pure_eval 0.2.3 pyhd8ed1ab_0 conda-forge +pybind11-abi 5 hd3eb1b0_0 +pygments 2.18.0 pyhd8ed1ab_0 conda-forge +pyparsing 3.2.0 py312haa95532_0 +pyqt 5.15.10 py312hd77b12b_0 +pyqt5-sip 12.13.0 py312h2bbff1b_0 +python 3.12.7 h14ffc60_0 +python-dateutil 2.9.0post0 py312haa95532_2 +python-tzdata 2023.3 pyhd3eb1b0_0 +pytz 2024.1 py312haa95532_0 +pywin32 305 py312h2bbff1b_0 +pyzmq 25.1.2 py312hd77b12b_0 +qt-main 5.15.2 h19c9488_10 +scikit-learn 1.5.1 py312h0158946_0 +scipy 1.13.1 py312hbb039d4_0 +seaborn 0.13.2 py312haa95532_0 +setuptools 75.1.0 py312haa95532_0 +sip 6.7.12 py312hd77b12b_0 +six 1.16.0 pyhd3eb1b0_1 +sqlite 3.45.3 h2bbff1b_0 +stack_data 0.6.2 pyhd8ed1ab_0 conda-forge +statsmodels 0.14.2 py312h4b0e54e_0 anaconda +tbb 2021.8.0 h59b6b97_0 +threadpoolctl 3.5.0 py312hfc267ef_0 +tk 8.6.14 h0416ee5_0 +tornado 6.4.1 py312h827c3e9_0 +traitlets 5.14.3 pyhd8ed1ab_0 conda-forge +typing_extensions 4.12.2 pyha770c72_0 conda-forge +tzdata 2024b h04d1e81_0 +unicodedata2 15.1.0 py312h2bbff1b_0 +vc 14.40 h2eaa2aa_1 +vs2015_runtime 14.40.33807 h98bb1dd_1 +wcwidth 0.2.13 pyhd8ed1ab_0 conda-forge +wheel 0.44.0 py312haa95532_0 +xz 5.4.6 h8cc25b3_1 +zeromq 4.3.5 hd77b12b_0 +zipp 3.21.0 pyhd8ed1ab_0 conda-forge +zlib 1.2.13 h8cc25b3_1 +zstd 1.5.6 h8880b57_0 \ No newline at end of file diff --git a/Application/run.py b/Application/run.py index f704c0e..c98e57e 100644 --- a/Application/run.py +++ b/Application/run.py @@ -1,58 +1,69 @@ from Grid import Grid -from Cell import Cell, SpawnCell, BorderCell, ObstacleCell, Agent, TargetCell +from Cell import Cell, SpawnCell, ObstacleCell, Agent, TargetCell import matplotlib.pyplot as plt import numpy as np from Grid import Grid, Visualization -from tests import room_square, ChickenTest, RiMEA9, RiMEA4 -import tests -grid = RiMEA9(1, "floodfill") +from tests import room_square, ChickenTest, RiMEA9, RiMEA4, Experiment +import matplotlib +#This line is needed for the plots to render in Pychamr Sciplot-View +matplotlib.use("Qt5Agg") +#Abschnitt in dem Die simulations Parameter angegeben werden. +time_input = input("Bitte geben sie die Anzahl Zeitschritte an (ein Zeitschritt ist 1 Sekunde)") +timesteps = int(time_input) +user_input = input("Bitte geben sie an welchen Test (RiMEA4, RiMEA9 oder Experiment) sie durchführen wollen") +user_input = user_input.lower() +if user_input == "rimea9": + door_input = input("Bitte Anzahl (1-4) Türen angeben") + pathfinding = input("Bitte Algorithmus wählen (dijkstra, floodfill)") + if pathfinding == "floodfill": + algo = "floodfill" + elif pathfinding == "dijkstra": + algo = "dijkstra" + else: + algo = "dijkstra" #Standardwert ist dijkstra + doors = int(door_input) + grid, door_cells, roi = RiMEA9(movement_method=algo, Doors=doors) +elif user_input == "rimea4": + pathfinding = input("Bitte Algorithmus wählen (dijkstra, floodfill)") + if pathfinding == "floodfill": + algo = "floodfill" + elif pathfinding == "dijkstra": + algo = "dijkstra" + else: + algo = "dijkstra" #Standardwert ist dijkstra + spawn_input = input("Bitte spawnrate zwischen 0 und 1 eingeben um Personendichte zu variieren (Wird mit einer Zufallsvariable zwischen 0 und 1 verglichen um spawn zu bestimmen)") + grid, door_cells, roi = RiMEA4(movement_method=algo,spawn_rate=float(spawn_input)) +elif user_input == "experiment": + pathfinding = input("Bitte Algorithmus wählen (dijkstra, floodfill)") + if pathfinding == "floodfill": + algo = "floodfill" + elif pathfinding == "dijkstra": + algo = "dijkstra" + else: + algo = "dijkstra" # Standardwert ist dijkstra + spawn_input = input("Bitte spawnrate zwischen 0 und 1 eingeben um Personendichte zu variieren (Wird mit einer Zufallsvariable zwischen 0 und 1 verglichen um spawn zu bestimmen)") + grid, door_cells, roi = Experiment(movement_method=algo, spawn_rate=float(spawn_input)) +#################################################### +#Visualisierung initialisieren und Variablen für Resultate anlegen visualization = Visualization(grid) - agent_count_list = [] -average_distance = [] -density_list = [] -timesteps = 10 +fundamental_data = [] -for i in range(timesteps): - grid.update(target_list=grid.target_cells, timestep=i) - #visualization.plot_grid_state(i) - grid.plot_grid_state(i) - plt.pause(0.2) - - agent_count = len(grid.agents) - agent_count_list.append(agent_count) - - # mittlere distanz von Agenten zu Ziel - total_distance_to_target = 0 - if len(grid.agents) > 0: - total_istance_to_target = 0 - for agent in grid.agents: - target = agent.find_target(grid.target_cells) - total_distance_to_target += agent.euclidean_distance_to(grid.grid[target[0]][target[1]]) - - average_distance.append(total_distance_to_target / len(grid.agents)) - else: - average_distance.append(np.nan) +agents_crossed = {} # Dictionary to track agents crossing the boundary +for i in range(timesteps): + grid.update(target_list=grid.target_cells, timestep=i) + #Print statement trent einzelne Zeitschritte von einander (für einfacheres debugging) + print("--------------------------------------------------------------------------------------------------") + #Visualisierung aktueller grid-state + visualization.plot_grid_state(i) + #Bereich für Density berechnung von aktuelem Grid state wählen + area = grid.select_area_by_coordinates(roi[0], roi[1], roi[2], roi[3]) + #grid.plot_grid_state(i) -plt.figure(figsize=(10,5)) - -plt.subplot(1, 3, 1) -plt.plot(agent_count_list) -plt.title('Anzahl Agenten über Zeit') -plt.xlabel('Zeitschritt') -plt.ylabel('Anzahl Agenten') - -plt.subplot(1, 3, 2) -plt.plot(average_distance) -plt.title('Mittlere Distanz Agenten zum Ziel') -plt.xlabel('Zeitschritt') -plt.ylabel('Distanz') - - - -plt.tight_layout() -plt.show() - -visualization.animate_grid_states(timesteps) \ No newline at end of file + #Berechnung fürs Fundamentaldiagram (Aktuelle Agenten dichte, Fluss und durchschnittsgeschwindigkeit) + fd_results = grid.calculate_fundamental_diagram(door_cells, i, agents_crossed, area) + fundamental_data.append(fd_results) +#Nachdem die Simulation durchgeführt wurde plotten wir noch die Ergebnisse der Fundamentaldiagram Berechnungen +visualization.plot_fundamental_diagram(fundamental_data) \ No newline at end of file diff --git a/Application/test_components.py b/Application/test_components.py index 0d2a013..5e54469 100644 --- a/Application/test_components.py +++ b/Application/test_components.py @@ -1,7 +1,8 @@ import unittest, math from Grid import Grid from Cell import Cell, SpawnCell, TargetCell, Agent, ObstacleCell - +from concurrent.futures import ThreadPoolExecutor +import numpy as np #Werden wir später noch in einen dedizierten Testordner verschieben class TestAgentBehavior(unittest.TestCase): @@ -14,9 +15,10 @@ def setUp(self): cell_size=1, spawn_cells=[(1, 1)], target_cells=[(3, 3)], + movement_method="dijkstra", obstacle_cells=[]#Momentan noch keine Kolisionsvermeidung ) - + self.grid.update_distance_maps() def test_spawn_agents_restrict_to_neighbors(self): spawn_cell = self.grid.grid[1][1] # Define a spawn cell self.assertIsInstance(spawn_cell, SpawnCell, "Expected a SpawnCell at (1, 1)") @@ -34,7 +36,7 @@ def test_spawn_agents(self): self.assertEqual(len(self.grid.agents), 0) # Update erzeugt neue Agenten - self.grid.update(self.grid.target_cells, timestep=1) + #self.grid.update(self.grid.target_cells, timestep=1) # Verify all agents are in valid spawn neighbor cells @@ -57,6 +59,8 @@ def test_agent_removal_on_arrival(self): # Update bewirkt das er sich Ziel Nähert for timestep in range(10): self.grid.update(self.grid.target_cells, timestep=timestep) + #self.grid.plot_distance_map(self.grid.dijkstra_distance_maps([(3,3)])) + self.grid.plot_grid_state(timestep) # Verifiziere Löschung self.assertNotIn(agent, self.grid.agents, "Agent was not removed after arrival.") @@ -227,7 +231,7 @@ def test_flood_fill_distance_map(self): # Compute the distance map target_row, target_col = 2, 2 - distance_map = grid.flood_fill(target_row, target_col, target_state=3) + distance_map = grid.flood_fill_map(target_row, target_col, target_state=3) # Expected distance map expected_map = [ @@ -251,7 +255,176 @@ def test_flood_fill_distance_map(self): msg=f"Mismatch in Flood Fill distance map for target ({target_row}, {target_col}) at ({row}, {col})" ) grid.plot_distance_map(distance_map) +class TestPenalties(unittest.TestCase): + + def setUp(self): + """Set up a grid for testing.""" + # Create a 5x5 grid with cell size 1.0 + self.grid = Grid( + length=5.0, height=5.0, + spawn_cells=[], + target_cells=[(4, 4)], + obstacle_cells=[], + cell_size=1.0 + ) + # Place a target at (4, 4) + self.grid.place_target(4, 4) + # Place an agent at (0, 0) + self.grid.place_agent(0, 0) + self.agent = self.grid.agents[0] + + def test_precomputed_penalties(self): + """Test that precomputed social penalties are calculated correctly.""" + # Place multiple agents in the grid + self.grid.place_agent(1, 1) + self.grid.place_agent(2, 2) + + # Compute social penalties using the precomputed method + penalties = self.grid.compute_social_penalties() + + # Ensure penalties are calculated for each agent + self.assertEqual(len(penalties), len(self.grid.agents)) + + # Verify that penalties are non-zero and consistent with agent positions + positions = np.array([[agent.row, agent.col] for agent in self.grid.agents]) + for i, penalty in enumerate(penalties): + self.assertGreater(penalty, 0) + + # Check penalty consistency with distances + deltas = positions - positions[i] + distances = np.linalg.norm(deltas, axis=1) + within_cutoff = distances <= 2.0 + + # Penalty should be influenced by nearby agents within cutoff distance + expected_penalty = sum( + np.exp(-(distances[j] ** 2) / (2 * 0.5 ** 2)) + for j in range(len(distances)) + if within_cutoff[j] and i != j + ) + + self.assertAlmostEqual(penalty, expected_penalty, places=2) + + def test_update_with_precomputed_penalties(self): + """Test that the update method uses precomputed penalties correctly.""" + # Place a second agent near the first agent + self.grid.place_agent(0, 1) + self.grid.update_distance_maps() + # Run the update method + self.grid.update(self.grid.target_cells, timestep=1) + + # Verify penalties were used in movement decisions + precomputed_penalties = self.grid.compute_social_penalties() + for i, agent in enumerate(self.grid.agents): + penalty = precomputed_penalties[i] + self.assertGreater(penalty, 0) + +class TestGridPenalties(unittest.TestCase): + + def setUp(self): + # Create a Grid instance + self.cell_size = 1.0 + self.length = 5 # 5 cells wide + self.height = 5 # 5 cells tall + + # Define the initial setup for the grid + spawn_cells = [] # No spawn cells for this test + target_cells = [(4, 4)] # Target at the bottom-right corner + obstacle_cells = [] # No obstacles for simplicity + + # Create the grid + self.grid = Grid(self.length, self.height, spawn_cells, target_cells, obstacle_cells, cell_size=self.cell_size) + + # Place agents in the grid + self.grid.place_agent(1, 1) + self.grid.place_agent(1, 2) + self.grid.place_agent(2, 1) + self.grid.place_agent(2, 2) + self.agents = self.grid.agents + + def test_social_penalties_computation(self): + # Compute social penalties for all agents + cutoff_distance = 2.0 + penalty_decay_factor = 0.5 + precomputed_penalties = self.grid.compute_social_penalties(cutoff_distance, penalty_decay_factor) + + # Print computed penalties for debugging + print(f"Computed Penalties: {precomputed_penalties}") + + # Validate penalties are computed correctly + self.assertEqual(len(precomputed_penalties), len(self.agents)) + + + expected_penalties = [ + 0.28898620536195957, + 0.28898620536195957, + 0.28898620536195957, + 0.28898620536195957 + ] + + # Check penalties against expected values + for computed, expected in zip(precomputed_penalties, expected_penalties): + self.assertAlmostEqual(computed, expected, delta=0.2) + +class TestAgentMovementRange(unittest.TestCase): -if __name__ == '__main__': + def setUp(self): + # Create a Grid instance + self.cell_size = 1 + self.length = 5 # 5 cells wide + self.height = 5 # 5 cells tall + + # Define the initial setup for the grid + spawn_cells = [] # No spawn cells for this test + target_cells = [(4, 4)] # Target at the bottom-right corner + obstacle_cells = [] # No obstacles for simplicity + + # Create the grid + self.grid = Grid(self.length, self.height, spawn_cells, target_cells, obstacle_cells, cell_size=self.cell_size) + + # Add a target at (4, 4) + self.grid.place_target(4, 4) + + # Place an agent at (0, 0) + self.grid.place_agent(0, 0) + self.agent = self.grid.grid[0][0] + + # Precompute distance maps for the grid + self.grid.update_distance_maps() + + def test_movement_range_increases(self): + # Set initial conditions + + original_range = self.agent._original_movement_range + original_velocity = self.agent._original_velocity + # Debug initial values + print(f"Initial Movement Range: {original_range}") + print(f"Original Velocity: {original_velocity}") + precomputed_penalties = self.grid.compute_social_penalties() + + # Simulate insufficient movement range + self.agent.movement_towards_target(self.grid, precomputed_penalties, 0, 0) + + # Assert no movement happened due to insufficient range + self.assertEqual(self.agent.row, 0) + self.assertEqual(self.agent.col, 0) + + # Verify movement range increased + + expected_range = self.agent._original_movement_range+self.agent.velocity + print(f"Current Velocity afet movement update{self.agent.velocity} range {self.agent.movement_range}")# Initial range + velocity + print(f"New Movement Range: {self.agent.movement_range} (Expected: {expected_range})") + self.assertAlmostEqual(self.agent.movement_range, expected_range, delta=0.1) + + self.agent.movement_towards_target(self.grid, precomputed_penalties, 0, 0) + + + + + + +if __name__ == "__main__": unittest.main() + + + diff --git a/Application/tests.py b/Application/tests.py index a848dab..46142dc 100644 --- a/Application/tests.py +++ b/Application/tests.py @@ -5,16 +5,11 @@ def room_square(): height = 50 length = 50 - spawn_cells = [(0, 1), (0, 2), (0, 3), (0, 4)] obstacle_cells = [(5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (5, 10), (5, 11), (5, 12), (5, 13), (5, 14), (5, 15), (5, 16), (5, 17), (5, 18), (5, 19), (5, 20), (5, 21), (5, 22), (5, 23), (5, 24), (5, 25), (5, 26), (5, 27), (5, 28), (5, 29), (5, 30), (5, 31), (5, 32), (5, 33), (5, 34), (5, 35), (5, 36), (5, 37), (5, 38), (5, 39), (5, 40), (5, 41), (5, 42), (5, 43), (5, 44), (5, 45), (5, 46), (5, 47), (5, 48), (5, 49), (2, 4), (3, 25)] - target_cells = [(0, 49), (1, 49), (2, 49), (3, 49), (4, 49)] - + target_cells = [(0, 49), (1, 49), (2, 49), (3, 49), (4, 49)] grid = Grid(height=height, length=length, spawn_cells=spawn_cells, obstacle_cells=obstacle_cells, target_cells=target_cells, cell_size=0.5) - return grid - - def ChickenTest(movement_method): height = 20 length = 20 @@ -43,88 +38,116 @@ def ChickenTest(movement_method): return grid - - def RiMEA9(Doors, movement_method): room_height = 20 room_length = 30 frameSize = 5 - height = room_height+2*frameSize - length = room_length+2*frameSize + height = room_height + 2 * frameSize + length = room_length + 2 * frameSize grid = Grid(height=height, length=length, spawn_cells=[], obstacle_cells=[], target_cells=[], cell_size=0.5, movement_method=movement_method) - # place obstacles and targets - vertical_placement = grid.select_area_by_coordinates(frameSize, frameSize, frameSize, height-frameSize) # Obstacle-Zellen links - vertical_placement += grid.select_area_by_coordinates(length-frameSize, frameSize, length-frameSize, height-frameSize) # Obstacle-Zellen rechts - horizontal_placement = grid.select_area_by_coordinates(frameSize, frameSize, length-frameSize, frameSize) # Obstacle-Zellen oben - horizontal_placement += grid.select_area_by_coordinates(frameSize, height-frameSize, length-frameSize, height-frameSize) # Obstacle-Zellen unten - - obstacles = vertical_placement+horizontal_placement + # Place obstacles and targets + vertical_placement = grid.select_area_by_coordinates(frameSize, frameSize, frameSize, height - frameSize) + vertical_placement += grid.select_area_by_coordinates(length - frameSize, frameSize, length - frameSize, height - frameSize) + horizontal_placement = grid.select_area_by_coordinates(frameSize, frameSize, length - frameSize, frameSize) + horizontal_placement += grid.select_area_by_coordinates(frameSize, height - frameSize, length - frameSize, height - frameSize) + obstacles = vertical_placement + horizontal_placement for cell in obstacles: grid.place_obstacle(cell.row, cell.col) if Doors == 1: - door_cells = grid.select_area_by_coordinates(frameSize+4, frameSize, frameSize+5, frameSize) - print(len(door_cells)) - target_placement = grid.select_area_by_coordinates(frameSize+4, 0, frameSize+5, 0) + door_cells = grid.select_area_by_coordinates(frameSize + 4, frameSize, frameSize + 5, frameSize) + target_placement = grid.select_area_by_coordinates(frameSize + 4, 0, frameSize + 5, 0) elif Doors == 2: - door_cells = grid.select_area_by_coordinates(frameSize+4, frameSize, frameSize+5, frameSize) - door_cells += grid.select_area_by_coordinates(length-frameSize-5, height-frameSize, length-frameSize-4, height-frameSize) - target_placement = grid.select_area_by_coordinates(frameSize+4, 0, frameSize+5, 0) - target_placement += grid.select_area_by_coordinates(length-frameSize-5, height, length-frameSize-4, height) + door_cells = grid.select_area_by_coordinates(frameSize + 4, frameSize, frameSize + 5, frameSize) + door_cells += grid.select_area_by_coordinates(length - frameSize - 5, height - frameSize, length - frameSize - 4, height - frameSize) + target_placement = grid.select_area_by_coordinates(frameSize + 4, 0, frameSize + 5, 0) + target_placement += grid.select_area_by_coordinates(length - frameSize - 5, height, length - frameSize - 4, height) elif Doors == 3: - door_cells = grid.select_area_by_coordinates(frameSize+4, frameSize, frameSize+5, frameSize) - door_cells += grid.select_area_by_coordinates(length-frameSize-5, height-frameSize, length-frameSize-4, height-frameSize) - door_cells += grid.select_area_by_coordinates(length-frameSize-5, frameSize, length-frameSize-4, frameSize) - target_placement = grid.select_area_by_coordinates(frameSize+4, 0, frameSize+5, 0) - target_placement += grid.select_area_by_coordinates(length-frameSize-5, height, length-frameSize-4, height) - target_placement += grid.select_area_by_coordinates(length-frameSize-5, 0, length-frameSize-4, 0) + door_cells = grid.select_area_by_coordinates(frameSize + 4, frameSize, frameSize + 5, frameSize) + door_cells += grid.select_area_by_coordinates(length - frameSize - 5, height - frameSize, length - frameSize - 4, height - frameSize) + door_cells += grid.select_area_by_coordinates(length - frameSize - 5, frameSize, length - frameSize - 4, frameSize) + target_placement = grid.select_area_by_coordinates(frameSize + 4, 0, frameSize + 5, 0) + target_placement += grid.select_area_by_coordinates(length - frameSize - 5, height, length - frameSize - 4, height) + target_placement += grid.select_area_by_coordinates(length - frameSize - 5, 0, length - frameSize - 4, 0) else: - # hiermit wird abgefangen, falls Zahlen ausserhalb der Menge 1-3 eingegeben werden - dann default 4 - door_cells = grid.select_area_by_coordinates(frameSize+4, frameSize, frameSize+5, frameSize) - door_cells += grid.select_area_by_coordinates(length-frameSize-5, height-frameSize, length-frameSize-4, height-frameSize) - door_cells += grid.select_area_by_coordinates(length-frameSize-5, frameSize, length-frameSize-4, frameSize) - door_cells += grid.select_area_by_coordinates(frameSize+4, height-frameSize, frameSize+5, height-frameSize) - target_placement = grid.select_area_by_coordinates(frameSize+4, 0, frameSize+5, 0) - target_placement += grid.select_area_by_coordinates(length-frameSize-5, height, length-frameSize-4, height) - target_placement += grid.select_area_by_coordinates(length-frameSize-5, 0, length-frameSize-4, 0) - target_placement += grid.select_area_by_coordinates(frameSize+4, height, frameSize+5, height) + door_cells = grid.select_area_by_coordinates(frameSize + 4, frameSize, frameSize + 5, frameSize) + door_cells += grid.select_area_by_coordinates(length - frameSize - 5, height - frameSize, length - frameSize - 4, height - frameSize) + door_cells += grid.select_area_by_coordinates(length - frameSize - 5, frameSize, length - frameSize - 4, frameSize) + door_cells += grid.select_area_by_coordinates(frameSize + 4, height - frameSize, frameSize + 5, height - frameSize) + target_placement = grid.select_area_by_coordinates(frameSize + 4, 0, frameSize + 5, 0) + target_placement += grid.select_area_by_coordinates(length - frameSize - 5, height, length - frameSize - 4, height) + target_placement += grid.select_area_by_coordinates(length - frameSize - 5, 0, length - frameSize - 4, 0) + target_placement += grid.select_area_by_coordinates(frameSize + 4, height, frameSize + 5, height) for cell in target_placement: grid.place_target(cell.row, cell.col) - # place doors for cell in door_cells: grid.place_empty_cell(cell.row, cell.col) - # place agents - potential_agent_cells = grid.select_area_by_coordinates(frameSize+2, frameSize+2, length-frameSize-2, height-frameSize-2) - + potential_agent_cells = grid.select_area_by_coordinates(frameSize + 2, frameSize + 2, length - frameSize - 2, height - frameSize - 2) + roi = (frameSize, frameSize, length - frameSize, height - frameSize) for cell in potential_agent_cells: chance = random.randint(0, 10) - if chance <= 5 and len(grid.agents)<=1000: + if chance <= 5 and len(grid.agents) <= 1000: grid.place_agent(cell.row, cell.col) + return grid, door_cells, roi - return grid - - -# Anpassungen aus den anderen Maps müssen für RiMEA4 noch übernommen werden -def RiMEA4(): +def RiMEA4(movement_method, spawn_rate=1): height = 10 - length = 150 - - obstacle_cells = [(4, 24), (5, 24), (4, 25), (5, 25), (4, 74), (5, 74), (6, 74), (7, 74), (4, 75), (5, 75), (6, 75), (7, 75)] + length = 100 + warm_up = 25 + corridor_length = warm_up + length + grid = Grid(height=height, length=corridor_length, spawn_cells=[], obstacle_cells=[], target_cells=[], cell_size=0.5, movement_method=movement_method, spawn_rate=spawn_rate) - grid = Grid(height=height, length=length, spawn_cells=[], obstacle_cells=[], target_cells=[], cell_size=0.5) + # Door_cells sind in diesem Fall eine Zelle vor Ende des Ganges + door_cells = grid.select_area_by_coordinates(corridor_length-1, 0, corridor_length-1, height) + obstacle_placement = grid.select_area_by_coordinates(24, 4, 25, 5) + obstacle_placement += grid.select_area_by_coordinates(74, 2, 75, 5) for i in range(0, grid.rows-1): grid.place_spawn_cell(i, 0) grid.place_target(i, grid.cols-1) - for j in range(0, len(obstacle_cells)-1): - grid.place_obstacle(obstacle_cells[j][0], obstacle_cells[j][1]) + for cell in obstacle_placement: + grid.place_obstacle(cell.row, cell.col) - return grid \ No newline at end of file + for cell in door_cells: + grid.place_empty_cell(cell.row, cell.col) + roi = (0,0,corridor_length,height) + return grid, door_cells, roi +def Experiment(movement_method, spawn_rate): + length = 8 + height = 16 + grid = Grid(height=height, length=length, spawn_cells=[], obstacle_cells=[], target_cells=[], cell_size=0.5, spawn_rate=spawn_rate) + target_cells = grid.select_area_by_coordinates(3.5, 0, 4.5,1) + #Tische beim Einweiser + obstacle_cells = grid.select_area_by_coordinates(0, 16, 3, 16) + obstacle_cells += grid.select_area_by_coordinates(length-3, 16, length, 16) + #Säulen rechts und links + obstacle_cells += grid.select_area_by_coordinates(0, height/2, 0, height/2) + obstacle_cells += grid.select_area_by_coordinates(length, height/2, length, height/2) + spawn_cells = grid.select_area_by_coordinates(3.5, height,4.5 , height) + + agent_pos_1 = grid.meter_to_rowcol(3.5, 8) + agent_pos_2 = grid.meter_to_rowcol(4.5, 8) + door_cells = grid.select_area_by_coordinates(1, 8, 3, 8) + door_cells += grid.select_area_by_coordinates(5, 8, length-1, 8) + grid.place_obstacle(agent_pos_1[0], agent_pos_1[1]) + grid.place_obstacle(agent_pos_2[0], agent_pos_2[1]) + roi = (0,0,length,height) + for target in target_cells: + grid.place_target(target.row, target.col) + for cell in obstacle_cells: + grid.place_obstacle(cell.row, cell.col) + for cell in spawn_cells: + grid.place_spawn_cell(cell.row, cell.col) + for agent in grid.agents: + #Set velocity to 0 so these Agents dont move + agent._original_velocity = 0 + agent.original_velocity = 0 + return grid, door_cells, roi \ No newline at end of file diff --git a/README.md b/README.md index a4c7e4e..e853b17 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,118 @@ Um die Tests zu überprüfen, ins Verzeichnis Application wechseln und folgenden ` python -m unittest -v test_components` # Rimea Test -Um den Rimea Test durchzuführen muss die Datei run.py gestartet werden. Der Algorithmus (Floodfill, Dijkstra) kann in der run.py movement_method angepasst werden. -Visualisierung (in der Timestep loop ausgeklammert) kann auch angepasst werden. \ No newline at end of file +Um den Rimea Test durchzuführen muss die Datei run.py gestartet werden. Die Parameter für die Simulation werden per Konsole übergeben und danach wird automatisch die Simulation gestartet. + +# Packages +Da wir bei der Visualisierung einige Fehler wegen fehlender Packages erhalten haben, sind hier noch die Packages aufgelistet. +| Name | Version | Build |Channel | +| ----------------------|--------------------------|---------|------------| +asttokens | 2.4.1 | pyhd8ed1ab_0 |conda-forge | +blas | 1.0 | mkl | | +bottleneck | 1.4.2 |py312h4b0e54e_0 | | +brotli | 1.0.9 | h2bbff1b_8 | | +brotli-bin | 1.0.9 | h2bbff1b_8 | | +bzip2 | 1.0.8 | h2bbff1b_6 | | +ca-certificates | 2024.9.24 | haa95532_0 | | +colorama | 0.4.6 | pyhd8ed1ab_0 |conda-forge | +comm | 0.2.2 | pyhd8ed1ab_0 |conda-forge | +contourpy | 1.2.0 |py312h59b6b97_0 | | +cycler | 0.11.0 | pyhd3eb1b0_0 | | +debugpy | 1.6.7 |py312hd77b12b_0 | | +decorator | 5.1.1 | pyhd8ed1ab_0 |conda-forge | +exceptiongroup | 1.2.2 | pyhd8ed1ab_0 |conda-forge | +executing | 2.1.0 | pyhd8ed1ab_0 |conda-forge | +expat | 2.6.3 | h5da7b33_0 | | +fonttools | 4.51.0 |py312h2bbff1b_0 | | +freetype | 2.12.1 | ha860e81_0 | | +icc_rt | 2022.1.0 | h6049295_2 | | +icu | 73.1 | h6c2663c_0 | | +importlib-metadata | 8.5.0 | pyha770c72_0 |conda-forge | +intel-openmp | 2023.1.0 | h59b6b97_46320 | | +ipykernel | 6.29.5 | pyh4bbf305_0 |conda-forge | +ipython | 8.29.0 | pyh7428d3b_0 |conda-forge | +jedi | 0.19.2 | pyhff2d567_0 |conda-forge | +joblib | 1.4.2 |py312haa95532_0 | | +jpeg | 9e | h827c3e9_3 | | +jupyter_client | 8.6.3 | pyhd8ed1ab_0 |conda-forge | +jupyter_core | 5.7.2 |py312haa95532_0 | | +kiwisolver | 1.4.4 |py312hd77b12b_0 | | +krb5 | 1.20.1 | h5b6d351_0 | | +lcms2 | 2.12 | h83e58a3_0 | | +lerc | 3.0 | hd77b12b_0 | | +libbrotlicommon | 1.0.9 | h2bbff1b_8 | | +libbrotlidec | 1.0.9 | h2bbff1b_8 | | +libbrotlienc | 1.0.9 | h2bbff1b_8 | | +libclang | 14.0.6 |default_hb5a9fac_1 | | +libclang13 | 14.0.6 |default_h8e68704_1 | | +libdeflate | 1.17 | h2bbff1b_1 | | +libffi | 3.4.4 | hd77b12b_1 | | +libpng | 1.6.39 | h8cc25b3_0 | | +libpq | 12.20 | h70ee33d_0 | | +libsodium | 1.0.18 | h8d14728_1 |conda-forge | +libtiff | 4.5.1 | hd77b12b_0 | | +libwebp-base | 1.3.2 | h3d04722_1 | | +lz4-c | 1.9.4 | h2bbff1b_1 | | +matplotlib | 3.9.2 |py312haa95532_0 | | +matplotlib-base | 3.9.2 |py312hbdc63d0_0 | | +matplotlib-inline | 0.1.7 | pyhd8ed1ab_0 |conda-forge | +mkl | 2023.1.0 | h6b88ed4_46358 | | +mkl-service | 2.4.0 |py312h2bbff1b_1 | | +mkl_fft | 1.3.11 |py312h827c3e9_0 | | +mkl_random | 1.2.8 |py312h0158946_0 | | +nest-asyncio | 1.6.0 | pyhd8ed1ab_0 |conda-forge | +numexpr | 2.10.1 |py312h4cd664f_0 | | +numpy | 1.26.4 |py312hfd52020_0 | | +numpy-base | 1.26.4 |py312h4dde369_0 | | +openjpeg | 2.5.2 | hae555c5_0 | | +openssl | 3.0.15 | h827c3e9_0 | | +packaging | 24.1 |py312haa95532_0 | | +pandas | 2.2.2 |py312h0158946_0 | | +parso | 0.8.4 | pyhd8ed1ab_0 |conda-forge | +patsy | 0.5.6 |py312haa95532_0 | | +pickleshare | 0.7.5 | py_1003 |conda-forge | +pillow | 10.4.0 |py312h827c3e9_0 | | +pip | 24.2 |py312haa95532_0 | | +platformdirs | 4.3.6 | pyhd8ed1ab_0 |conda-forge | +ply | 3.11 |py312haa95532_1 | | +prompt-toolkit | 3.0.48 | pyha770c72_0 |conda-forge | +psutil | 5.9.0 |py312h2bbff1b_0 | | +pure_eval | 0.2.3 | pyhd8ed1ab_0 |conda-forge | +pybind11-abi | 5 | hd3eb1b0_0 | | +pygments | 2.18.0 | pyhd8ed1ab_0 |conda-forge | +pyparsing | 3.2.0 |py312haa95532_0 | | +pyqt | 5.15.10 |py312hd77b12b_0 | | +pyqt5-sip | 12.13.0 |py312h2bbff1b_0 | | +python | 3.12.7 | h14ffc60_0 | | +python-dateutil | 2.9.0post0 |py312haa95532_2 | | +python-tzdata | 2023.3 | pyhd3eb1b0_0 | | +pytz | 2024.1 |py312haa95532_0 | | +pywin32 | 305 |py312h2bbff1b_0 | | +pyzmq | 25.1.2 |py312hd77b12b_0 | | +qt-main | 5.15.2 | h19c9488_10 | | +scikit-learn | 1.5.1 |py312h0158946_0 | | +scipy | 1.13.1 |py312hbb039d4_0 | | +seaborn | 0.13.2 |py312haa95532_0 | | +setuptools | 75.1.0 |py312haa95532_0 | | +sip | 6.7.12 |py312hd77b12b_0 | | +six | 1.16.0 | pyhd3eb1b0_1 | | +sqlite | 3.45.3 | h2bbff1b_0 | | +stack_data | 0.6.2 | pyhd8ed1ab_0 |conda-forge | +statsmodels | 0.14.2 |py312h4b0e54e_0 |anaconda | +tbb | 2021.8.0 | h59b6b97_0 | | +threadpoolctl | 3.5.0 |py312hfc267ef_0 | | +tk | 8.6.14 | h0416ee5_0 | | +tornado | 6.4.1 |py312h827c3e9_0 | | +traitlets | 5.14.3 | pyhd8ed1ab_0 |conda-forge | +typing_extensions | 4.12.2 | pyha770c72_0 |conda-forge | +tzdata | 2024b | h04d1e81_0 | | +unicodedata2 | 15.1.0 |py312h2bbff1b_0 | | +vc | 14.40 | h2eaa2aa_1 | | +vs2015_runtime | 14.40.33807 | h98bb1dd_1 | | +wcwidth | 0.2.13 | pyhd8ed1ab_0 |conda-forge | +wheel | 0.44.0 |py312haa95532_0 | | +xz | 5.4.6 | h8cc25b3_1 | | +zeromq | 4.3.5 | hd77b12b_0 | | +zipp | 3.21.0 | pyhd8ed1ab_0 |conda-forge | +zlib | 1.2.13 | h8cc25b3_1 | | +zstd | 1.5.6 | h8880b57_0 | \ No newline at end of file