Introduction to the Digital Twin part 2: different scenarios, more details.¶
In this notebook we will build on what we saw in Part 1, and look at some more complex scenarios, as well as how to programatically access the data in the environment.
# import the top-level "Simulator" class.
from bluebird_dt.simulator import Simulator
# import everything necessary for the visualisation
import matplotlib.pyplot as plt
import os
import shutil
from IPython.display import SVG, Image
%matplotlib inline
from bluebird_dt.core import Pos2D
from bluebird_dt.render import Radar
view_centre = Pos2D.from_str("50.75N 3.5W")
view_width = 60.0 # [nmi] about 1 degree of longitude
aspect_ratio = 1
Simple "Y-Sector" scenario¶
The "Y" sector is one level of complexity up from the I-sector - there are now three legs to the sector, which all intersect in the middle. We can again generate a simple scenario with two aircraft in this sector.
sim = Simulator.from_category("Artificial", "Y-Sector Two Aircraft")
import IPython.display
# the radar is re-instantiated. no need to clear the screen.
radar = Radar(view_centre, view_width, aspect_ratio) # re-introduce the spines/axes
for _ in range(200):
sim.evolve(6.0)
figure, ax = radar.draw(sim.manager.environment)
IPython.display.display(figure) # or IPython.display.display(plt.gcf())
IPython.display.clear_output(wait=True)
How do I see what scenarios are available?¶
So far we have looked at two "Artificial" scenarios, but how do we know what is available to us, to write as the "category" and "scenario_name" arguments of Simulator.from_category(category, scenario_name)? The easiest way is for us to use the following functions:
from bluebird_dt.simulator.common import list_sim_scenario_categories, list_sim_scenarios
# list the categories
list_sim_scenario_categories()
['Artificial', 'Springfield', 'Infinite']
# pick one of these and list the scenarios within it
list_sim_scenarios("Springfield")
['example-scenario', 'llm-scenario', 'testScenario']
Loading a Springfield scenario¶
The "Springfield" airspace is more complex than the very simple Artificial airspaces we have seen so far - it has a more complex geometry, and a set of possible "airways" that are designed to test how agents (or humans) could deal with tricky problems in real life Air Traffic Control.
sim = Simulator.from_category("Springfield", "example-scenario")
In order to show the Springfield airspace, we will need to modify the settings for the Radar class
view_centre = Pos2D.from_str("51.5N 1W")
view_width = 240.0 # [nmi] about 1 degree of longitude
aspect_ratio = 1.6
radar = Radar(view_centre, view_width, aspect_ratio)
radar.draw(sim.manager.environment)
plt.show()
Accessing the environment programatically¶
You probably noticed that when we did the visualisation in the cell above, the object we passed to Radar's draw method was sim.manager.environment. This is an instance of the bluebird_dt Environment class, and it holds essentially all the data that we (or an AI agent) might need, at a particular snapshot of time.
For example, we can get the current positions of all the aircraft:
for aircraft in sim.manager.environment.aircraft.values():
print(f"{aircraft.callsign}: latlon = ({aircraft.lat},{aircraft.lon}), Flight Level = {aircraft.fl}")
AIR01: latlon = (49.5230110183851,-1.21774853983122), Flight Level = 20.0 AIR03: latlon = (53.0821052255996,-0.072430948633466), Flight Level = 250.0
If we evolve the simulation a few steps, we can see how these positions change.
for _ in range(10):
sim.evolve(6)
for aircraft in sim.manager.environment.aircraft.values():
print(f"{aircraft.callsign}: latlon = ({aircraft.lat},{aircraft.lon}), Flight Level = {aircraft.fl}")
AIR01: latlon = (49.67915642787576,-1.138333898163054), Flight Level = 40.000000000000014 AIR03: latlon = (52.975459018413716,-0.12137608775694521), Flight Level = 250.0
So we can see that both aircraft moved in this time.
How do I know what data is available for Environment, Aircraft, etc.?¶
There are a couple of options:
- You can look at the code itself - the classes that represent "things" such as Aircraft are in the
bluebird_dt/core/directory. - You can look at the auto-generated source code reference in the web-based documentation.
- You can use the python
helporinspectfunctionality to investigate the class you are interested in
from bluebird_dt.core import Aircraft
help(Aircraft)
Help on class Aircraft in module bluebird_dt.core.aircraft:
class Aircraft(bluebird_dt.mixin.core_mixin.Comparison)
| Aircraft(
| lat: 'float',
| lon: 'float',
| fl: 'float',
| heading: 'float',
| flight_plan: 'FlightPlan | None',
| callsign: 'str',
| selected_fl: 'int | None' = None,
| ufid: 'str | None' = None,
| rate_of_turn: 'float | None' = None,
| aircraft_type: 'str | None' = None,
| operation_params: 'dict | None' = None,
| controllable: 'bool' = True,
| simulated: 'bool' = True,
| current_sector: 'str | None' = None,
| random_seed: 'int | None' = None,
| pilot: 'Pilot | None' = None,
| squawk: 'str | None' = None,
| wake_vortex: 'str | None' = None,
| last_passed_filed_idx: 'int | None' = None,
| last_passed_current_idx: 'int | None' = None,
| squawk_ident_until: 'float | None' = None
| )
|
| An aeroplane, helicopter, or other machine capable of flight.
|
| In its most minimal form, an Aircraft has a position, heading, speed and a callsign.
| Optionally, it can have operational parameters and information associated with how
| it is viewed from an air traffic control point of view.
|
| Method resolution order:
| Aircraft
| bluebird_dt.mixin.core_mixin.Comparison
| builtins.object
|
| Methods defined here:
|
| __init__(
| self,
| lat: 'float',
| lon: 'float',
| fl: 'float',
| heading: 'float',
| flight_plan: 'FlightPlan | None',
| callsign: 'str',
| selected_fl: 'int | None' = None,
| ufid: 'str | None' = None,
| rate_of_turn: 'float | None' = None,
| aircraft_type: 'str | None' = None,
| operation_params: 'dict | None' = None,
| controllable: 'bool' = True,
| simulated: 'bool' = True,
| current_sector: 'str | None' = None,
| random_seed: 'int | None' = None,
| pilot: 'Pilot | None' = None,
| squawk: 'str | None' = None,
| wake_vortex: 'str | None' = None,
| last_passed_filed_idx: 'int | None' = None,
| last_passed_current_idx: 'int | None' = None,
| squawk_ident_until: 'float | None' = None
| )
| Construct a new instance.
|
| Parameters
| ----------
| lat: float
| The Aircraft latitude in degrees.
| lon: float
| The Aircraft longitude in degrees.
| fl: float
| The Aircraft flight level.
| heading: float
| The Aircraft heading in degrees.
| flight_plan: FlightPlan
| A dict of the Aircraft planned Route
| callsign: str
| The Aircraft callsign.
| selected_fl: int, optional
| The flight level selected on the aircraft computer
| ufid : str, optional
| The Aircraft's unique flight ID.
| rate_of_turn: float, optional
| The Aircraft rate of turn in degrees/sec.
| aircraft_type: str, optional
| The Aircraft type
| operation_params: dict, optional
| Dictionary of aircraft operational parameters.
| controllable: bool, optional
| Indicates whether the Aircraft responds to Agent issued Actions (default=True).
| simulated: bool, optional
| Indicates whether the Aircraft is subject to Predictor manipulation (default=True)
| current_sector: str, optional
| Indicates which Sector is currently controlling the Aircraft.
| If None, is set to "background"
| random_seed: int or None
| Random seed for sampling from speed distributions
| pilot: Aircraft Pilot, default is a Pilot()
| The Aircraft's Pilot, which processes actions issued to the aircraft
| squawk: str, optional
| Secondary surveillance radar (squawk) code
| wake_vortex: str, optional
| Wake vortex category of the aircraft. Will be set automatically using bada OPF files if None.
| last_passed_filed_idx: int or None
| Index of flight_plan.route.filed which was last passed.
| None indicates no filed fixes have been passed yet.
| last_passed_current_idx: int or None
| Index of flight_plan.route.current which was last passed.
| None indicates no current fixes have been passed yet.
| squawk_ident_until: The unix time that the aircraft squawk idents until.
|
| Attributes
| ----------
| cleared_instructions: Instructions
| The instructions given by the ATCO.
| selected_instructions: Instructions
| The instructions enacted by the Pilot.
| percentile_rank_dict: dict[str, [float, None]], optional
| The percentile rank dictionary of the aircraft. The allowed keys in the dict are: "cas_cr", "cas_cl",
| "cas_des", "rocd_cl" and "rocd_des". The values to these keys are used to assign the aircraft a horizontal
| and vertical speed scores from a probability distribution.
| cleared_fl: float, multiple of 10.
| Last-cleared flight level.
| Upon instantiation this is set to the current flight level.
| Note that if a flight level is given that is not a multiple of 10, it will be rounded to the nearest
| multiple of 10.
| selected_fl: float, multiple of 10.
| Selected flight level (causes the Aircraft to climb/descend).
| Upon instantiation this is set to the current flight level.
| Note that if a flight level is given that is not a multiple of 10, it will be rounded to the nearest
| multiple of 10.
| speed_tas: float or None
| The true airspeed (TAS) of the Aircraft in knots.
| This is only set by the Predictors.
| vertical_speed: float
| The Aircraft vertical speed (climb or descend) in feet/min.
| Upon instantiation this is set to 0.0 (i.e. level flight).
| on_route: bool
| A flag indicating if the aircraft is route following or not
| heading_changing_to: float or None
| A flag indicating whether or not an aircraft needs its heading changing. If None, aircraft is not expected
| to change heading, otherwise it is expected to change to the given heading.
| next_fix_index: int or None
| Index of the next fix in the flight plan route, when aircraft is flying on_route.
| If None, aircraft has passed its last route fix.
| ground_speed: float or None
| The ground speed of the aircraft in knots.
| ground_track_angle: float or None
| The ground track angle of the aircraft in degrees.
| predictor_params: dict[str, Any]
| Dictionary of parameters for use by any of the predictors to store state.
|
| data(self) -> 'dict[str, typing.Any]'
| Create a dictionary with key/value pairs representing the Aircraft data.
|
| Returns
| ----------
| data_dict: Dict[str, Any]
| Data dictionary of Aircraft data
|
| default_pilot(self)
| Create a Pilot instance.
|
| distance(self, other: 'Pos2D | Pos3D') -> 'float'
| Calculate lateral distance [nmi] between Aircraft and another Pos3D location
| (vertical distance is ignored).
|
| Parameters
| ----------
| other: Pos3D
| A latitude, longitude, altitude position.
|
| Returns
| ----------
| float
| Lateral distance in nautical miles between Aircraft and the given 3D position.
|
| distance_to_abeam(self, position: 'Pos2D', radius: 'float' = 20.0) -> 'float | None'
| Calculate distance [nmi] between Aircraft and the point on the ground track angle which is abeam position.
|
| The supplied position is normally a fix location.
| The term 'abeam' means the fix is perpendicular to the aircraft's ground track. So we get a right-angled
| triangle with the hypotenuse being the line from the aircraft to the fix, and the other two sides being the
| path of the aircraft to the abeam point and the line from the abeam point to the fix.
| If the abeam point is behind the aircraft, the distance returned is negative.
| If the current path of the aircraft doesn't pass within the supplied radius of the fix, None is returned.
|
| Parameters
| ----------
| position: Pos2D
| A latitude, longitude position, normally of a fix.
| radius: float, optional
| The maximum distance from the ground track to consider the point abeam the aircraft. Default is 20.0 nmi.
|
| Returns
| ----------
| float | None
| Distance in nautical miles between Aircraft and the point on the current ground track which is abeam the
| supplied position. If the point is behind the aircraft the distance is negative. If the path of the aircraft
| doesn't pass within the radius of the point, None is returned.
|
| ident(self, time: 'float', duration_seconds: 'float' = 12)
| Makes the aircraft's squawk ident.
|
| The aircraft's ident is stored as seconds from epoch until when the aircraft is identing.
|
| Parameters
| ----------
| time: float
| Time from epoch the aircraft begins to ident
| duration_seconds: float (optional)
| How long to ident for. Defaults to 12 seconds.
|
| is_in_or_aproximately_aproaching_sector(
| self,
| sector: 'Sector',
| max_distance: 'int' = 30,
| ds: 'int' = 15
| ) -> 'bool'
| Checks if an aircraft is in a sector, or approaching the sector by checking if the point a specified distance
| ahead of the aircraft is in the sector.
|
| Note this function does not consider turn radius.
|
| Parameters
| ----------
| sector: Sector
| The sector to check if the aircraft is in or approaching.
|
| max_distance: int
| The distance to consider ahead of the aircraft. Defaults to 30.
|
| ds: int
| The step distances to step through. Defaults to 15.
|
| Returns
| -------
| bool
|
| pos2d(self) -> 'Pos2D'
| Get the two-dimensional position of the Aircraft.
|
| Returns
| ----------
| Pos2D
| The latitude, longitude of the Aircraft.
|
| pos3d(self) -> 'Pos3D'
| Get the three-dimensional position of the Aircraft.
|
| Returns
| ----------
| Pos3D
| The latitude, longitude, altitude (flight level) of the Aircraft.
|
| randomise_speed_performance(self, random_seed: 'int | None')
| If the aircraft random seed is not set this method sets it, and
| defines its performance characteristics.
|
| Parameters
| ----------
| random_seed: int
| The random seed to use. If None, the aircraft performance characteristics
| will effectively default to default values
|
| save(self, filename: 'str')
| Write the instance to a file.
|
| Parameters
| ----------
| filename: str
| Path to file.
|
| set_aircraft_type_from_table(self)
| Use data tables to set aircraft type if none is provided.
| Can be overridden by derived classes.
|
| set_attributes(self, attributes: 'dict[str, typing.Any]')
| Set the 'fl', 'heading', and 'controllable' attributes of the Aircraft.
|
| Parameters
| ----------
| attributes: dict
| A dictionary of attributes to set.
|
| set_position(self, lat: 'float', lon: 'float')
| Set the Aircraft position.
|
| Parameters
| ----------
| lat: float
| The Aircraft latitude in degrees.
| lon: float
| The Aircraft longitude in degrees.
|
| set_speed_performance(
| self,
| cas_pr: 'float | None' = None,
| rocd_pr: 'float | None' = None
| )
| A method to set the percentile rank performance of the aircraft
|
| Parameters
| ----------
| cas_pr: float, optional
| The aircraft's calibrated airspeed percentile rank (0 to 100)
| rocd_pr: float, optional
| The aircraft's rate of climb and descent percentile rank (0 to 100)
|
| set_squawk(self, transponder_code: 'int | str')
|
| set_wake_vortex_category(self)
| Derived classes may use data tables to set wake vortex category if none is provided.
| Here we just set a default value of "M"
|
| to_json(self) -> 'str'
| Serialise the instance to JSON string.
|
| Returns
| -------
| str
|
| update_route_status(
| self,
| airspace: 'Airspace',
| distance_threshold_NMI: 'float' = 5.0,
| set_next_fix_index: 'bool' = False
| )
| Update the status of the route progression (route_status)
|
| Check the position of an aircraft relative to fixes on its filed and current flight plan Route, and update the
| respective passed-fix indices.
|
| Parameters
| ----------
| airspace: Airspace
| Airspace in which the Aircraft is flying - route updated based on the closest forward fix method.
| distance_threshold_NMI: float, default=5
| Consider Fix as having been passed if Aircraft is less than distance_threshold from it (NMI).
| set_next_fix_index: bool, default False
| Set the next_fix_index to be aligned with the calculated passed-fix indices. This is False by default
| as setting next_fix_index is usually performed by the predictor. However, at initialisation it can be
| convenient to set the next_fix_index manually here.
|
| Side-Effects
| ------------
| aircraft.last_passed_filed_idx
| Updates the index if the aircraft is within a threshold distance of a fix later in the filed route.
| aircraft.last_passed_current_idx
| Matches the filed index if the filed and the current are the same, otherwise updates with the same logic
| but for the progression along the current flight plan (updated by a route-direct Action).
| aircraft.route_status
| The main intended change by this function is the route_status, which uses the above passed indices.
|
| ----------------------------------------------------------------------
| Class methods defined here:
|
| from_json(s: 'str') -> 'typing_extensions.Self'
| Construct a new instance from a string in JSON format.
|
| Parameters
| ----------
| s: str
| A string representation of Aircraft in a JSON/dictionary structure.
|
| Returns
| --------
| Aircraft
|
| Examples
| --------
| >>> Aircraft.from_json(''' PROBABLY NEEDS UPDATING
| >>> {
| >>> "flight_plan": {
| >>> "entry": "160.0FL 00:00:00",
| >>> "exit": "200.0FL 00:15:00",
| >>> "route": {"current": ["ALFA", "BRAVO", "CHAR"], "filed": ["ALFA", "BRAVO", "CHAR"]},
| >>> },
| >>> "lat": 51.4702,
| >>> "lon": -0.4479,
| >>> "fl": 120.0,
| >>> "heading": 249.68,
| >>> "aircraft_type": None,
| >>> "callsign": "ABC123",=======
| >>> "cleared_instructions": {
| >>> "fl": 120.0,
| >>> "mach": null,
| >>> "cas": null,
| >>> "vertical_speed": 0.0,
| >>> "heading": 249.68,
| >>> "on_route": true,
| >>> "speed_action": null,
| >>> "vertical_speed_action": null,
| >>> "vertical_action": null,
| >>> "lateral_action": null
| >>> },
| >>> "selected_instructions": {
| >>> "fl": 120.0,
| >>> "mach": null,
| >>> "cas": null,
| >>> "vertical_speed": 0.0,
| >>> "heading": 249.68,
| >>> "on_route": true,
| >>> "speed_action": null,
| >>> "vertical_speed_action": null,
| >>> "vertical_action": null,
| >>> "lateral_action": null,
| >>> "last_passed_filed_idx": null,
| >>> "last_passed_current_idx": null
| >>> },
| >>> "coordinations": { # NO LONGER CORRECT
| >>> "25": {
| >>> "entry": {
| >>> "fl": 160,
| >>> "fix": "ABC",
| >>> "direction": "Up",
| >>> "next_sector": null,
| >>> "time": null,
| >>> "level_by": false,
| >>> "level_by_details": null,
| >>> "secondary_coord_conditions": null
| >>> }
| >>> "exit": {
| >>> "fl": 320,
| >>> "fix": "DEF",
| >>> "direction": "Horizontal",
| >>> "next_sector": null,
| >>> "time": null,
| >>> "level_by": false,
| >>> "level_by_details": null,
| >>> "secondary_coord_conditions": null
| >>> }
| >>> }
| >>> },
| >>> }''')
|
| ----------------------------------------------------------------------
| Static methods defined here:
|
| load(filename: 'str') -> 'Aircraft'
| Construct a new instance from a file.
|
| Parameters
| ----------
| filename: str
| Path to a JSON file with an Aircraft definition in a dictionary format.
|
| Returns
| ----------
| Aircraft
|
| ----------------------------------------------------------------------
| Readonly properties defined here:
|
| flight_state
| Returns the aircraft flight state from its flight level and its selected flight level.
|
| Returns
| ----------
| flight_state: FlightState
| The aircraft flight state, either descend, climb or cruise
|
| previous_sector
| The previous sector that the aircraft is controlled by
|
| route_status
| Report the progression status for each fix along the flight plan route.
|
| The status of each fix is reported as:
|
| - passed: these fixes are considered passed, whether via a close approach or from being skipped.
| - next: when on-route one fix is considered next, and indicated by the trajectory prediction.
| - skipping: fixes that were in the filed route, but are planned to be omitted due to a route-direct.
| - to-come (""): future fixes in the current flight plan route, typically following the next fix.
|
| Note that fixes marked "passed" are typically followed by "next", sometimes with intermediate "skipping".
| However, due to interactions between the trajectory prediction and the aircraft's route-progression status,
| there may be "to-come" fixes before the "next" fix.
|
| The underlying indices should be updated by update_route_status in Airspace.
|
| Returns
| -------
| dict[str,str]
| Key-value pairs of each fix along a route with the corresponding status.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| cleared_fl
| Cleared Flight Level of the Aircraft
|
| current_sector
| Current sector that the aircraft is controlled by
|
| on_route
| Whether the aircraft is route following
|
| selected_fl
| Selected Flight Level of the Aircraft
|
| ----------------------------------------------------------------------
| Methods inherited from bluebird_dt.mixin.core_mixin.Comparison:
|
| __eq__(self: ~T, other: ~T) -> bool
| Check if two class instances are equal. This should work on
| any class that has a data() method for serialization (i.e. can
| be expressed as nested structure of dicts and lists).
| However, it may be slow, and it may be preferable for derived
| classes to override this with custom logic if speed is important.
|
| Parameters
| ----------
| other: Any
| Another class instance
|
| Returns
| -------
| bool
|
| __ge__(self, other) from functools
| Return a >= b. Computed by @total_ordering from (not a < b).
|
| __gt__(self: ~T, other: ~T) -> bool
| Check if one class instance is "greater" than another by
| comparing their variables as strings
|
| Parameters
| ----------
| other: Any
| Another class instance
|
| Returns
| -------
| bool
|
| __hash__(self: ~T) -> int
| Compute the hash value of an instance
|
| Returns
| -------
| int
|
| __le__(self, other) from functools
| Return a <= b. Computed by @total_ordering from (a < b) or (a == b).
|
| __lt__(self: ~T, other: ~T) -> bool
| Check if one class instance is "less" than another by
| comparing their variables as strings
|
| Parameters
| ----------
| other: Any
| Another class instance
|
| Returns
| -------
| bool
|
| ----------------------------------------------------------------------
| Data descriptors inherited from bluebird_dt.mixin.core_mixin.Comparison:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
In this notebook, we have taken a small peek under the hood of the digital twin, as well as finding out how to know what scenarios are available.
In the next notebook, Part 3, we will look at yet another scenario type, and how to customize it to meet our needs.