Trajectory data
The Trajectory class is a data container that stores both per-point data for
a trajectory and per-trajectory metadata values.
The data stored in Trajectory values is defined by a list of “field sets”
using the FieldSet and FieldMetadata classes. All trajectories include the
“base” field set (defined in the BASE_FIELDS value in the
AEIC.trajectories.trajectory module). Trajectories may contain other field
sets in addition to the base field set. Additional field sets are added to a
trajectory using the add_fields method.
The Trajectory class has a flexible system for saving and accessing
trajectory data: attribute access on a Trajectory value is managed with
reference to the field sets included in the trajectory. This means that any
field in the base field set can be accessed directly for a trajectory, as, for
example, traj.rate_of_climb or traj.n_cruise. The first of these is a
per-point Numpy array value, while the second is an integer metadata value.
This approach is designed to work cleanly with the TrajectoryStore class for
saving trajectory data to NetCDF files.
Note
Currently trajectories have to be created with a fixed number of points. This matches the way the legacy trajectory builder works, but I will extend the class to allow incremental construction of trajectories once that’s needed.
- AEIC.trajectories.trajectory.BASE_FIELDS = <FieldSet base: ['fuel_flow', 'aircraft_mass', 'fuel_mass', 'ground_distance', 'altitude', 'flight_level', 'rate_of_climb', 'flight_time', 'latitude', 'longitude', 'azimuth', 'heading', 'true_airspeed', 'ground_speed', 'flight_level_weight', 'starting_mass', 'total_fuel_mass', 'n_idle_origin', 'n_taxi_origin', 'n_takeoff', 'n_climb', 'n_cruise', 'n_descent', 'n_approach', 'n_taxi_destination', 'n_idle_destination', 'flight_id', 'name']>
Base field set included in every trajectory.
- class AEIC.trajectories.trajectory.Trajectory(npoints: int, name: str | None = None, fieldsets: list[str] | None = None)
Class representing a 1-D trajectory with various data fields and metadata.
The “various fields” include a base set of trajectory fields, one value per trajectory point and a base set of metadata fields, one value per trajectory, plus optional additional per-point or metadata fields added by adding field sets to the trajectory.
- __getattr__(name: str) ndarray[tuple[int], Any] | Any
Override attribute retrieval to access data and metadata fields.
- __hash__()
The hash of a trajectory is based on its data dictionary.
- __init__(npoints: int, name: str | None = None, fieldsets: list[str] | None = None)
Initialized with a fixed number of points and an optional name.
The name is used for labelling trajectories within trajectory sets (and NetCDF files).
All per-point data and per-trajectory metadata fields included in every trajectory by default are taken from the BASE_FIELDS dictionary above. Other per-point and metadata fields may be added using the add_fields method.
- __len__()
The total number of points in the trajectory.
- __setattr__(name: str, value: Any)
Override attribute setting to handle data and metadata fields.
The type and length of assigned values are checked to ensure consistency with the trajectory’s data dictionary. The type checking rules used here are the same as those used by NumPy’s np.can_cast with casting=’same_kind’.
NOTE: These type checking rules mean that assigning a value of type int to a field of type np.int32 will work, but may result in loss of information if the integer value is too large to fit in an np.int32. Caveat emptor!
- __weakref__
list of weak references to the object
- add_fields(fieldset: FieldSet | HasFieldSets)
Add fields from a FieldSet to the trajectory.
Either just add fields with empty values, or, if the field set is attached to a value object using the HasFieldSet protocol, try to initialize data values too.
- copy_point(from_idx: int, to_idx: int)
Copy data from one point to another within the trajectory.
- property nbytes: int
Calculate approximate memory size of the trajectory in bytes.
(This only needs to be approximate because it’s just used for sizing the TrajectoryStore LRU cache.)
Field sets
Each field in a field set is represented by a FieldMetadata value, which
records the type of each field and whether it is a per-point data field
(metadata = False) or a per-trajectory metadata field (metadata = True).
Additional fields provide NetCDF metadata for the field description and units,
and allow fields to be marked as required, or to be provided with a default
value.
A field set is a collection of FieldMetadata records, keyed by the field
name. Field sets are stored by name in a registry, and use an MD5-based hash
to ensure that named field sets from different sources correspond to the same
sets of fields.
Note
We probably need a mechanism for versioning field sets. At the moment, adding a new field to the base field set, for example, will invalidate files that were created before the new field was added. That’s not sustainable, so we need to implement some sort of simple version control for these things.
- class AEIC.trajectories.field_sets.FieldMetadata(metadata: bool = False, field_type: type = <class 'numpy.float64'>, description: str = '', units: str = '', required: bool = True, default: ~typing.Any | None = None)
Metadata for a single field.
This is intended to describe the properties of a field in a dataset that may be serialized to NetCDF. Fields can either be per-data-point variables (metadata=False) or per-trajectory metadata (metadata=True).
- default: Any | None = None
Default value for the field if not present in the dataset.
- description: str = ''
Human-readable description of the field (used for the NetCDF “description” attribute).
- field_type
Type of the field; should be a Numpy dtype or str for variable-length strings. Note that Python int and float are not allowed because we need to have a 1-to-1 mapping from Python to Numpy types.
alias of
float64
- metadata: bool = False
Is this a metadata field (one value per trajectory) or a data variable (one value per point along a trajectory)?
- required: bool = True
Is this field required to be present in the dataset?
- units: str = ''
Units of the field (used for the NetCDF “units” attribute).
- class AEIC.trajectories.field_sets.FieldSet(fieldset_name: str, *, registered: bool = True, **fields: FieldMetadata)
A collection of field definitions.
Represented as a mapping from field name to metadata.
- REGISTRY: dict[str, FieldSet] = {'base': <FieldSet base: ['fuel_flow', 'aircraft_mass', 'fuel_mass', 'ground_distance', 'altitude', 'flight_level', 'rate_of_climb', 'flight_time', 'latitude', 'longitude', 'azimuth', 'heading', 'true_airspeed', 'ground_speed', 'flight_level_weight', 'starting_mass', 'total_fuel_mass', 'n_idle_origin', 'n_taxi_origin', 'n_takeoff', 'n_climb', 'n_cruise', 'n_descent', 'n_approach', 'n_taxi_destination', 'n_idle_destination', 'flight_id', 'name']>}
Registry of named field sets for reuse.
- static calc_hash(name: str, fields: dict[str, FieldMetadata]) int
Calculate a hash from a FieldSet name and field data.
- property digest
Generate persistent hash for FieldSet.
This MD5-based hash is used for identifying field sets within NetCDF files and is used to check the integrity of the link between associated NetCDF files and base trajectory NetCDF files in the TrajectoryStore class.
- property fields
Return an immutable view of the field definitions.
- classmethod known(name: str) bool
Check if a FieldSet with the given name exists in the registry.
- merge(other)
Combine field sets, ensuring unique field names.
Flight phases
Trajectories are divided into a sequence of “phases” (e.g., climb, cruise, descent). Each phase has a corresponding field in the Trajectory class that records the number of points in that phase for each trajectory.
Some phases are expected to be simulated by all performance models (climb, cruise, descent) while others are optional (taxi, takeoff, approach, idle) and may only be simulated by more detailed models or may be populated from time-in-mode values. For uniformity of representation, these optional phases are included as “normal” points in the trajectory.
Each Trajectory records the number of points in each phase using fields with names prefixed by n_ (e.g., n_climb, n_cruise, n_descent).
- enum AEIC.trajectories.phase.FlightPhase(value)
Flight phases known to AEIC.
Valid values are as follows:
- IDLE_ORIGIN = <FlightPhase.IDLE_ORIGIN: 1>
- TAXI_ORIGIN = <FlightPhase.TAXI_ORIGIN: 2>
- TAKEOFF = <FlightPhase.TAKEOFF: 3>
- CLIMB = <FlightPhase.CLIMB: 4>
- CRUISE = <FlightPhase.CRUISE: 5>
- DESCENT = <FlightPhase.DESCENT: 6>
- APPROACH = <FlightPhase.APPROACH: 7>
- TAXI_DESTINATION = <FlightPhase.TAXI_DESTINATION: 8>
- IDLE_DESTINATION = <FlightPhase.IDLE_DESTINATION: 9>
The
Enumand its members also have the following methods:- property field_name
Trajectory point count field name for this flight phase.
- property method_name
Method name used in trajectory builders for this flight phase.
- property field_label
Human-readable label for this flight phase.
- classmethod from_field_name(field_name: str)
Parse trajectory point count field name to get flight phase.
- AEIC.trajectories.phase.FlightPhases
Type alias for trajectory point counts in each flight phase.
alias of
dict[FlightPhase,int]
- AEIC.trajectories.phase.PHASE_FIELDS = {'n_approach': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in approach phase', units='count', required=False, default=0), 'n_climb': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in climb phase', units='count', required=True, default=0), 'n_cruise': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in cruise phase', units='count', required=True, default=0), 'n_descent': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in descent phase', units='count', required=True, default=0), 'n_idle_destination': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in idle at destination phase', units='count', required=False, default=0), 'n_idle_origin': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in idle at origin phase', units='count', required=False, default=0), 'n_takeoff': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in takeoff phase', units='count', required=False, default=0), 'n_taxi_destination': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in taxi at destination phase', units='count', required=False, default=0), 'n_taxi_origin': FieldMetadata(metadata=True, field_type=<class 'numpy.int32'>, description='Number of points in taxi at origin phase', units='count', required=False, default=0)}
Convenience dictionary of field metadata for all flight phases.
This is used in the “base” field set for trajectory datasets to add the phase point count metadata variables to every trajectory.
- AEIC.trajectories.phase.REQUIRED_PHASES = {FlightPhase.CLIMB, FlightPhase.CRUISE, FlightPhase.DESCENT}
Flight phases we expect all performance models to simulate.
Ground tracks
Warning
Ground tracks currently only really work for great circle routes between an origin and a destination. Trajectory builders that need more complex ground tracks with intermediate waypoints will need to take account of the exceptions that GroundTrack.step raises when an attempt is made to step past a waypoint. There should probably be an option on creation of a GroundTrack to determine whether these exceptions are generated: for a “dense” ground track, maybe we don’t care about stepping past waypoints; for a “flight plan” ground track defined by a sequence of navigation aid positions, maybe we would like our trajectory to hit each waypoint exactly, so we do need to care about the “stepped over a waypoint” exceptions.
Ground tracks are used to represent the path of an aircraft over the Earth’s surface, defined by a series of waypoints. They support interpolation along great circle paths between waypoints. Trajectory builders can use ground tracks to determine the aircraft’s position as they simulate flight along its route.
- class AEIC.trajectories.ground_track.GroundTrack(waypoints: list[Location])
Great circle interpolator along a set of waypoints.
A GroundTrack defines a path in longitude/latitude by an ordered set of waypoints. Between waypoints the ground track follows great circle paths.
This abstraction supports both great circle paths (one start point, one end point, no intermediate waypoints) and paths with multiple waypoints (for example, paths defined by ADS-B data).
The fundamental operation on a GroundTrack is to find the location of a point based on its distance from the ground track’s start location.
- exception Exception
Exception raised for errors in the GroundTrack class.
- class Point(location: Location, azimuth: float)
A point along the ground track, defined by a location and azimuth.
- classmethod great_circle(start_loc: Location, end_loc: Location) Self
Create a great circle ground track from a start and end location.
- lookup_waypoint(distance: float) int
Find index of waypoint immediately after or at given distance.
- step(from_distance: float, distance_step: float) Point
Calculate location a given distance step from a starting distance.
This is a convenience method that calls location with the sum of from_distance and distance_step.
- property total_distance: float
Get total distance of ground track.
- waypoint_distance(idx: int) float
Get cumulative distance to waypoint at given index.