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 from_netcdf_group(group: Group) FieldSet

Construct a FieldSet from a NetCDF group.

classmethod from_registry(name: str) FieldSet

Retrieve a FieldSet from the registry by name.

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 Enum and 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.

location(distance: float) Point

Calculate location at a given distance from start of ground track.

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.