Skip to content

Commit

Permalink
Merge branch 'main' into flank-protection
Browse files Browse the repository at this point in the history
  • Loading branch information
arneboockmeyer committed Aug 1, 2023
2 parents 7ac6956 + 33f0290 commit fa23776
Show file tree
Hide file tree
Showing 20 changed files with 2,268 additions and 76 deletions.
103 changes: 97 additions & 6 deletions interlocking/infrastructureprovider/infrastructureprovider.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,65 @@
import logging
from abc import ABC, abstractmethod
import asyncio
from yaramo.model import Node, Signal, Topology


class InfrastructureProvider(ABC):

def __init__(self):
def __init__(self,
apply_for_signals: bool = True,
apply_for_points: bool = True,
only_apply_for_signals: list[str] = None,
only_apply_for_points: list[str] = None,
apply_for_all_signals_except: list[str] = None,
apply_for_all_points_except: list[str] = None):
self.apply_for_signals = apply_for_signals
self.apply_for_points = apply_for_points

if not self.apply_for_points and not self.apply_for_signals:
raise Exception("The infrastructure provider has to apply for signals, points or both.")

if only_apply_for_signals is None:
only_apply_for_signals = []
self.only_apply_for_signals = only_apply_for_signals
if only_apply_for_points is None:
only_apply_for_points = []
self.only_apply_for_points = only_apply_for_points
if apply_for_all_signals_except is None:
apply_for_all_signals_except = []
self.apply_for_all_signals_except = apply_for_all_signals_except
if apply_for_all_points_except is None:
apply_for_all_points_except = []
self.apply_for_all_points_except = apply_for_all_points_except

if len(self.only_apply_for_signals) > 0 and len(self.apply_for_all_signals_except) > 0:
raise Exception(f"You can not limit the infrastructure provider with only_apply_for_signals and "
f"apply_for_all_signals_except at the same time.")
if len(self.only_apply_for_points) > 0 and len(self.apply_for_all_points_except) > 0:
raise Exception(f"You can not limit the infrastructure provider with only_apply_for_points and "
f"apply_for_all_points_except at the same time.")

self.tds_count_in_callback = None
self.tds_count_out_callback = None

#
# Point Interaction
#

def is_point_covered(self, yaramo_point: Node):
if not self.apply_for_points:
return False
point_id = yaramo_point.uuid[-5:]
return point_id in self.only_apply_for_points or \
(len(self.only_apply_for_points) == 0 and point_id not in self.apply_for_all_points_except)

async def call_turn_point(self, yaramo_point: Node, target_orientation: str):
if self.is_point_covered(yaramo_point):
return await self.turn_point(yaramo_point, target_orientation)
# return True to skip this call and not prevent successfully turning of point.
return True

@abstractmethod
async def turn_point(self, yaramo_point, target_orientation: str):
async def turn_point(self, yaramo_point: Node, target_orientation: str):
"""This method will be called when the interlocking controller wants to set the point.
The `yaramo_point` is the yaramo identifier of the point and `target_orientation` is one of `"left"` and `"right"` """
pass
Expand All @@ -21,10 +68,22 @@ async def turn_point(self, yaramo_point, target_orientation: str):
# Signal Interaction
#

def is_signal_covered(self, yaramo_signal: Signal):
if not self.apply_for_signals:
return False
return yaramo_signal.name in self.only_apply_for_signals or \
(len(self.only_apply_for_signals) == 0 and yaramo_signal.name not in self.apply_for_all_signals_except)

async def call_set_signal_aspect(self, yaramo_signal: Signal, target_state: str):
if self.is_signal_covered(yaramo_signal):
return await self.set_signal_aspect(yaramo_signal, target_state)
# return True to skip this call and not prevent successfully turning of signal.
return True

@abstractmethod
async def set_signal_aspect(self, yaramo_signal, target_aspect):
"""This method will be called when the interlocking controller wants to change the signal-state of a specific signal.
`yaramo_signal` corresponds to the identifier of the signal in the yaramo model; `target_state` is one of `"halt"` and `"go"`.
async def set_signal_aspect(self, yaramo_signal: Signal, target_aspect: str):
"""This method will be called when the interlocking controller wants to change the signal-aspect of a specific signal.
`yaramo_signal` corresponds to the identifier of the signal in the yaramo model; `target_aspect` is one of `"halt"` and `"go"`.
"""
pass

Expand All @@ -45,3 +104,35 @@ def _set_tds_count_out_callback(self, tds_count_out_callback):
async def tds_count_out(self, track_segment_id, train_id: str):
"""Removes a train to the segment identified by the `segment_id`"""
self.tds_count_out_callback(track_segment_id, train_id)

#
# Verify that all elements are covered by some infrastructure providers and add a default provider if not
#

@staticmethod
def verify_all_elements_covered_by_infrastructure_provider(topology: Topology, infrastructure_providers, settings):
uncovered_signals = []
uncovered_points = []

for signal in list(topology.signals.values()):
if not any(ip.is_signal_covered(signal) for ip in infrastructure_providers):
logging.warning(f"The signal {signal.name} is not covered by any infrastructure provider.")
uncovered_signals.append(signal.name)

for point in list(topology.nodes.values()):
if len(point.connected_nodes) != 3:
continue # Skip all track ends
if not any(ip.is_point_covered(point) for ip in infrastructure_providers):
point_id = point.uuid[-5:]
logging.warning(f"The point {point_id} is not covered by any infrastructure provider.")
uncovered_points.append(point_id)

if len(uncovered_signals) > 0 or len(uncovered_points) > 0:
if settings.default_interlocking_provider is not None:
return settings.default_interlocking_provider(apply_for_points=len(uncovered_points) > 0,
apply_for_signals=len(uncovered_signals) > 0,
only_apply_for_points=uncovered_points,
only_apply_for_signals=uncovered_signals)
return None


Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

class LoggingInfrastructureProvider(InfrastructureProvider):

def __init__(self):
super().__init__()
def __init__(self, **kwargs):
super().__init__(**kwargs)

async def set_signal_aspect(self, yaramo_signal, target_aspect):
logging.info(f"{time.strftime('%X')} Set signal {yaramo_signal.name} to {target_aspect}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class RandomWaitInfrastructureProvider(InfrastructureProvider):

def __init__(self, fail_probability=0.0, signal_time_range: range = range(2, 5),
point_turn_time_range: range = range(5, 8), always_succeed_for: list[str] = None,
always_fail_for: list[str] = None):
super().__init__()
always_fail_for: list[str] = None, **kwargs):
super().__init__(**kwargs)
if always_succeed_for is None:
always_succeed_for = []
if always_fail_for is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

class SUMOInfrastructureProvider(InfrastructureProvider):

def __init__(self, traci_instance):
super().__init__()
def __init__(self, traci_instance, **kwargs):
super().__init__(**kwargs)
self.traci_instance = traci_instance

async def set_signal_aspect(self, yaramo_signal, target_aspect):
Expand Down
8 changes: 4 additions & 4 deletions interlocking/interlockingcontroller/pointcontroller.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from interlocking.model import OccupancyState
from interlocking.model import OccupancyState, Point
from interlocking.model.helper import Settings
from interlocking.infrastructureprovider import InfrastructureProvider
from .flankprotectioncontroller import FlankProtectionController
Expand All @@ -11,7 +11,7 @@ class PointController(object):

def __init__(self, signal_controller: SignalController, infrastructure_providers: list[InfrastructureProvider],
settings: Settings):
self.points = None
self.points: dict[str, Point] = {}
self.infrastructure_providers = infrastructure_providers
self.settings = settings
self.flank_protection_controller = FlankProtectionController(self, signal_controller)
Expand Down Expand Up @@ -61,11 +61,11 @@ async def turn_point(self, point, orientation):
# tasks = []
results = []
for infrastructure_provider in self.infrastructure_providers:
results.append(await infrastructure_provider.turn_point(point.yaramo_node, orientation))
results.append(await infrastructure_provider.call_turn_point(point.yaramo_node, orientation))

# async with asyncio.TaskGroup() as tg:
# for infrastructure_provider in self.infrastructure_providers:
# tasks.append(tg.create_task(infrastructure_provider.turn_point(point.yaramo_node, orientation)))
# tasks.append(tg.create_task(infrastructure_provider.call_turn_point(point.yaramo_node, orientation)))
# if all(list(map(lambda task: task.result(), tasks))):
if all(results):
point.orientation = orientation
Expand Down
4 changes: 2 additions & 2 deletions interlocking/interlockingcontroller/signalcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ async def set_signal_aspect(self, signal, signal_aspect):

results = []
for infrastructure_provider in self.infrastructure_providers:
results.append(await infrastructure_provider.set_signal_aspect(signal.yaramo_signal, signal_aspect))
results.append(await infrastructure_provider.call_set_signal_aspect(signal.yaramo_signal, signal_aspect))

# tasks = []
# async with asyncio.TaskGroup() as tg:
# for infrastructure_provider in self.infrastructure_providers:
# tasks.append(tg.create_task(infrastructure_provider.set_signal_state(signal.yaramo_signal, state)))
# tasks.append(tg.create_task(infrastructure_provider.call_set_signal_aspect(signal.yaramo_signal, state)))
# if all(list(map(lambda task: task.result(), tasks))):
if all(results):
signal.signal_aspect = signal_aspect
Expand Down
81 changes: 73 additions & 8 deletions interlocking/interlockinginterface.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
from interlocking.interlockingcontroller import PointController, SignalController, TrackController, TrainDetectionController
from interlocking.infrastructureprovider import InfrastructureProvider
from interlocking.model import Point, Track, Signal, Route
from interlocking.model.helper import SetRouteResult, Settings, InterlockingOperationType
from interlockinglogicmonitor import InterlockingLogicMonitor
from yaramo.model import Route as YaramoRoute
import asyncio
import time
import logging


class Interlocking(object):

def __init__(self, infrastructure_providers, settings=Settings()):
def __init__(self,
infrastructure_providers,
settings=Settings(),
interlocking_logic_monitor: InterlockingLogicMonitor = None):
if not isinstance(infrastructure_providers, list):
infrastructure_providers = [infrastructure_providers]
self.infrastructure_providers = infrastructure_providers
self.infrastructure_providers: list[InfrastructureProvider] = infrastructure_providers
self.settings = settings
self.interlocking_logic_monitor = interlocking_logic_monitor

self.signal_controller = SignalController(self.infrastructure_providers)
self.point_controller = PointController(self.signal_controller, self.infrastructure_providers, self.settings)
self.track_controller = TrackController(self, self.point_controller, self.signal_controller)
self.train_detection_controller = TrainDetectionController(self.track_controller, self.infrastructure_providers)
self.routes = []
self.active_routes = []
self.routes: list[Route] = []
self.active_routes: list[Route] = []

def prepare(self, yaramo_topoloy):
# Nodes
Expand All @@ -38,6 +45,12 @@ def prepare(self, yaramo_topoloy):
signals[yaramo_signal.uuid] = signal
self.signal_controller.signals = signals

new_ip = InfrastructureProvider.verify_all_elements_covered_by_infrastructure_provider(yaramo_topoloy,
self.infrastructure_providers,
self.settings)
if new_ip is not None:
self.infrastructure_providers.append(new_ip)

# Tracks
tracks = dict()
for edge_uuid in yaramo_topoloy.edges:
Expand Down Expand Up @@ -116,13 +129,20 @@ def print_state(self):
logging.debug(active_route.to_string())
logging.debug("##############")

async def set_route(self, yaramo_route, train_id: str):
async def set_route(self, yaramo_route: YaramoRoute, train_id: str):
route_formation_time_start = time.time()
set_route_result = SetRouteResult()

# Test, if train is already on track and if yes, check for consecutive routes:
if not self._is_route_valid_consecutive_route(yaramo_route, train_id):
set_route_result.success = False
return set_route_result

if not self.can_route_be_set(yaramo_route, train_id):
set_route_result.success = False
return set_route_result
route = self.get_route_from_yaramo_route(yaramo_route)
route: Route = self.get_route_from_yaramo_route(yaramo_route)
route.used_by = train_id
self.active_routes.append(route)

async with asyncio.TaskGroup() as tg:
Expand All @@ -138,7 +158,10 @@ async def set_route(self, yaramo_route, train_id: str):
# Set route failed, so the route has to be reset
await self.reset_route(yaramo_route, train_id)
set_route_result.success = False

set_route_result.route_formation_time = time.time() - route_formation_time_start
if self.interlocking_logic_monitor is not None:
self.interlocking_logic_monitor.monitor_set_route(yaramo_route)
return set_route_result

def can_route_be_set(self, yaramo_route, train_id: str):
Expand All @@ -155,21 +178,63 @@ def do_two_routes_collide(self, yaramo_route_1, yaramo_route_2):
return do_collide

def free_route(self, yaramo_route, train_id: str):
route = self.get_route_from_yaramo_route(yaramo_route)
route: Route = self.get_route_from_yaramo_route(yaramo_route)
if route not in self.active_routes:
raise Exception(f"Route from {yaramo_route.start_signal.name} to "
f"{yaramo_route.end_signal.name} was not set.")
if route.used_by != train_id:
raise Exception(f"Wrong Train ID: The route from {yaramo_route.start_signal.name} to "
f"{yaramo_route.end_signal.name} was not set with the train id "
f"{train_id}.")
self.track_controller.free_route(route, train_id)
self.signal_controller.free_route(route, train_id)
self.active_routes.remove(route)
route.used_by = None
if self.interlocking_logic_monitor is not None:
self.interlocking_logic_monitor.monitor_free_route(yaramo_route)

async def reset_route(self, yaramo_route, train_id: str):
route = self.get_route_from_yaramo_route(yaramo_route)
route: Route = self.get_route_from_yaramo_route(yaramo_route)
if route not in self.active_routes:
raise Exception(f"Route from {yaramo_route.start_signal.name} to "
f"{yaramo_route.end_signal.name} was not set.")
if route.used_by != train_id:
raise Exception(f"Wrong Train ID: The route from {yaramo_route.start_signal.name} to "
f"{yaramo_route.end_signal.name} was not set with the train id "
f"{train_id}.")
self.point_controller.reset_route(route, train_id)
self.track_controller.reset_route(route, train_id)
self.train_detection_controller.reset_track_segments_of_route(route)
await self.signal_controller.reset_route(route, train_id)
self.active_routes.remove(route)
route.used_by = None
if self.interlocking_logic_monitor is not None:
self.interlocking_logic_monitor.monitor_reset_route(yaramo_route)

def get_route_from_yaramo_route(self, yaramo_route):
for route in self.routes:
if route.yaramo_route.uuid == yaramo_route.uuid:
return route
return None

def _is_route_valid_consecutive_route(self, new_route: YaramoRoute, train_id: str):
all_routes_of_train = list(filter(lambda active_route: active_route.used_by == train_id, self.active_routes))
if len(all_routes_of_train) == 0:
# New train, per definition consecutive route
return True

# Find route with no successor
last_route = None
for route in all_routes_of_train:
# All other routes
found_successor = False
for other_route in all_routes_of_train:
if route.id != other_route.id:
if route.end_signal.yaramo_signal.name == other_route.start_signal.yaramo_signal.name:
found_successor = True
if not found_successor:
if last_route is not None:
raise ValueError("Multiple last routes found")
last_route = route
return last_route.end_signal.yaramo_signal.name == new_route.start_signal.name and \
last_route.start_signal.yaramo_signal.name != new_route.end_signal.name
12 changes: 11 additions & 1 deletion interlocking/model/helper/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
from interlocking.infrastructureprovider import LoggingInfrastructureProvider, InfrastructureProvider
from typing import Type


class Settings(object):

def __init__(self, max_number_of_points_at_same_time=5):
def __init__(self,
max_number_of_points_at_same_time: int = 5,
default_interlocking_provider: Type[InfrastructureProvider] | None = LoggingInfrastructureProvider):
self.max_number_of_points_at_same_time = max(max_number_of_points_at_same_time, 1)

# For all elements that are not covered by an infrastructure provider, an instance of this default provider will
# be created. This default provider can be None.
self.default_interlocking_provider: Type[InfrastructureProvider] | None = default_interlocking_provider
1 change: 1 addition & 0 deletions interlocking/model/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def __init__(self, yaramo_route):
self.end_signal = None
self.tracks = []
self.overlap = None
self.used_by = None

def contains_segment(self, segment):
for track in self.tracks:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ python = "^3.11"
yaramo = { git = "https://github.com/simulate-digital-rail/yaramo" }
planpro-importer = { git = "https://github.com/simulate-digital-rail/planpro-importer" }
railway-route-generator = { git = "https://github.com/simulate-digital-rail/railway-route-generator" }
interlocking-logic-monitor = { git = "https://github.com/simulate-digital-rail/interlocking-logic-monitor" }
#yaramo = { git = "https://github.com/bp2022-ap1/yaramo", branch="refactor/replace-connected-nodes-with-edges" }
#planpro-importer = { git = "https://github.com/bp2022-ap1/planpro-importer", branch = "refactor/replace-connected-nodes-edges" }
#railway-route-generator = { git = "https://github.com/bp2022-ap1/railway-route-generator", branch = "refactor/replace-connected-nodes-edges" }
Expand Down
Loading

0 comments on commit fa23776

Please sign in to comment.