feder 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- feder-0.2.2/.gitignore +132 -0
- feder-0.2.2/PKG-INFO +6 -0
- feder-0.2.2/README.md +0 -0
- feder-0.2.2/pyproject.toml +17 -0
- feder-0.2.2/src/feder/__init__.py +124 -0
- feder-0.2.2/src/feder/available.py +87 -0
- feder-0.2.2/src/feder/common/__init__.py +13 -0
- feder-0.2.2/src/feder/common/db.py +424 -0
- feder-0.2.2/src/feder/common/models.py +190 -0
- feder-0.2.2/src/feder/common/py.typed +0 -0
- feder-0.2.2/src/feder/common/utils.py +68 -0
- feder-0.2.2/src/feder/common/version.py +23 -0
- feder-0.2.2/src/feder/query.py +244 -0
- feder-0.2.2/src/feder/tutorial.py +324 -0
feder-0.2.2/.gitignore
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
# IPython
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
# pyenv
|
|
85
|
+
.python-version
|
|
86
|
+
|
|
87
|
+
# pipenv
|
|
88
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
89
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
90
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
91
|
+
# install all needed dependencies.
|
|
92
|
+
#Pipfile.lock
|
|
93
|
+
|
|
94
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
95
|
+
__pypackages__/
|
|
96
|
+
|
|
97
|
+
# Celery stuff
|
|
98
|
+
celerybeat-schedule
|
|
99
|
+
celerybeat.pid
|
|
100
|
+
|
|
101
|
+
# SageMath parsed files
|
|
102
|
+
*.sage.py
|
|
103
|
+
|
|
104
|
+
# Environments
|
|
105
|
+
.env
|
|
106
|
+
.venv
|
|
107
|
+
env/
|
|
108
|
+
venv/
|
|
109
|
+
ENV/
|
|
110
|
+
env.bak/
|
|
111
|
+
venv.bak/
|
|
112
|
+
|
|
113
|
+
# Spyder project settings
|
|
114
|
+
.spyderproject
|
|
115
|
+
.spyproject
|
|
116
|
+
|
|
117
|
+
# Rope project settings
|
|
118
|
+
.ropeproject
|
|
119
|
+
|
|
120
|
+
# mkdocs documentation
|
|
121
|
+
/site
|
|
122
|
+
|
|
123
|
+
# mypy
|
|
124
|
+
.mypy_cache/
|
|
125
|
+
.dmypy.json
|
|
126
|
+
dmypy.json
|
|
127
|
+
|
|
128
|
+
# Pyre type checker
|
|
129
|
+
.pyre/
|
|
130
|
+
|
|
131
|
+
/.dir-locals.el
|
|
132
|
+
/config*.toml
|
feder-0.2.2/PKG-INFO
ADDED
feder-0.2.2/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "feder"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Feder client API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Ian Ross", email = "iross@mit.edu" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["src/feder"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""# Feder flight data collection system
|
|
2
|
+
|
|
3
|
+
Feder is a system for collecting flight data from a range of sources into a
|
|
4
|
+
consolidated database for applications that need easy access to historical
|
|
5
|
+
flight data for statistical or flight attribution purposes. It comes in two
|
|
6
|
+
main parts:
|
|
7
|
+
|
|
8
|
+
1. An API (published as a Python package) for querying the collected data.
|
|
9
|
+
This data is stored in SQLite3 database files, and the API provides a
|
|
10
|
+
simple interface for making efficient queries to these database files.
|
|
11
|
+
|
|
12
|
+
2. A set of server processes that collaborate to collect and integrate data
|
|
13
|
+
from different flight data sources, plus infrastructure to deploy these
|
|
14
|
+
server processes so that they work correctly together.
|
|
15
|
+
|
|
16
|
+
**The name**: *Feder* is the German word for "feather" and is also supposed to
|
|
17
|
+
give the feeling of **F**light **D**ata **R**ecorder. It's short and unique
|
|
18
|
+
and easy to say ("fay-der", more or less).
|
|
19
|
+
|
|
20
|
+
## Concepts: positions vs. trajectories
|
|
21
|
+
|
|
22
|
+
Flight data is usually provided by data sources like FlightAware's Firehose or
|
|
23
|
+
the OpenSky network as individual flight *state vectors*, encoding a single
|
|
24
|
+
snapshot in time of a flight's position. These state vectors are not *exactly*
|
|
25
|
+
individual ADS-B position fixes, because most data provides filter the raw
|
|
26
|
+
ADS-B fixes to reduce data volumes, but they do represent individual position
|
|
27
|
+
fixes at a single point in time. For most applications, it is more useful to
|
|
28
|
+
provide access to *flight trajectories*, i.e. all the position fixes for a
|
|
29
|
+
given flight (possibly restricted to a selected temporal or spatial domain).
|
|
30
|
+
|
|
31
|
+
The Feder API works in terms of these trajectories (or parts of trajectories),
|
|
32
|
+
and the server processes generate trajectory records for full flights by
|
|
33
|
+
collecting position fixes until it appears that a flight is complete and then
|
|
34
|
+
saving a full trajectory record for the flight. As well as being a more natural
|
|
35
|
+
way to think about this data for most applications, this also makes storing and
|
|
36
|
+
querying the flight data much more efficient than a solution that deals only
|
|
37
|
+
with individual position fixes. Applications that need to work with exactly
|
|
38
|
+
those waypoints in a trajectory that lie within a given temporal or spatial
|
|
39
|
+
range may use the waypoint filtering option of the `feder.FlightQuery` class.
|
|
40
|
+
|
|
41
|
+
## Data units
|
|
42
|
+
|
|
43
|
+
All data values are reported from Feder exactly as they come from the flight
|
|
44
|
+
data providers. [Experiments have
|
|
45
|
+
shown](https://github.mit.edu/iross/flight-data-project/blob/main/experiments/flight-data-comparison/flight-data-comparison.pdf)
|
|
46
|
+
that the different data providers (at least FlightAware and the Contrails API)
|
|
47
|
+
provide comparable data, in the sense that the data is exactly what is sent by
|
|
48
|
+
the aircraft's ADS-B transponder. (FlightAware does some data cleaning using
|
|
49
|
+
other sources, but they make the result look as though it came from the ADS-B
|
|
50
|
+
transponder, just with any anomalous values being cleaned up.)
|
|
51
|
+
|
|
52
|
+
For the user of data from Feder, what this means is:
|
|
53
|
+
|
|
54
|
+
- Latitude and longitude values are "normal" WGS-84 latitudes and longitudes;
|
|
55
|
+
|
|
56
|
+
- The `alt` field of `feder.Point` contains uncorrected [pressure
|
|
57
|
+
altitude](https://en.wikipedia.org/wiki/Pressure_altitude) in feet
|
|
58
|
+
(relative to 1013 hPa) as reported in ADS-B messages;
|
|
59
|
+
|
|
60
|
+
- The `alt_gnss` field of `feder.Point` contains GNSS height in feet relative
|
|
61
|
+
to the WGS-84 datum.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## API quickstart
|
|
65
|
+
|
|
66
|
+
There is a detailed tutorial for the API [here](feder/tutorial.html), but to
|
|
67
|
+
get started quickly and check that things are working, you can do this in the
|
|
68
|
+
shell:
|
|
69
|
+
|
|
70
|
+
``` shell
|
|
71
|
+
pip install --extra-index-url https://www.mit.edu/~iross/pypi feder
|
|
72
|
+
export FEDER_DATA_DIR=/home/mcast/data/feder
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
and then this in Python:
|
|
76
|
+
|
|
77
|
+
``` python
|
|
78
|
+
from datetime import datetime, timedelta
|
|
79
|
+
from feder import FlightQuery
|
|
80
|
+
t1 = datetime(2025, 5, 22, 20, 0)
|
|
81
|
+
t2 = t1 + timedelta(minutes=30)
|
|
82
|
+
flights = list(FlightQuery(t1, t2).with_orig('KBOS').run())
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This will return a Python array of 10
|
|
87
|
+
[`Trajectory`](doc/api-reference.md#Trajectory) objects of flights crossing
|
|
88
|
+
the given latitude/longitude bounding box in the one hour window starting at
|
|
89
|
+
the given time (all times in UTC).
|
|
90
|
+
|
|
91
|
+
The API efficiently supports a range of query options and data return formats.
|
|
92
|
+
See the [tutorial](feder/tutorial.html) or the API reference documentation
|
|
93
|
+
below for details.
|
|
94
|
+
|
|
95
|
+
## API documentation
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
from .query import get_flights, FlightQuery # noqa
|
|
100
|
+
from .available import available_days, available_times, available_sources # noqa
|
|
101
|
+
from .common.models import DataSource, Point, Trajectory # noqa
|
|
102
|
+
from .common.db import BoundingBox, TemporalQueryType, SpatialQueryType # noqa
|
|
103
|
+
from .common.version import get_feder_version # noqa
|
|
104
|
+
|
|
105
|
+
# The following is needed just to build the documentation.
|
|
106
|
+
import feder.tutorial # noqa
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
'get_flights',
|
|
110
|
+
'FlightQuery',
|
|
111
|
+
'available_days',
|
|
112
|
+
'available_times',
|
|
113
|
+
'available_sources',
|
|
114
|
+
'DataSource',
|
|
115
|
+
'Point',
|
|
116
|
+
'Trajectory',
|
|
117
|
+
'BoundingBox',
|
|
118
|
+
'TemporalQueryType',
|
|
119
|
+
'SpatialQueryType',
|
|
120
|
+
'get_feder_version',
|
|
121
|
+
'tutorial'
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
__version__ = '0.2.2'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from datetime import date, datetime, timezone
|
|
2
|
+
import glob
|
|
3
|
+
import itertools
|
|
4
|
+
from operator import itemgetter
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from feder.common import DB, DataSource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def available_days() -> list[tuple[date, date]]:
|
|
11
|
+
"""Get a list of available days in the data directory."""
|
|
12
|
+
data_dir = os.environ.get('FEDER_DATA_DIR')
|
|
13
|
+
if data_dir is None:
|
|
14
|
+
raise ValueError('environment variable FEDER_DATA_DIR must be set')
|
|
15
|
+
|
|
16
|
+
days = sorted([
|
|
17
|
+
datetime.strptime(os.path.basename(p)[:8], '%Y-%j').date().toordinal()
|
|
18
|
+
for p in glob.iglob(os.path.join(data_dir, '????/*.sqlite'))
|
|
19
|
+
])
|
|
20
|
+
ranges = []
|
|
21
|
+
for _, g in itertools.groupby(enumerate(days), lambda x: x[0] - x[1]):
|
|
22
|
+
g = list(g)
|
|
23
|
+
ranges.append((date.fromordinal(g[0][1]), date.fromordinal(g[-1][1])))
|
|
24
|
+
return ranges
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def available_sources(day: date) -> set[DataSource]:
|
|
28
|
+
"""Return data sources in use on a given day."""
|
|
29
|
+
data_dir = os.environ.get('FEDER_DATA_DIR')
|
|
30
|
+
if data_dir is None:
|
|
31
|
+
raise ValueError('environment variable FEDER_DATA_DIR must be set')
|
|
32
|
+
|
|
33
|
+
# Get all data sources from the database.
|
|
34
|
+
return DB(data_dir, day).data_sources()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def available_times(day: date) -> list[tuple[datetime, datetime]]:
|
|
38
|
+
"""Get a list of available times for a given day."""
|
|
39
|
+
data_dir = os.environ.get('FEDER_DATA_DIR')
|
|
40
|
+
if data_dir is None:
|
|
41
|
+
raise ValueError('environment variable FEDER_DATA_DIR must be set')
|
|
42
|
+
|
|
43
|
+
# Get all min, max timestamps pairs from the database.
|
|
44
|
+
try:
|
|
45
|
+
timestamp_ranges = DB(data_dir, day).timestamp_ranges()
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
# Union the timestamp ranges.
|
|
50
|
+
minval = int(datetime.combine(day, datetime.min.time(), tzinfo=timezone.utc).timestamp())
|
|
51
|
+
maxval = int(datetime.combine(day, datetime.max.time(), tzinfo=timezone.utc).timestamp())
|
|
52
|
+
merged = union_of_ranges([clamp_range(r, minval, maxval) for r in timestamp_ranges])
|
|
53
|
+
|
|
54
|
+
# Convert to datetime values and clip to the given day.
|
|
55
|
+
return [
|
|
56
|
+
(datetime.fromtimestamp(r[0], tz=timezone.utc),
|
|
57
|
+
datetime.fromtimestamp(r[1], tz=timezone.utc))
|
|
58
|
+
for r in merged
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def clamp_range(r: tuple[int, int], minval: int, maxval: int) -> tuple[int, int]:
|
|
63
|
+
"""Clamp a range to the given min and max values."""
|
|
64
|
+
return (min(maxval, max(minval, r[0])), min(maxval, max(minval, r[1])))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def union_of_ranges(inp: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
68
|
+
"""Union a list of timestamp ranges."""
|
|
69
|
+
if len(inp) == 0:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
# Sort ranges by start timestamp.
|
|
73
|
+
inp.sort(key=itemgetter(0))
|
|
74
|
+
|
|
75
|
+
# Merge overlapping or contiguous ranges.
|
|
76
|
+
merged = []
|
|
77
|
+
current_start, current_end = inp[0]
|
|
78
|
+
|
|
79
|
+
for start, end in inp[1:]:
|
|
80
|
+
if start <= current_end: # Overlapping or contiguous
|
|
81
|
+
current_end = max(current_end, end)
|
|
82
|
+
else:
|
|
83
|
+
merged.append((current_start, current_end))
|
|
84
|
+
current_start, current_end = start, end
|
|
85
|
+
|
|
86
|
+
merged.append((current_start, current_end))
|
|
87
|
+
return merged
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Code shared between the Feder public API and backend.
|
|
2
|
+
|
|
3
|
+
The parts of this module relevant to the public API are:
|
|
4
|
+
|
|
5
|
+
- `feder_common.models`: Model classes for trajectories and trajectory points.
|
|
6
|
+
- `feder_common.db`: Database access code.
|
|
7
|
+
"""
|
|
8
|
+
from .models import * # noqa
|
|
9
|
+
from .db import * # noqa
|
|
10
|
+
from .utils import * # noqa
|
|
11
|
+
from .version import * # noqa
|
|
12
|
+
|
|
13
|
+
__version__ = '0.2.2'
|