From 1473cbdec9d9a74e13b44eb5b395a237f2579d52 Mon Sep 17 00:00:00 2001 From: samuelh-f Date: Wed, 17 Jun 2026 13:12:14 +0000 Subject: [PATCH 1/2] Add cas to hmi --- bluebird-dt/bluebird_dt/core/aircraft.py | 5 +++++ bluebird-dt/bluebird_dt/events/event_dtypes.py | 3 ++- bluebird-dt/bluebird_dt/events/event_handler.py | 11 ++++++++++- bluebird-dt/bluebird_dt/predictor/linear_predictor.py | 5 +++++ bluebird-dt/bluebird_dt/predictor/simple_predictor.py | 11 +++++++---- bluebird-dt/bluebird_dt/simulator/simulator.py | 1 + bluebird-dt/bluebird_dt/utility/paths.py | 4 ++-- bluebird-dt/tests/unit/core/conftest.py | 1 + bluebird-dt/tests/unit/core/test_aircraft.py | 2 ++ 9 files changed, 35 insertions(+), 8 deletions(-) diff --git a/bluebird-dt/bluebird_dt/core/aircraft.py b/bluebird-dt/bluebird_dt/core/aircraft.py index 24f389f..5a65f57 100644 --- a/bluebird-dt/bluebird_dt/core/aircraft.py +++ b/bluebird-dt/bluebird_dt/core/aircraft.py @@ -291,6 +291,8 @@ def __init__( 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_cas: float or None + The calibrated airspeed (CAS) of the Aircraft in knots. speed_tas: float or None The true airspeed (TAS) of the Aircraft in knots. This is only set by the Predictors. @@ -373,6 +375,7 @@ def __init__( self.selected_instructions = Instructions(fl=use_fl, on_route=True) self.vertical_speed: float = 0.0 + self.speed_cas: float | None = None self.speed_tas: float | None = None self.ground_speed: float | None = None @@ -640,6 +643,7 @@ def from_json(cls, s: str) -> typing_extensions.Self: ) # attributes + aircraft.speed_cas = data.get("speed_cas", None) aircraft.speed_tas = data.get("speed_tas", None) aircraft.ground_speed = data.get("ground_speed", None) aircraft.ground_track_angle = data.get("ground_track_angle", None) @@ -727,6 +731,7 @@ def data(self) -> dict[str, typing.Any]: "squawk": self.squawk, "squawk_ident_until": self.squawk_ident_until, "wake_vortex": self.wake_vortex, + "speed_cas": self.speed_cas, "speed_tas": self.speed_tas, "vertical_speed": self.vertical_speed, "ground_speed": self.ground_speed, diff --git a/bluebird-dt/bluebird_dt/events/event_dtypes.py b/bluebird-dt/bluebird_dt/events/event_dtypes.py index a90b2a1..3306c21 100644 --- a/bluebird-dt/bluebird_dt/events/event_dtypes.py +++ b/bluebird-dt/bluebird_dt/events/event_dtypes.py @@ -6,7 +6,7 @@ class EventDtypes: # specify expected datatypes for each EventHandler dataframe radar_dtypes: ClassVar[dict[str, str]] = { - # ufid, speed_tas, ground_speed, ground_track angle and selected_fl + # ufid, speed_cas, speed_tas, ground_speed, ground_track angle and selected_fl # are all optional columns and values and will be added automatically # if required "datetime": "datetime64[us]", # datetime in utc @@ -16,6 +16,7 @@ class EventDtypes: "fl": "float64", "heading": "float64", "ufid": "category", # optional + "speed_cas": "float64", # optional "speed_tas": "float64", # optional "ground_speed": "float64", # optional "ground_track_angle": "float64", # optional diff --git a/bluebird-dt/bluebird_dt/events/event_handler.py b/bluebird-dt/bluebird_dt/events/event_handler.py index 8a5c709..d18bf74 100644 --- a/bluebird-dt/bluebird_dt/events/event_handler.py +++ b/bluebird-dt/bluebird_dt/events/event_handler.py @@ -8,8 +8,8 @@ import numpy as np import pandas as pd -from pydantic import BaseModel import typing_extensions +from pydantic import BaseModel from typing_extensions import Self from bluebird_dt.core import ( @@ -262,6 +262,7 @@ def add_optional_radar_columns_if_required(self) -> None: columns = self.radar_df.columns optional_float_cols = [ + "speed_cas", "speed_tas", "ground_speed", "ground_track_angle", @@ -401,6 +402,7 @@ def add_aircraft(self, the_datetime: datetime, aircraft: Aircraft): fl=aircraft.fl, heading=aircraft.heading, ufid=aircraft.ufid, + speed_cas=aircraft.speed_cas, speed_tas=aircraft.speed_tas, ground_speed=aircraft.ground_speed, ground_track_angle=aircraft.ground_track_angle, @@ -537,6 +539,7 @@ def add_radar_event( fl: float, heading: float, ufid: str | None = None, + speed_cas: float | None = None, speed_tas: float | None = None, ground_speed: float | None = None, ground_track_angle: float | None = None, @@ -561,6 +564,8 @@ def add_radar_event( Heading of aircraft ufid: str, optional Unique Flight ID of aircraft + speed_cas: float, optional + Calibrated Airspeed of the aircraft speed_tas: float, optional True Airspeed of the aircraft ground_speed: float, optional @@ -577,6 +582,7 @@ def add_radar_event( "fl": fl, "heading": heading, "ufid": ufid, + "speed_cas": speed_cas, "speed_tas": speed_tas, "ground_speed": ground_speed, "ground_track_angle": ground_track_angle, @@ -1637,6 +1643,9 @@ def update_from_radar( # calculate the tas aircraft.speed_tas = tas_from_ground_speed(aircraft.ground_speed, aircraft.ground_track_angle, wind_vector) + if row.speed_cas is not None: + aircraft.speed_cas = row.speed_cas + if row.heading is not None: aircraft.heading = row.heading diff --git a/bluebird-dt/bluebird_dt/predictor/linear_predictor.py b/bluebird-dt/bluebird_dt/predictor/linear_predictor.py index 850d584..4eebb4f 100644 --- a/bluebird-dt/bluebird_dt/predictor/linear_predictor.py +++ b/bluebird-dt/bluebird_dt/predictor/linear_predictor.py @@ -18,6 +18,7 @@ cas_to_tas, mach_cas_trans_altitude, mach_to_tas, + tas_to_cas, ) from bluebird_dt.utility.paths import ( AIRCRAFT_WEIGHT_MAPPING_FILE, @@ -234,6 +235,9 @@ def update_total_speeds(self, aircraft: Aircraft): # Set the aircraft TAS aircraft.speed_tas = tas_KT + # Set the aircraft CAS + aircraft.speed_cas = tas_to_cas(aircraft.fl, aircraft.speed_tas * KT_TO_MPS, self.delta_T) * MPS_TO_KT + def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): """ Updates total speeds of a selected aircraft by writing the selected_instructions.cas value directly to the true @@ -251,6 +255,7 @@ def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): raise ValueError("Aircraft.selected_instructions.cas must be set to use cas_is_tas speed modelling.") aircraft.speed_tas = cas + aircraft.speed_cas = cas @functools.lru_cache def _get_performance_profile(self, key: str | None) -> dict[str, list[float | None] | float]: diff --git a/bluebird-dt/bluebird_dt/predictor/simple_predictor.py b/bluebird-dt/bluebird_dt/predictor/simple_predictor.py index c197ae1..cbc8ec8 100644 --- a/bluebird-dt/bluebird_dt/predictor/simple_predictor.py +++ b/bluebird-dt/bluebird_dt/predictor/simple_predictor.py @@ -149,9 +149,9 @@ def update_speeds(self, aircraft: Aircraft, wind_field: WindField | None): def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): """ Updates total speeds of a selected aircraft by writing the selected_instructions.cas value directly to the true - airspeed (speed_tas) value. Mach values are ignored. This method is called when the flag "use_cas_as_tas" is set - to True. If the selected_instructions.cas value is None, then an error is raised: this must be set to use this - simplified speed modelling. + airspeed (speed_tas) and calibrated airspeed (speed_cas) values. Mach values are ignored. This method is called + when the flag "use_cas_as_tas" is set to True. If the selected_instructions.cas value is None, then an error is + raised: this must be set to use this simplified speed modelling. Parameters ---------- @@ -162,7 +162,10 @@ def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): if aircraft.selected_instructions.cas is None: raise ValueError("Aircraft.selected_instructions.cas must be set to use cas_is_tas speed modelling.") - aircraft.speed_tas = aircraft.selected_instructions.cas + cas = aircraft.selected_instructions.cas + + aircraft.speed_tas = cas + aircraft.speed_cas = cas def update_vertical_speeds(self, aircraft: Aircraft): """ diff --git a/bluebird-dt/bluebird_dt/simulator/simulator.py b/bluebird-dt/bluebird_dt/simulator/simulator.py index 9353f16..f646824 100644 --- a/bluebird-dt/bluebird_dt/simulator/simulator.py +++ b/bluebird-dt/bluebird_dt/simulator/simulator.py @@ -616,6 +616,7 @@ def dynamic_data( "heading": aircraft.heading, "ground_track": aircraft.ground_track_angle, "cleared_heading": aircraft.cleared_instructions.heading, + "calibrated_air_speed": aircraft.speed_cas, "true_air_speed": aircraft.speed_tas, "ground_speed": aircraft.ground_speed, "filed_true_airspeed": filed_true_airspeed, diff --git a/bluebird-dt/bluebird_dt/utility/paths.py b/bluebird-dt/bluebird_dt/utility/paths.py index bd89d79..0dd917e 100644 --- a/bluebird-dt/bluebird_dt/utility/paths.py +++ b/bluebird-dt/bluebird_dt/utility/paths.py @@ -1,7 +1,7 @@ -import importlib.resources import os +from pathlib import Path -ROOT_DIR = importlib.resources.files("bluebird_dt") +ROOT_DIR = Path(__file__).resolve().parent.parent # We have the data in the bluebird_dt package itself. BASE_DATA_DIR = ROOT_DIR.joinpath("scenario_data") LOG_DIR: str = os.path.join(BASE_DATA_DIR, "scenario_logs") diff --git a/bluebird-dt/tests/unit/core/conftest.py b/bluebird-dt/tests/unit/core/conftest.py index ac4b293..283da88 100644 --- a/bluebird-dt/tests/unit/core/conftest.py +++ b/bluebird-dt/tests/unit/core/conftest.py @@ -422,6 +422,7 @@ def make_random_aircraft(callsign: str | None = None) -> Aircraft: squawk_ident_until=squawk_ident_until, ) + aircraft.speed_cas = cleared_cas aircraft.speed_tas = cleared_cas # based on what happens in linear predictor aircraft.vertical_speed = vertical_speed aircraft.heading_changing_to = heading_changing_to diff --git a/bluebird-dt/tests/unit/core/test_aircraft.py b/bluebird-dt/tests/unit/core/test_aircraft.py index 7f90295..b970ac6 100644 --- a/bluebird-dt/tests/unit/core/test_aircraft.py +++ b/bluebird-dt/tests/unit/core/test_aircraft.py @@ -88,6 +88,7 @@ def test_data(): assert data["cleared_instructions"]["cas"] is None assert data["cleared_instructions"]["mach"] is None assert data["cleared_instructions"]["vertical_speed"] is None + assert data["speed_cas"] is None assert data["speed_tas"] is None assert data["vertical_speed"] == pytest.approx(0.0) assert data["ground_speed"] is None @@ -173,6 +174,7 @@ def test_data(): assert data["selected_instructions"]["mach"] == cleared_mach assert data["selected_instructions"]["vertical_speed"] == cleared_vertical_speed assert data["selected_instructions"]["on_route"] == on_route + assert data["speed_cas"] is None assert data["speed_tas"] is None assert data["vertical_speed"] == vertical_speed assert data["ground_speed"] is None From 755ccfc3f4e2a769cda17e457a4bc64b4b30c484 Mon Sep 17 00:00:00 2001 From: samuelh-f Date: Mon, 22 Jun 2026 14:59:46 +0000 Subject: [PATCH 2/2] Make cas read-only --- bluebird-dt/bluebird_dt/core/aircraft.py | 9 +++++++-- bluebird-dt/bluebird_dt/events/event_handler.py | 2 +- bluebird-dt/bluebird_dt/predictor/linear_predictor.py | 4 ++-- bluebird-dt/bluebird_dt/predictor/simple_predictor.py | 2 +- bluebird-dt/tests/unit/core/conftest.py | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/bluebird-dt/bluebird_dt/core/aircraft.py b/bluebird-dt/bluebird_dt/core/aircraft.py index 5a65f57..839a715 100644 --- a/bluebird-dt/bluebird_dt/core/aircraft.py +++ b/bluebird-dt/bluebird_dt/core/aircraft.py @@ -375,7 +375,7 @@ def __init__( self.selected_instructions = Instructions(fl=use_fl, on_route=True) self.vertical_speed: float = 0.0 - self.speed_cas: float | None = None + self._speed_cas: float | None = None self.speed_tas: float | None = None self.ground_speed: float | None = None @@ -643,7 +643,7 @@ def from_json(cls, s: str) -> typing_extensions.Self: ) # attributes - aircraft.speed_cas = data.get("speed_cas", None) + aircraft._speed_cas = data.get("speed_cas", None) aircraft.speed_tas = data.get("speed_tas", None) aircraft.ground_speed = data.get("ground_speed", None) aircraft.ground_track_angle = data.get("ground_track_angle", None) @@ -959,6 +959,11 @@ def set_performance(self, cas_pr: float | None = None, rocd_pr: float | None = N for key in ["rocd_des", "rocd_cl"]: self.percentile_rank_dict[key] = None + @property + def speed_cas(self) -> float | None: + """The calibrated airspeed of the aircraft, set by the predictors or livestream""" + return self._speed_cas + @property def current_sector(self) -> str: """Current sector that the aircraft is controlled by""" diff --git a/bluebird-dt/bluebird_dt/events/event_handler.py b/bluebird-dt/bluebird_dt/events/event_handler.py index d18bf74..b4ca232 100644 --- a/bluebird-dt/bluebird_dt/events/event_handler.py +++ b/bluebird-dt/bluebird_dt/events/event_handler.py @@ -1644,7 +1644,7 @@ def update_from_radar( aircraft.speed_tas = tas_from_ground_speed(aircraft.ground_speed, aircraft.ground_track_angle, wind_vector) if row.speed_cas is not None: - aircraft.speed_cas = row.speed_cas + aircraft._speed_cas = row.speed_cas if row.heading is not None: aircraft.heading = row.heading diff --git a/bluebird-dt/bluebird_dt/predictor/linear_predictor.py b/bluebird-dt/bluebird_dt/predictor/linear_predictor.py index 4eebb4f..fbd3c57 100644 --- a/bluebird-dt/bluebird_dt/predictor/linear_predictor.py +++ b/bluebird-dt/bluebird_dt/predictor/linear_predictor.py @@ -236,7 +236,7 @@ def update_total_speeds(self, aircraft: Aircraft): aircraft.speed_tas = tas_KT # Set the aircraft CAS - aircraft.speed_cas = tas_to_cas(aircraft.fl, aircraft.speed_tas * KT_TO_MPS, self.delta_T) * MPS_TO_KT + aircraft._speed_cas = tas_to_cas(aircraft.fl, aircraft.speed_tas * KT_TO_MPS, self.delta_T) * MPS_TO_KT def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): """ @@ -255,7 +255,7 @@ def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): raise ValueError("Aircraft.selected_instructions.cas must be set to use cas_is_tas speed modelling.") aircraft.speed_tas = cas - aircraft.speed_cas = cas + aircraft._speed_cas = cas @functools.lru_cache def _get_performance_profile(self, key: str | None) -> dict[str, list[float | None] | float]: diff --git a/bluebird-dt/bluebird_dt/predictor/simple_predictor.py b/bluebird-dt/bluebird_dt/predictor/simple_predictor.py index cbc8ec8..05fa8fe 100644 --- a/bluebird-dt/bluebird_dt/predictor/simple_predictor.py +++ b/bluebird-dt/bluebird_dt/predictor/simple_predictor.py @@ -165,7 +165,7 @@ def update_total_speeds_cas_is_tas(self, aircraft: Aircraft): cas = aircraft.selected_instructions.cas aircraft.speed_tas = cas - aircraft.speed_cas = cas + aircraft._speed_cas = cas def update_vertical_speeds(self, aircraft: Aircraft): """ diff --git a/bluebird-dt/tests/unit/core/conftest.py b/bluebird-dt/tests/unit/core/conftest.py index 283da88..8a4ef2c 100644 --- a/bluebird-dt/tests/unit/core/conftest.py +++ b/bluebird-dt/tests/unit/core/conftest.py @@ -422,7 +422,7 @@ def make_random_aircraft(callsign: str | None = None) -> Aircraft: squawk_ident_until=squawk_ident_until, ) - aircraft.speed_cas = cleared_cas + aircraft._speed_cas = cleared_cas aircraft.speed_tas = cleared_cas # based on what happens in linear predictor aircraft.vertical_speed = vertical_speed aircraft.heading_changing_to = heading_changing_to