Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for flank protection #13

Merged
merged 21 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6da5799
Add state for signals and add tests for flank protection
arneboockmeyer Jun 30, 2023
e8716dd
Merge branch 'async' into flank-protection
arneboockmeyer Jun 30, 2023
2532d08
Add Halt zeigendes Signal and Schutzweiche as flank protections
arneboockmeyer Jul 5, 2023
7af45c1
Support Flank Protection Transport Points
arneboockmeyer Jul 5, 2023
f853fe1
Fix test imports
arneboockmeyer Jul 5, 2023
32e5852
Add additional signal in test one and fix signal selection
arneboockmeyer Jul 5, 2023
b473bb5
Add flank protection test that tests two routes
arneboockmeyer Jul 10, 2023
7ac6956
Merge branch 'async' into flank-protection
arneboockmeyer Jul 18, 2023
fa23776
Merge branch 'main' into flank-protection
arneboockmeyer Aug 1, 2023
935a44c
Merge branch 'main' into flank-protection
arneboockmeyer Jun 24, 2024
c9affcb
Add flank protection flag
arneboockmeyer Jun 27, 2024
8495793
Update poetry lock
arneboockmeyer Jun 27, 2024
2288139
Add flank protection reset and increase logging
arneboockmeyer Jun 27, 2024
140c3f6
Add test for three consectutive routes with overlap
arneboockmeyer Jun 27, 2024
ad2e959
Fix reset of signals
arneboockmeyer Jun 27, 2024
71ed295
Fix reset test
arneboockmeyer Jun 27, 2024
020f5d8
Add flank protection for overlaps
arneboockmeyer Jun 27, 2024
a209a6c
Free flank protection for routes
arneboockmeyer Jun 27, 2024
5c8e725
Fix typo
arneboockmeyer Jun 27, 2024
be35ef1
Remove old code
arneboockmeyer Sep 30, 2024
c00f84e
Remove old flank protection occupancy states
arneboockmeyer Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions interlocking/infrastructureprovider/infrastructureprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,16 @@ def is_signal_covered(self, yaramo_signal: Signal):
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_state(self, yaramo_signal: Signal, target_state: str):
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_state(yaramo_signal, target_state)
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_state(self, yaramo_signal: Signal, target_state: str):
"""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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class LoggingInfrastructureProvider(InfrastructureProvider):
def __init__(self, **kwargs):
super().__init__(**kwargs)

async def set_signal_state(self, yaramo_signal, target_state):
logging.info(f"{time.strftime('%X')} Set signal {yaramo_signal.name} to {target_state}")
async def set_signal_aspect(self, yaramo_signal, target_aspect):
logging.info(f"{time.strftime('%X')} Set signal {yaramo_signal.name} to {target_aspect}")
return True

async def turn_point(self, yaramo_point, target_orientation: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ def __init__(self, fail_probability=0.0, signal_time_range: range = range(2, 5),
self.always_succeed_for = always_succeed_for
self.always_fail_for = always_fail_for

async def set_signal_state(self, yaramo_signal: Signal, target_state):
async def set_signal_aspect(self, yaramo_signal: Signal, target_aspect: str):
wait = random.sample(self.signal_time_range, 1)[0]
await asyncio.sleep(wait)
if (random.random() >= self.fail_probability or yaramo_signal.name in self.always_succeed_for) \
and yaramo_signal.name not in self.always_fail_for:
logging.info(f"{time.strftime('%X')} Completed setting signal {yaramo_signal.name} to {target_state} (waited {wait})")
logging.info(f"{time.strftime('%X')} Completed setting signal {yaramo_signal.name} to {target_aspect} (waited {wait})")
return True
logging.warning(f"{time.strftime('%X')} Failed setting signal {yaramo_signal.name} to {target_state} (waited {wait})")
logging.warning(f"{time.strftime('%X')} Failed setting signal {yaramo_signal.name} to {target_aspect} (waited {wait})")
return False

async def turn_point(self, yaramo_point: Node, target_orientation: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ def __init__(self, traci_instance, **kwargs):
super().__init__(**kwargs)
self.traci_instance = traci_instance

async def set_signal_state(self, yaramo_signal, target_state):
if target_state == "go":
async def set_signal_aspect(self, yaramo_signal, target_aspect):
if target_aspect == "go":
self.traci_instance.trafficlight.setRedYellowGreenState(yaramo_signal.name, "GG")
elif target_state == "halt":
elif target_aspect == "halt":
if yaramo_signal.direction == SignalDirection.IN:
self.traci_instance.trafficlight.setRedYellowGreenState(yaramo_signal.name, "rG")
else:
Expand Down
106 changes: 106 additions & 0 deletions interlocking/interlockingcontroller/flankprotectioncontroller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from .signalcontroller import SignalController
from interlocking.model import Route, Point, OccupancyState, Signal
from yaramo.model import SignalDirection, NodeConnectionDirection
import logging


class FlankProtectionController(object):

def __init__(self, point_controller, signal_controller: SignalController):
self.point_controller = point_controller
self.signal_controller = signal_controller

def reset(self):
for point in self.point_controller.points.values():
point.is_used_for_flank_protection = False
for signal in self.signal_controller.signals.values():
signal.is_used_for_flank_protection = False

async def add_flank_protection_for_point(self, point: Point, point_orientation: str,
route: Route, train_id: str) -> bool:
signals, points = self._get_flank_protection_elements_of_point(point, point_orientation)
results = []
for signal in signals:
logging.info(f"--- Use signal {signal.yaramo_signal.name} for flank protection as 'halt-zeigendes Signal'")
change_successful = await self.signal_controller.set_signal_halt(signal)
results.append(change_successful)
if change_successful:
signal.is_used_for_flank_protection = True
for point in points:
orientation = points[point]
if orientation is not None:
# In case of a Schutztansportweiche the orientation is not relevant (None).
logging.info(f"--- Use point {point.point_id} for flank protection as 'Schutzweiche'")
change_successful = await self.point_controller.turn_point(point, orientation)
results.append(change_successful)
if change_successful:
point.is_used_for_flank_protection = True
else:
logging.info(f"--- Use point {point.point_id} for flank protection as 'Schutztransportweiche'")
point.is_used_for_flank_protection = True
return all(results)

def free_flank_protection_of_point(self, point: Point, point_orientation: str):
signals, points = self._get_flank_protection_elements_of_point(point, point_orientation)
for signal in signals:
signal.is_used_for_flank_protection = False
for point in points:
point.is_used_for_flank_protection = False

def _get_flank_protection_elements_of_point(self,
point: Point,
point_orientation: str | None) -> tuple[list[Signal],
dict[Point, str | None]]:
flank_protection_tracks = []
if point_orientation is None:
# It's only none, iff there is a flank protection transport point (where the flank
# protection area comes from Spitze
flank_protection_tracks = [point.left, point.right]
elif point_orientation == "left":
flank_protection_tracks = [point.right]
elif point_orientation == "right":
flank_protection_tracks = [point.left]

signal_results: list[Signal] = []
point_results: dict[Point, str | None] = {}

for flank_protection_track in flank_protection_tracks:
# Search for signals
yaramo_edge = flank_protection_track.yaramo_edge
node_a = point.yaramo_node
node_b = yaramo_edge.get_other_node(node_a)
direction = yaramo_edge.get_direction_based_on_nodes(node_a, node_b)

opposite_direction = SignalDirection.IN
if direction == SignalDirection.IN:
opposite_direction = SignalDirection.GEGEN
yaramo_signals_in_direction = yaramo_edge.get_signals_with_direction_in_order(opposite_direction)
# If there is any signal, take the closest one to the point and use it as halt-showing signal.
found_signal = False
if len(yaramo_signals_in_direction) > 0:
yaramo_signal = yaramo_signals_in_direction[-1] # Take the last one, which is the closest one.
for signal_uuid in self.signal_controller.signals:
if signal_uuid == yaramo_signal.uuid:
signal_results.append(self.signal_controller.signals[signal_uuid])
found_signal = True
break

# No Halt zeigendes Signal detected. Try to find Schutzweiche
if not found_signal:
other_point: Point | None = None
for point_uuid in self.point_controller.points:
_point: Point = self.point_controller.points[point_uuid]
if node_b.uuid == _point.yaramo_node.uuid:
other_point = _point

if other_point is not None and other_point.is_point:
connection_direction = other_point.get_connection_direction_of_track(flank_protection_track)
if connection_direction == NodeConnectionDirection.Spitze:
point_results[other_point] = None
signal_results, sub_point_results = self._get_flank_protection_elements_of_point(other_point, None)
point_results = point_results | sub_point_results
elif connection_direction == NodeConnectionDirection.Links:
point_results[other_point] = "right"
elif connection_direction == NodeConnectionDirection.Rechts:
point_results[other_point] = "left"
return signal_results, point_results
7 changes: 5 additions & 2 deletions interlocking/interlockingcontroller/overlapcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async def reserve_overlap_of_route(self, route, train_id: str):
if overlap is None:
raise ValueError("No reservable overlap found")
self.reserve_segments_of_overlap(overlap, train_id)
success = await self.reserve_points_of_overlap(overlap, train_id)
success = await self.reserve_points_of_overlap(overlap, route, train_id)
route.overlap = overlap
return success

Expand Down Expand Up @@ -50,7 +50,7 @@ def reserve_segments_of_overlap(self, overlap, train_id: str):
segment.state = OccupancyState.RESERVED_OVERLAP
segment.used_by.add(train_id)

async def reserve_points_of_overlap(self, overlap, train_id: str):
async def reserve_points_of_overlap(self, overlap, route, train_id: str):
tasks = []
async with asyncio.TaskGroup() as tg:
for point in overlap.points:
Expand All @@ -69,6 +69,9 @@ async def reserve_points_of_overlap(self, overlap, train_id: str):
raise ValueError("Overlap contains points without 2 of their tracks")
necessery_orientation = point.get_necessary_orientation(found_tracks[0], found_tracks[1])
tasks.append(tg.create_task(self.point_controller.turn_point(point, necessery_orientation)))
tasks.append(tg.create_task(self.point_controller.flank_protection_controller.
add_flank_protection_for_point(point, necessery_orientation, route,
train_id)))
return all(list(map(lambda task: task.result(), tasks)))

def free_overlap_of_route(self, route, train_id: str):
Expand Down
18 changes: 16 additions & 2 deletions interlocking/interlockingcontroller/pointcontroller.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
from interlocking.model import OccupancyState, Point
from interlocking.model.helper import Settings
from interlocking.infrastructureprovider import InfrastructureProvider
from .flankprotectioncontroller import FlankProtectionController
from .signalcontroller import SignalController
import asyncio
import logging


class PointController(object):

def __init__(self, infrastructure_providers, settings):
def __init__(self, signal_controller: SignalController, infrastructure_providers: list[InfrastructureProvider],
settings: Settings):
self.points: dict[str, Point] = {}
self.infrastructure_providers = infrastructure_providers
self.settings = settings
self.flank_protection_controller = FlankProtectionController(self, signal_controller)

def reset(self):
for point_id in self.points:
self.points[point_id].orientation = "undefined"
self.points[point_id].state = OccupancyState.FREE
self.points[point_id].used_by = set()
self.flank_protection_controller.reset()

async def set_route(self, route, train_id: str):
tasks = []
Expand All @@ -27,6 +34,7 @@ async def set_route(self, route, train_id: str):
if orientation == "left" or orientation == "right":
self.set_point_reserved(point, train_id)
tasks.append(tg.create_task(self.turn_point(point, orientation)))
tasks.append(tg.create_task(self.flank_protection_controller.add_flank_protection_for_point(point, orientation, route, train_id)))
else:
raise ValueError("Turn should happen but is not possible")

Expand All @@ -50,6 +58,10 @@ async def turn_point(self, point, orientation):
if point.orientation == orientation:
# Everything is fine
return True
if point.is_used_for_flank_protection is True:
logging.error(f"Can not turn point of point {point.point_id} to {orientation}, "
f"since it is used for flank protection.")
return False
logging.info(f"--- Move point {point.point_id} to {orientation}")
# tasks = []
results = []
Expand Down Expand Up @@ -77,6 +89,7 @@ def set_point_free(self, point, train_id: str):
logging.info(f"--- Set point {point.point_id} to free")
point.state = OccupancyState.FREE
point.used_by.remove(train_id)
self.flank_protection_controller.free_flank_protection_of_point(point, point.orientation)

def reset_route(self, route, train_id: str):
for point in route.get_points_of_route():
Expand All @@ -86,4 +99,5 @@ def print_state(self):
logging.debug("State of Points:")
for point_id in self.points:
point = self.points[point_id]
logging.debug(f"{point.point_id}: {point.state} (Orientation: {point.orientation}) (used by: {point.used_by})")
logging.debug(f"{point.point_id}: {point.state} (Orientation: {point.orientation}) "
f"(used by: {point.used_by}) (is used for FP: {point.is_used_for_flank_protection})")
57 changes: 41 additions & 16 deletions interlocking/interlockingcontroller/signalcontroller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import logging
from interlocking.model import Signal
from interlocking.model import OccupancyState, Signal


class SignalController(object):
Expand All @@ -11,45 +11,70 @@ def __init__(self, infrastructure_providers):

async def reset(self):
# Run non-concurrently
for signal_id in self.signals:
await self.set_signal_halt(self.signals[signal_id])
for signal in self.signals.values():
await self.set_signal_halt(signal)
signal.state = OccupancyState.FREE
signal.used_by = set()

async def set_route(self, route):
return await self.set_signal_go(route.start_signal)

async def set_route(self, route, train_id: str):
result = await self.set_signal_go(route.start_signal)
if result:
route.start_signal.state = OccupancyState.RESERVED
route.start_signal.used_by.add(train_id)
return result

async def set_signal_halt(self, signal):
return await self.set_signal_state(signal, "halt")
return await self.set_signal_aspect(signal, "halt")

async def set_signal_go(self, signal):
return await self.set_signal_state(signal, "go")
return await self.set_signal_aspect(signal, "go")

async def set_signal_state(self, signal, state):
if signal.state == state:
async def set_signal_aspect(self, signal, signal_aspect):
if signal.signal_aspect == signal_aspect:
# Everything is fine
return True
logging.info(f"--- Set signal {signal.yaramo_signal.name} to {state}")
if signal.is_used_for_flank_protection is True:
logging.error(f"Can not set signal aspect of signal {signal.yaramo_signal.name} to {signal_aspect}, "
f"since it is used for flank protection.")
return False
logging.info(f"--- Set signal {signal.yaramo_signal.name} to {signal_aspect}")

results = []
for infrastructure_provider in self.infrastructure_providers:
results.append(await infrastructure_provider.call_set_signal_state(signal.yaramo_signal, state))
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.call_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.state = state
signal.signal_aspect = signal_aspect
return True
else:
# TODO: Incident
return False

async def reset_route(self, route):
await self.set_signal_halt(route.start_signal)
def free_route(self, route, train_id: str):
if route.start_signal.signal_aspect == "go":
raise ValueError("Try to free route with start signal aspect is go")
self.free_signal(route.start_signal, train_id)

async def reset_route(self, route, train_id: str):
result = await self.set_signal_halt(route.start_signal)
if result:
route.start_signal.state = OccupancyState.FREE
if train_id in route.start_signal.used_by:
route.start_signal.used_by.remove(train_id)

def free_signal(self, signal: Signal, train_id: str):
signal.state = OccupancyState.FREE
signal.used_by.remove(train_id)

def print_state(self):
logging.debug("State of Signals:")
for signal_uuid in self.signals:
signal = self.signals[signal_uuid]
logging.debug(f"{signal.yaramo_signal.name}: {signal.state}")
logging.debug(f"{signal.yaramo_signal.name}: {signal.state} (Signal Aspect: {signal.signal_aspect}) "
f"(is used for FP: {signal.is_used_for_flank_protection})")
Loading
Loading