2023-10-19 06:49:13 +00:00
import time
import sys
import datetime
import threading
class SimulationThread ( threading . Thread ) :
2023-10-21 05:12:40 +00:00
"""
Singleton class for the simulation thread .
Usage is the call the classmethod initialize first
"""
2023-10-19 06:49:13 +00:00
instance = None
@classmethod
2023-10-21 06:17:10 +00:00
def initialize ( cls , periods , start_time = None , run_time = None , time_factor = 1.0 , verbose = False ) :
2023-10-21 05:12:40 +00:00
"""
Initializes the singleton Thread with the given values . The given arguments are passed directly to the instance constructor . See the ` __init__ ` method for argument details .
"""
2023-10-21 06:17:10 +00:00
cls . instance = SimulationThread ( periods , start_time , run_time , time_factor , verbose )
2023-10-20 06:05:24 +00:00
@classmethod
2023-10-21 05:12:40 +00:00
def get_instance ( cls ) :
"""
Returns the simulation thread object , or None if initialize ( ) has not been called .
"""
2023-10-19 06:49:13 +00:00
return cls . instance
2023-10-21 06:17:10 +00:00
def __init__ ( self , periods , start_time , run_time , time_factor , verbose ) :
2023-10-21 05:12:40 +00:00
"""
Creates a simulation with the provided data and settings .
Args :
periods ( list ) : A list of dictionaries that represent the traffic light periods ( peak / off - peak etc ) as specified in the problem description . See the provided ` periods . json ` file for the intended format , as this variable is intended to be a json . load ( ) version of this data .
start_time ( datetime . time ) : The time at which the simulation should start .
run_time ( int ) : The amount of ( real ) time the simulation should run for before terminating . If None , run indefinitely or until signal_stop ( ) is called .
time_factor ( float ) : The time " accelleration " factor at which the simulation should run . A value of 1.0 indicates indentity with real time , and higher values run faster ( e . g . 4.0 = four simulated seconds for each real second ) .
2023-10-21 06:17:10 +00:00
verbose ( bool ) : If True , outputs progress information to stdout ( for testing purposes only ) .
2023-10-21 05:12:40 +00:00
"""
# Parent constructor call (required). Uses parameter daemon=True to ensure thread shutdown when parent program finishes executing
# See https://docs.python.org/3/library/threading.html
2023-10-19 06:49:13 +00:00
super ( ) . __init__ ( daemon = True )
2023-10-21 05:12:40 +00:00
2023-10-19 06:49:13 +00:00
self . mutex = threading . Lock ( )
2023-10-21 05:12:40 +00:00
2023-10-19 06:49:13 +00:00
self . periods = periods
2023-10-21 06:38:12 +00:00
self . start_simulation_time = start_time
self . run_real_time = run_time
2023-10-19 06:49:13 +00:00
self . time_factor = time_factor
2023-10-21 06:17:10 +00:00
self . verbose = verbose
2023-10-19 06:49:13 +00:00
2023-10-21 05:12:40 +00:00
# NOT a status variable, instead indicates whether signal_stop() has been called
self . running = True
2023-10-19 06:49:13 +00:00
self . current_period_index = 0
self . current_state_index = 0
2023-10-21 06:38:12 +00:00
self . next_changeover_real_time = None
self . start_simulation_datetime = None
self . current_simulation_datetime = None
2023-10-19 06:49:13 +00:00
2023-10-21 05:12:40 +00:00
# For convenience, convert all ISO format times to datetime.time objects
2023-10-19 06:49:13 +00:00
for period in self . periods :
period [ " timestart " ] = datetime . time . fromisoformat ( period [ " timestart " ] )
2023-10-19 07:01:52 +00:00
# Essential - periods are assumed to be in chronological order
self . periods . sort ( key = lambda x : x [ " timestart " ] )
2023-10-19 06:49:13 +00:00
def signal_stop ( self ) :
2023-10-21 05:12:40 +00:00
"""
Signals the thread to stop , regardless of run time
"""
2023-10-19 06:49:13 +00:00
self . mutex . acquire ( )
self . running = False
self . mutex . release ( )
def force_to_time ( self , new_time ) :
2023-10-21 05:12:40 +00:00
"""
Resets the simulation time to the provided time . This is for testing purposes ; the current state is IGNORED and the sequence immediately resets to the beginning .
"""
2023-10-21 06:38:12 +00:00
new_simulation_datetime = datetime . datetime . fromisoformat ( " 1900-01-01 " + new_time . isoformat ( ) )
2023-10-19 06:49:13 +00:00
self . mutex . acquire ( )
print ( )
2023-10-21 06:38:12 +00:00
self . current_period_index = self . _get_period_index ( self . periods , new_simulation_datetime . time ( ) )
2023-10-19 06:49:13 +00:00
self . current_state_index = 0
2023-10-21 06:38:12 +00:00
self . next_changeover_real_time = time . time ( ) + ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] [ " duration " ] / self . time_factor )
2023-10-19 06:49:13 +00:00
self . mutex . release ( )
def get_snapshot ( self ) :
2023-10-21 05:12:40 +00:00
"""
Returns a dictionary of the current simulated state
"""
2023-10-19 06:49:13 +00:00
self . mutex . acquire ( )
2023-10-20 09:41:31 +00:00
return_data = {
2023-10-21 06:38:12 +00:00
" current_period_name " : self . periods [ self . current_period_index ] [ " name " ] ,
2023-10-20 09:41:31 +00:00
" current_period_verbose_name " : self . periods [ self . current_period_index ] [ " verbose-name " ] ,
2023-10-21 05:52:26 +00:00
" current_period_index " : self . current_period_index ,
2023-10-21 06:38:12 +00:00
" current_state_index " : self . current_state_index ,
" current_state_data " : self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] ,
" next_changeover_real_time " : self . next_changeover_real_time ,
2023-10-20 09:41:31 +00:00
" simulation_time " : self . current_simulation_datetime . time ( ) ,
2023-10-21 06:38:12 +00:00
" time_remaining " : ( self . next_changeover_real_time - time . time ( ) ) * self . time_factor ,
2023-10-20 09:41:31 +00:00
}
2023-10-19 06:49:13 +00:00
self . mutex . release ( )
return return_data
2023-10-21 06:03:44 +00:00
def _get_period_index ( self , test_time : datetime . time ) :
2023-10-21 05:12:40 +00:00
"""
Returns the index of the period corresponding to the given time
"""
2023-10-21 06:03:44 +00:00
for i , period in enumerate ( self . periods ) :
if i == len ( self . periods ) - 1 :
2023-10-19 06:49:13 +00:00
return i
2023-10-21 06:03:44 +00:00
if test_time > = period [ " timestart " ] and \
test_time < self . periods [ i + 1 ] [ " timestart " ] :
2023-10-19 06:49:13 +00:00
return i
# Should NOT be reached since last state assumed to last until midnight
2023-10-21 05:12:40 +00:00
raise
2023-10-19 06:49:13 +00:00
def run ( self ) :
2023-10-21 05:12:40 +00:00
"""
Main thread loop . Note that this method should NOT be called directly : as a subclass of threading . Thread , the inherited start ( ) method should be used instead .
The loop executes until one of the following conditions is met :
- The given run_time is exceeded ( in real seconds ) , or
- signal_stop ( ) has been called
- The executing program finishes ( daemon = True )
"""
2023-10-21 06:38:12 +00:00
start_real_time = time . time ( )
2023-10-19 06:49:13 +00:00
2023-10-21 06:38:12 +00:00
if self . start_simulation_time is None :
self . start_simulation_datetime = datetime . datetime . now ( )
2023-10-19 06:49:13 +00:00
else :
2023-10-21 06:38:12 +00:00
self . start_simulation_datetime = datetime . datetime . fromisoformat (
" 1900-01-01 " + self . start_simulation_time . isoformat ( ) )
2023-10-19 06:49:13 +00:00
2023-10-21 06:38:12 +00:00
self . current_period_index = self . _get_period_index ( self . start_simulation_datetime . time ( ) )
2023-10-19 06:49:13 +00:00
self . current_state_index = 0
2023-10-21 06:38:12 +00:00
self . next_changeover_real_time = start_real_time + ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] [ " duration " ] / self . time_factor )
2023-10-19 06:49:13 +00:00
2023-10-21 06:17:10 +00:00
if self . verbose :
2023-10-21 06:38:12 +00:00
print ( " Starting with simulation time {} " . format ( self . start_simulation_datetime . time ( ) ) )
2023-10-21 06:17:10 +00:00
print ( " Initial state data: {} " . format ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] ) )
2023-10-21 06:38:12 +00:00
if self . run_real_time is None :
2023-10-21 06:17:10 +00:00
print ( " No simulation time limit. " )
else :
2023-10-21 06:38:12 +00:00
print ( " Simulation duration (in real second(s)): {} " . format ( self . run_real_time ) )
2023-10-21 06:17:10 +00:00
print ( " Time compression factor: {} " . format ( self . time_factor ) )
2023-10-19 06:49:13 +00:00
2023-10-21 06:38:12 +00:00
while ( self . run_real_time is None ) or ( self . run_real_time is not None and time . time ( ) < start_real_time + self . run_real_time ) :
2023-10-19 06:49:13 +00:00
self . mutex . acquire ( )
2023-10-21 05:12:40 +00:00
2023-10-19 06:49:13 +00:00
if not self . running :
self . mutex . release ( )
break
now = time . time ( )
2023-10-21 06:38:12 +00:00
self . current_simulation_datetime = self . start_simulation_datetime + datetime . timedelta ( seconds = int ( ( now - start_real_time ) * self . time_factor ) )
2023-10-21 06:03:44 +00:00
timed_period_index = self . _get_period_index ( self . current_simulation_datetime . time ( ) )
2023-10-19 06:49:13 +00:00
2023-10-21 06:38:12 +00:00
if now > = self . next_changeover_real_time :
2023-10-21 06:03:44 +00:00
timed_period_index = self . _get_period_index ( self . current_simulation_datetime . time ( ) )
2023-10-19 06:49:13 +00:00
if timed_period_index != self . current_period_index and self . current_state_index == len ( self . periods [ self . current_period_index ] [ " states " ] ) - 1 :
2023-10-21 06:17:10 +00:00
if self . verbose :
print ( )
print ( " {} : Changing PERIOD from {} to {} " . format ( self . current_simulation_datetime . time ( ) , self . periods [ self . current_period_index ] [ " name " ] , self . periods [ timed_period_index ] [ " name " ] ) )
print ( " Old state data: {} " . format ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] ) )
2023-10-19 06:49:13 +00:00
2023-10-21 05:12:40 +00:00
# Safe to change period
2023-10-19 06:49:13 +00:00
self . current_period_index = timed_period_index
self . current_state_index = 0
2023-10-21 06:17:10 +00:00
if self . verbose :
print ( " New state data: {} " . format ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] ) )
print ( " Next changeover in {} second(s) " . format ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] [ " duration " ] ) )
2023-10-19 06:49:13 +00:00
else :
2023-10-21 05:12:40 +00:00
# Current sequence still completing, not safe to change to next period
2023-10-19 06:49:13 +00:00
next_cycle_index = ( self . current_state_index + 1 ) % len ( self . periods [ self . current_period_index ] [ " states " ] )
2023-10-21 06:17:10 +00:00
if self . verbose :
print ( )
print ( " {} : {} : Changing state from {} to {} " . format ( self . current_simulation_datetime . time ( ) , self . periods [ self . current_period_index ] [ " name " ] , self . current_state_index , next_cycle_index ) )
if self . current_period_index != timed_period_index :
print ( " [period change pending] " )
print ( " Old state data: {} " . format ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] ) )
print ( " New state data: {} " . format ( self . periods [ self . current_period_index ] [ " states " ] [ next_cycle_index ] ) )
print ( " Next changeover in {} second(s) " . format ( self . periods [ self . current_period_index ] [ " states " ] [ next_cycle_index ] [ " duration " ] ) )
2023-10-19 06:49:13 +00:00
self . current_state_index = next_cycle_index
2023-10-21 06:38:12 +00:00
# Ensure this calculation is done based on the PREVIOUS scheduled time to avoid "timing drift"
self . next_changeover_real_time = self . next_changeover_real_time + ( self . periods [ self . current_period_index ] [ " states " ] [ self . current_state_index ] [ " duration " ] / self . time_factor )
2023-10-19 06:49:13 +00:00
else :
2023-10-21 06:17:10 +00:00
if self . verbose :
print ( " . " , end = " " )
sys . stdout . flush ( )
2023-10-19 06:49:13 +00:00
self . mutex . release ( )
time . sleep ( 1.0 / self . time_factor )