Skip to content

Data

data

Downloaders and preprocessers for third-party data.

adsblol

adsb.lol Mode-S/ADS-B snapshots and aircraft data links.

Requires extras:

  • networking downloading metadata
  • platformdirs storing metadata to default cache

Requires:

  • curl for downloading datalinks
  • zstd for datalink decompression

logger

logger = getLogger(__name__)

DEFAULT_GH_API_BASE_URL

DEFAULT_GH_API_BASE_URL = 'https://api.github.com'

MAX_RELEASES_PER_PAGE

MAX_RELEASES_PER_PAGE = 100

fp_modes_adsb_jsonl

fp_modes_adsb_jsonl(base_dir: Path) -> Path
Source code in src/aerocore/data/adsblol.py
47
48
def fp_modes_adsb_jsonl(base_dir: Path) -> Path:
    return base_dir / "adsblol_modes_adsb.jsonl"
fp_datalinks_jsonl(base_dir: Path) -> Path
Source code in src/aerocore/data/adsblol.py
51
52
def fp_datalinks_jsonl(base_dir: Path) -> Path:
    return base_dir / "adsblol_datalinks.jsonl"

AssetKey

AssetKey(repository: str, asset_id: int)
repository
repository: str
asset_id
asset_id: int

GitHubReleaseAsset

Bases: TypedDict

id
id: int
name
name: str
size
size: int
digest
digest: str | None
browser_download_url
browser_download_url: str

GitHubRelease

Bases: TypedDict

tag_name
tag_name: str
assets

ReleasePage

Bases: TypedDict

repository
repository: str
page
page: int
per_page
per_page: int
releases
releases: list[GitHubRelease]

AdsblolAsset

Bases: TypedDict

repository
repository: str
release_tag
release_tag: str
asset_id
asset_id: int
asset_name
asset_name: str
asset_size
asset_size: int
asset_digest
asset_digest: str | None
browser_download_url
browser_download_url: str

RepositoryMetadataSyncResult

RepositoryMetadataSyncResult(repository: str, pages_fetched: int, assets_seen: int, assets_appended: int, stopped_on_known_page: bool, hit_page_limit: bool)
repository
repository: str
pages_fetched
pages_fetched: int
assets_seen
assets_seen: int
assets_appended
assets_appended: int
stopped_on_known_page
stopped_on_known_page: bool
hit_page_limit
hit_page_limit: bool

MetadataFileSyncResult

MetadataFileSyncResult(path: Path, existing_assets: int, appended_assets: int, repositories: tuple[RepositoryMetadataSyncResult, ...])
path
path: Path
existing_assets
existing_assets: int
appended_assets
appended_assets: int
repositories
repositories: tuple[RepositoryMetadataSyncResult, ...]

MetadataSyncResult

MetadataSyncResult(files: tuple[MetadataFileSyncResult, ...])
files

get_github_auth_token

get_github_auth_token() -> str | None
Source code in src/aerocore/data/adsblol.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def get_github_auth_token() -> str | None:
    for env_name in ("GH_TOKEN", "GITHUB_TOKEN"):
        token = os.environ.get(env_name)
        if token is not None and token.strip():
            return token.strip()

    try:
        result = subprocess.run(
            ["gh", "auth", "token"],
            check=True,
            capture_output=True,
            text=True,
        )
    except (FileNotFoundError, subprocess.CalledProcessError):
        return None

    token = result.stdout.strip()
    return token or None

github_headers

github_headers(auth_token: str | None) -> dict[str, str]
Source code in src/aerocore/data/adsblol.py
137
138
139
140
141
142
143
144
def github_headers(auth_token: str | None) -> dict[str, str]:
    headers = {
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    if auth_token is not None:
        headers["Authorization"] = f"Bearer {auth_token}"
    return headers

fetch_release_page

fetch_release_page(client: AsyncClient, repository: str, *, page: int = 1, per_page: int = MAX_RELEASES_PER_PAGE) -> ReleasePage | None
Source code in src/aerocore/data/adsblol.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
async def fetch_release_page(
    client: AsyncClient,
    repository: str,
    *,
    page: int = 1,
    per_page: int = MAX_RELEASES_PER_PAGE,
) -> ReleasePage | None:
    response = await client.get(
        f"/repos/{repository}/releases",
        params={"page": page, "per_page": per_page},
    )
    if not response.is_success:
        return None
    releases: list[GitHubRelease] = response.json()
    return {
        "repository": repository,
        "page": page,
        "per_page": per_page,
        "releases": releases,
    }

iter_release_asset_downloads

iter_release_asset_downloads(page: ReleasePage) -> Iterator[AdsblolAsset]
Source code in src/aerocore/data/adsblol.py
169
170
171
172
173
174
175
176
177
178
179
180
def iter_release_asset_downloads(page: ReleasePage) -> Iterator[AdsblolAsset]:
    for release in page["releases"]:
        for asset in release["assets"]:
            yield {
                "repository": page["repository"],
                "release_tag": release["tag_name"],
                "asset_id": int(asset["id"]),
                "asset_name": str(asset["name"]),
                "asset_size": int(asset["size"]),
                "asset_digest": asset.get("digest"),
                "browser_download_url": str(asset["browser_download_url"]),
            }

iter_metadata_rows

iter_metadata_rows(path: Path) -> Iterator[AdsblolAsset]
Source code in src/aerocore/data/adsblol.py
183
184
185
186
187
def iter_metadata_rows(path: Path) -> Iterator[AdsblolAsset]:
    with path.open("r", encoding="utf-8") as handle:
        for line in handle:
            if stripped := line.strip():
                yield json.loads(stripped)

run_metadata

run_metadata(*, grouped_specs: Mapping[Path, Iterable[str]], base_url: str, auth_token: str | None, per_page: int = MAX_RELEASES_PER_PAGE, max_pages: int | None = None) -> MetadataSyncResult

Incrementally append newest adsb.lol GitHub release asset metadata.

It treats the release history as an append-only newest-first log and stops per repository once a fetched page contains no unknown asset ids. It does not repair or backfill, in those cases delete the metadata JSONL and rerun to rebuild.

Source code in src/aerocore/data/adsblol.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
async def run_metadata(
    *,
    grouped_specs: Mapping[Path, Iterable[str]],
    base_url: str,
    auth_token: str | None,
    per_page: int = MAX_RELEASES_PER_PAGE,
    max_pages: int | None = None,
) -> MetadataSyncResult:
    """Incrementally append newest adsb.lol GitHub release asset metadata.

    It treats the release history as an append-only newest-first log and stops
    per repository once a fetched page contains no unknown asset ids. It does
    not repair or backfill, in those cases delete the metadata JSONL and
    rerun to rebuild.
    """
    import httpx

    headers = github_headers(auth_token)
    file_results: list[MetadataFileSyncResult] = []

    async with httpx.AsyncClient(
        base_url=base_url,
        headers=headers,
        timeout=60.0,
        follow_redirects=True,
    ) as client:
        for output_path, repositories in grouped_specs.items():
            output_path = output_path.expanduser()
            known_asset_keys = _known_asset_keys(output_path)
            existing_assets = len(known_asset_keys)
            repository_results: list[RepositoryMetadataSyncResult] = []
            rows_to_append: list[AdsblolAsset] = []

            for repository in repositories:
                result, rows = await _sync_repository_incremental(
                    client,
                    repository,
                    known_asset_keys,
                    per_page=per_page,
                    max_pages=max_pages,
                )
                repository_results.append(result)
                rows_to_append.extend(rows)

            appended_assets = append_jsonl_rows_sync(
                output_path,
                rows_to_append,
            )
            file_results.append(
                MetadataFileSyncResult(
                    path=output_path,
                    existing_assets=existing_assets,
                    appended_assets=appended_assets,
                    repositories=tuple(repository_results),
                )
            )

    return MetadataSyncResult(files=tuple(file_results))

append_jsonl_rows_sync

append_jsonl_rows_sync(path: Path, rows: Iterable[AdsblolAsset]) -> int
Source code in src/aerocore/data/adsblol.py
327
328
329
330
331
332
333
334
def append_jsonl_rows_sync(path: Path, rows: Iterable[AdsblolAsset]) -> int:
    path.parent.mkdir(parents=True, exist_ok=True)
    count = 0
    with path.open("a", encoding="utf-8") as handle:
        for row in rows:
            handle.write(json.dumps(row, ensure_ascii=False) + "\n")
            count += 1
    return count

AssetTimePeriod

AssetTimePeriod(asset_name: str, start_at: datetime, end_at: datetime)
asset_name
asset_name: str
start_at
start_at: datetime
end_at
end_at: datetime
day
day: date

ModeVariant

ModeVariant = Literal['prod', 'staging', 'mlatonly', 'test']

ModeAssetPeriod

ModeAssetPeriod(asset_name: str, day: date, variant: ModeVariant, replica: str, part: str | None)
asset_name
asset_name: str
day
day: date
variant
variant: ModeVariant
replica
replica: str
part
part: str | None

ModeDailySizes

ModeDailySizes(prod: int = 0, staging: int = 0, mlatonly: int = 0, test: int = 0)
prod
prod: int = 0
staging
staging: int = 0
mlatonly
mlatonly: int = 0
test
test: int = 0
total
total: int

parse_asset_time_period

parse_asset_time_period(asset_name: str) -> AssetTimePeriod
Source code in src/aerocore/data/adsblol.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def parse_asset_time_period(asset_name: str) -> AssetTimePeriod:
    base_name = asset_name.removesuffix(".zst")

    if match := _DATALINK_RANGE_RE.match(base_name):
        return AssetTimePeriod(
            asset_name=asset_name,
            start_at=_parse_utc_datetime(match["start"], "%Y%m%d-%H%M%S"),
            end_at=_parse_utc_datetime(match["end"], "%Y%m%d-%H%M%S"),
        )

    if match := _DATALINK_DAY_RE.match(base_name):
        start_at = _parse_utc_datetime(match["day"], "%Y-%m-%d")
        return AssetTimePeriod(
            asset_name=asset_name,
            start_at=start_at,
            end_at=start_at + timedelta(days=1),
        )

    if match := _ADSB_DAY_RE.match(base_name):
        start_at = _parse_utc_datetime(
            match["day"].replace(".", "-"),
            "%Y-%m-%d",
        )
        return AssetTimePeriod(
            asset_name=asset_name,
            start_at=start_at,
            end_at=start_at + timedelta(days=1),
        )

    raise ValueError(f"unrecognized asset name: {asset_name}")

parse_mode_asset_period

parse_mode_asset_period(asset_name: str) -> ModeAssetPeriod
Source code in src/aerocore/data/adsblol.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def parse_mode_asset_period(asset_name: str) -> ModeAssetPeriod:
    if not (match := _MODE_DAY_RE.match(asset_name)):
        raise ValueError(f"unrecognized modes asset name: {asset_name}")

    return ModeAssetPeriod(
        asset_name=asset_name,
        day=_parse_utc_datetime(
            match["day"].replace(".", "-"),
            "%Y-%m-%d",
        ).date(),
        variant=cast(ModeVariant, match["variant"]),
        replica=match["replica"],
        part=match["part"],
    )
KNOWN_UNAVAILABLE_DATALINK_URLS = {'https://github.com/adsblol/aircraft-data-links-2026/releases/download/adsblol-adl-2026-05-14/adsblol-adl-vdl2_20260514-165234_20260514-165745.jsonl'}
dir_datalinks(base_dir: Path) -> Path
Source code in src/aerocore/data/adsblol.py
468
469
def dir_datalinks(base_dir: Path) -> Path:
    return base_dir / "adsblol_datalinks"

DatalinkDownloadItem

DatalinkDownloadItem(repository: str, release_tag: str, asset_name: str, asset_day: date, asset_size: int, browser_download_url: str, output_path: Path, already_downloaded: bool)
repository
repository: str
release_tag
release_tag: str
asset_name
asset_name: str
asset_day
asset_day: date
asset_size
asset_size: int
browser_download_url
browser_download_url: str
output_path
output_path: Path
already_downloaded
already_downloaded: bool

DatalinkPlan

DatalinkPlan(metadata_path: Path, output_root: Path, items: tuple[DatalinkDownloadItem, ...], selected_by_repo: Mapping[str, int], existing_by_repo: Mapping[str, int])
metadata_path
metadata_path: Path
output_root
output_root: Path
items
selected_by_repo
selected_by_repo: Mapping[str, int]
existing_by_repo
existing_by_repo: Mapping[str, int]
selected
selected: int
existing
existing: int
missing
missing: int
missing_items
missing_items: tuple[DatalinkDownloadItem, ...]

DatalinkDownloadResult

DatalinkDownloadResult(plan: DatalinkPlan, existing: int, downloaded: int, unavailable: int, output_paths: tuple[Path, ...])
plan
existing
existing: int
downloaded
downloaded: int
unavailable
unavailable: int
output_paths
output_paths: tuple[Path, ...]

DatalinkDownloadFailure

DatalinkDownloadFailure(item: DatalinkDownloadItem, exception: Exception)
exception
exception: Exception
plan_datalinks(*, metadata_path: Path, output_root: Path, start_date: date | datetime | None = None, end_date: date | datetime | None = None) -> DatalinkPlan
Source code in src/aerocore/data/adsblol.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
def plan_datalinks(
    *,
    metadata_path: Path,
    output_root: Path,
    start_date: date | datetime | None = None,
    end_date: date | datetime | None = None,
) -> DatalinkPlan:
    metadata_path = metadata_path.expanduser()
    output_root = output_root.expanduser()
    start = (
        start_date.date() if isinstance(start_date, datetime) else start_date
    )
    end = end_date.date() if isinstance(end_date, datetime) else end_date
    existing_jsonl = {
        path.name
        for path in output_root.glob("*.jsonl")
        if path.is_file() and path.stat().st_size > 0
    }
    selected_by_repo: Counter[str] = Counter()
    existing_by_repo: Counter[str] = Counter()
    items: list[DatalinkDownloadItem] = []
    seen_asset_keys: set[AssetKey] = set()
    seen_output_names: set[str] = set()

    for row in iter_metadata_rows(metadata_path):
        repository = row["repository"]
        if not repository.startswith("adsblol/aircraft-data-links-"):
            continue
        if row["browser_download_url"] in KNOWN_UNAVAILABLE_DATALINK_URLS:
            continue

        key = AssetKey(repository, int(row["asset_id"]))
        if key in seen_asset_keys:
            continue
        seen_asset_keys.add(key)

        asset_day = parse_asset_time_period(row["asset_name"]).day
        if start is not None and asset_day < start:
            continue
        if end is not None and asset_day > end:
            continue

        output_name = _asset_output_name(row["asset_name"])
        if output_name in seen_output_names:
            raise ValueError(f"duplicate datalink output name: {output_name}")
        seen_output_names.add(output_name)

        already_downloaded = output_name in existing_jsonl
        selected_by_repo[repository] += 1
        if already_downloaded:
            existing_by_repo[repository] += 1

        items.append(
            DatalinkDownloadItem(
                repository=repository,
                release_tag=row["release_tag"],
                asset_name=row["asset_name"],
                asset_day=asset_day,
                asset_size=int(row["asset_size"]),
                browser_download_url=row["browser_download_url"],
                output_path=output_root / output_name,
                already_downloaded=already_downloaded,
            )
        )

    return DatalinkPlan(
        metadata_path=metadata_path,
        output_root=output_root,
        items=tuple(
            sorted(
                items,
                key=lambda item: (
                    item.asset_day,
                    item.repository,
                    item.release_tag,
                    item.asset_name,
                ),
            )
        ),
        selected_by_repo=dict(sorted(selected_by_repo.items())),
        existing_by_repo=dict(sorted(existing_by_repo.items())),
    )
datalinks_download(*, metadata_path: Path, output_root: Path, start_date: date | datetime | None = None, end_date: date | datetime | None = None, dry_run: bool = False, jobs: int | None = None) -> DatalinkDownloadResult
Source code in src/aerocore/data/adsblol.py
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
def datalinks_download(
    *,
    metadata_path: Path,
    output_root: Path,
    start_date: date | datetime | None = None,
    end_date: date | datetime | None = None,
    dry_run: bool = False,
    jobs: int | None = None,
) -> DatalinkDownloadResult:
    plan = plan_datalinks(
        metadata_path=metadata_path,
        output_root=output_root,
        start_date=start_date,
        end_date=end_date,
    )
    missing_items = plan.missing_items
    if dry_run or not missing_items:
        return DatalinkDownloadResult(
            plan=plan,
            existing=plan.existing,
            downloaded=0,
            unavailable=0,
            output_paths=tuple(
                item.output_path
                for item in plan.items
                if item.already_downloaded
            ),
        )

    result = _download_missing_items(missing_items, jobs=jobs)
    return DatalinkDownloadResult(
        plan=plan,
        existing=result.existing,
        downloaded=result.downloaded,
        unavailable=result.unavailable,
        output_paths=result.output_paths,
    )

load_metadata_daily_sizes

load_metadata_daily_sizes(paths: Iterable[Path], stem: str) -> dict[date, int]
Source code in src/aerocore/data/adsblol.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
def load_metadata_daily_sizes(
    paths: Iterable[Path],
    stem: str,
) -> dict[date, int]:
    daily_sizes: dict[date, int] = {}
    seen_keys: set[AssetKey] = set()

    for path in _iter_input_paths(paths, stem):
        for row in iter_metadata_rows(path):
            key = AssetKey(row["repository"], int(row["asset_id"]))
            if key in seen_keys:
                continue
            seen_keys.add(key)
            day = parse_asset_time_period(row["asset_name"]).day
            daily_sizes[day] = daily_sizes.get(day, 0) + int(row["asset_size"])

    return dict(sorted(daily_sizes.items()))

load_modes_daily_sizes

load_modes_daily_sizes(paths: Iterable[Path], stem: str) -> dict[date, ModeDailySizes]
Source code in src/aerocore/data/adsblol.py
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
def load_modes_daily_sizes(
    paths: Iterable[Path],
    stem: str,
) -> dict[date, ModeDailySizes]:
    daily_sizes: dict[date, ModeDailySizes] = {}
    seen_keys: set[AssetKey] = set()

    for path in _iter_input_paths(paths, stem):
        for row in iter_metadata_rows(path):
            key = AssetKey(row["repository"], int(row["asset_id"]))
            if key in seen_keys:
                continue
            seen_keys.add(key)
            try:
                period = parse_mode_asset_period(row["asset_name"])
            except ValueError:
                continue
            day_sizes = daily_sizes.setdefault(period.day, ModeDailySizes())
            asset_size = int(row["asset_size"])
            if period.variant == "prod":
                day_sizes = ModeDailySizes(
                    prod=day_sizes.prod + asset_size,
                    staging=day_sizes.staging,
                    mlatonly=day_sizes.mlatonly,
                    test=day_sizes.test,
                )
            elif period.variant == "staging":
                day_sizes = ModeDailySizes(
                    prod=day_sizes.prod,
                    staging=day_sizes.staging + asset_size,
                    mlatonly=day_sizes.mlatonly,
                    test=day_sizes.test,
                )
            elif period.variant == "mlatonly":
                day_sizes = ModeDailySizes(
                    prod=day_sizes.prod,
                    staging=day_sizes.staging,
                    mlatonly=day_sizes.mlatonly + asset_size,
                    test=day_sizes.test,
                )
            elif period.variant == "test":
                day_sizes = ModeDailySizes(
                    prod=day_sizes.prod,
                    staging=day_sizes.staging,
                    mlatonly=day_sizes.mlatonly,
                    test=day_sizes.test + asset_size,
                )
            daily_sizes[period.day] = day_sizes

    return dict(sorted(daily_sizes.items()))

build_adsblol_figure

build_adsblol_figure(modes_daily_sizes: Mapping[date, ModeDailySizes], datalinks_daily_sizes: Mapping[date, int], *, line_color: str = '#5470c6', datalink_color: str = '#d9a15b', prod_color: str = '#5a9a8b', staging_color: str = '#c97c76', mlatonly_color: str = '#7d8a8c', test_color: str = '#8c7bd6') -> Figure
Source code in src/aerocore/data/adsblol.py
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
def build_adsblol_figure(
    modes_daily_sizes: Mapping[date, ModeDailySizes],
    datalinks_daily_sizes: Mapping[date, int],
    *,
    line_color: str = "#5470c6",
    datalink_color: str = "#d9a15b",
    prod_color: str = "#5a9a8b",
    staging_color: str = "#c97c76",
    mlatonly_color: str = "#7d8a8c",
    test_color: str = "#8c7bd6",
) -> Figure:
    import matplotlib.dates as mdates
    import matplotlib.pyplot as plt

    modes_days: list[Any] = list(modes_daily_sizes.keys())
    modes_prod_values = [modes_daily_sizes[day].prod for day in modes_days]
    modes_staging_values = [
        modes_daily_sizes[day].staging for day in modes_days
    ]
    modes_mlatonly_values = [
        modes_daily_sizes[day].mlatonly for day in modes_days
    ]
    modes_test_values = [modes_daily_sizes[day].test for day in modes_days]
    modes_total_values = [modes_daily_sizes[day].total for day in modes_days]
    modes_cumulative_values = list(accumulate(modes_total_values))

    dl_days: list[Any] = list(datalinks_daily_sizes.keys())
    dl_values = [datalinks_daily_sizes[day] for day in dl_days]
    dl_cumu_values = list(accumulate(dl_values))

    fig = plt.figure(figsize=(13, 8))
    grid = fig.add_gridspec(2, 1, height_ratios=[1, 1], hspace=0)
    ax_modes_top = fig.add_subplot(grid[0, 0])
    ax_modes_bottom = ax_modes_top.twinx()
    ax_dl_top = fig.add_subplot(grid[1, 0], sharex=ax_modes_top)
    ax_dl_bottom = ax_dl_top.twinx()

    ax_modes_top.plot(
        modes_days, modes_cumulative_values, color=prod_color, linewidth=2.4
    )
    ax_modes_bottom.bar(
        modes_days, modes_prod_values, color=prod_color, alpha=0.8, width=0.8
    )
    ax_modes_bottom.bar(
        modes_days,
        modes_staging_values,
        bottom=modes_prod_values,
        color=staging_color,
        alpha=0.55,
        width=0.8,
    )
    staging_bottom = [
        prod + staging
        for prod, staging in zip(modes_prod_values, modes_staging_values)
    ]
    ax_modes_bottom.bar(
        modes_days,
        modes_mlatonly_values,
        bottom=staging_bottom,
        color=mlatonly_color,
        alpha=0.5,
        width=0.8,
    )
    test_bottom = [
        bottom + mlatonly
        for bottom, mlatonly in zip(staging_bottom, modes_mlatonly_values)
    ]
    ax_modes_bottom.bar(
        modes_days,
        modes_test_values,
        bottom=test_bottom,
        color=test_color,
        alpha=0.45,
        width=0.8,
    )

    ax_dl_top.plot(dl_days, dl_cumu_values, color=line_color, linewidth=2.4)
    ax_dl_bottom.bar(
        dl_days, dl_values, color=datalink_color, alpha=0.28, width=0.8
    )

    ax_modes_top.set_ylabel("adsb cumulative raw hosted bytes")
    ax_modes_bottom.set_ylabel("daily raw hosted bytes")
    ax_modes_top.tick_params(axis="y", colors=prod_color)
    ax_modes_bottom.tick_params(axis="y")
    ax_modes_top.spines["left"].set_color(prod_color)
    ax_modes_top.set_ylim(bottom=0)
    ax_modes_bottom.set_ylim(bottom=0)
    ax_modes_top.tick_params(axis="x", labelbottom=False)

    ax_dl_top.set_ylabel(
        "datalinks cumulative raw hosted bytes", color=line_color
    )
    ax_dl_bottom.set_ylabel("daily raw hosted bytes", color=datalink_color)
    ax_dl_top.tick_params(axis="y", colors=line_color)
    ax_dl_bottom.tick_params(axis="y", colors=datalink_color)
    ax_dl_top.spines["left"].set_color(line_color)
    ax_dl_bottom.spines["right"].set_color(datalink_color)
    ax_dl_top.set_ylim(bottom=0)
    ax_dl_bottom.set_ylim(bottom=0)
    ax_dl_top.tick_params(axis="x", which="both", bottom=True, labelbottom=True)
    ax_dl_bottom.tick_params(axis="x", bottom=False, labelbottom=False)

    legend_items = [
        ("prod", prod_color, 0.01),
        ("staging", staging_color, 0.07),
        ("mlatonly", mlatonly_color, 0.18),
        ("test", test_color, 0.30),
    ]
    for text, color, x in legend_items:
        ax_modes_top.text(
            x,
            0.98,
            text,
            color=color,
            transform=ax_modes_top.transAxes,
            va="top",
        )
    ax_dl_top.text(
        0.01,
        0.98,
        "datalinks",
        color=datalink_color,
        transform=ax_dl_top.transAxes,
        va="top",
    )

    ax_dl_top.xaxis.set_major_locator(
        mdates.AutoDateLocator()  # type: ignore[no-untyped-call]
    )
    ax_dl_top.xaxis.set_major_formatter(
        mdates.DateFormatter("%Y-%m-%d")  # type: ignore[no-untyped-call]
    )
    for label in ax_dl_top.get_xticklabels():
        label.set_rotation(30)
        label.set_ha("right")  # type: ignore[attr-defined]

    fig.subplots_adjust(
        hspace=0,
        left=0.05,
        right=0.985,
        top=0.97,
        bottom=0.095,
    )
    return fig

DatalinkProtocol

DatalinkProtocol = Literal['acars', 'vdl2', 'hfdl']

DatalinkAppRequired

Bases: TypedDict

name
name: str

Decoder name, e.g. 'acarsdec', 'dumpvdl2', or 'dumphfdl'

ver
ver: str

Decoder version/git revision, e.g. '8fcf327' or '2.4.0-dirty'

DatalinkApp

Bases: DatalinkAppRequired

See: https://github.com/sdr-enthusiasts/acars_router

proxied
proxied: bool
proxied_by
proxied_by: str
acars_router_version
acars_router_version: str
acars_router_uuid
acars_router_uuid: str
name
name: str

Decoder name, e.g. 'acarsdec', 'dumpvdl2', or 'dumphfdl'

ver
ver: str

Decoder version/git revision, e.g. '8fcf327' or '2.4.0-dirty'

DatalinkAdsblolMeta

Bases: TypedDict

received_at
received_at: str

ISO-8601 UTC, e.g. '2025-11-22T00:18:44Z' or UTC offset

protocol
protocol: DatalinkProtocol
made_by
made_by: str

DatalinkTime

Bases: TypedDict

sec
sec: int

Unix epoch seconds, UTC

usec
usec: int

Microsecond component

AcarsFlightId

AcarsFlightId = str

Flight id/number from ACARS, e.g. 'UA0097'. Not exactly the callsign

Registration

Registration = str

Aircraft registration, e.g. 'B-320S' or 'N876UA'

AcarsDatalinkRequired

Bases: TypedDict

freq

VHF frequency in MHz, e.g. 131.45

channel
channel: int

Decoder input channel index, not one-to-one with freq; typically 0..11

error
error: int

Decoder error/correction count; typically 0..3

level
level: float

Decoder signal level metric; typically about -62.0..2.3

timestamp
timestamp: TimestampUtcS[float]

Decoder Unix epoch seconds, UTC; usually within ~1s of received_at

app
station_id
station_id: str

Receiver station id, e.g. 'RK-YBAF-ACARS'

assstat
assstat: Literal['complete', 'duplicate', 'in progress', 'out of sequence', 'skipped'] | str
mode
mode: str

ACARS mode character; usually '2'

label
label: str

ACARS message label, e.g. 'H1', 'Q0', 'MA'

Bases: AcarsDatalinkRequired

block_id
block_id: str

ACARS block id; usually one digit, sometimes one letter

ack
ack: Literal[False] | str

False or one-character decoded ACK/NAK/control field

tail
text
text: str

Application text; may be empty or thousands of characters

msgno
msgno: str

Message sequence number, commonly four chars, e.g. 'S35A'

flight
flight: AcarsFlightId
freq

VHF frequency in MHz, e.g. 131.45

channel
channel: int

Decoder input channel index, not one-to-one with freq; typically 0..11

error
error: int

Decoder error/correction count; typically 0..3

level
level: float

Decoder signal level metric; typically about -62.0..2.3

timestamp
timestamp: TimestampUtcS[float]

Decoder Unix epoch seconds, UTC; usually within ~1s of received_at

app
station_id
station_id: str

Receiver station id, e.g. 'RK-YBAF-ACARS'

assstat
assstat: Literal['complete', 'duplicate', 'in progress', 'out of sequence', 'skipped'] | str
mode
mode: str

ACARS mode character; usually '2'

label
label: str

ACARS message label, e.g. 'H1', 'Q0', 'MA'

EmbeddedAcarsRequired

Bases: TypedDict

err
err: bool

Decoder error flag

crc_ok
crc_ok: bool

Decoded ACARS CRC status

more
more: bool

True when more message blocks follow

reg
reg: str

Aircraft registration, often with leading '.', e.g. '.VH-XZD'

mode
mode: str

ACARS mode character; typically '2'

label
label: str

ACARS message label, e.g. 'H1', 'Q0', 'SA'

blk_id
blk_id: str
ack
ack: str

One-character decoded ACK/NAK/control field, e.g. '!'

msg_text
msg_text: str

ACARS application text; may be empty

EmbeddedAcars

Bases: EmbeddedAcarsRequired

flight
flight: AcarsFlightId
msg_num
msg_num: str

Message number, e.g. 'M23'

msg_num_seq
msg_num_seq: str

Message number sequence component, e.g. 'A'

sublabel
sublabel: str

ACARS sublabel when decoded

mfi
mfi: str

Message function identifier when decoded

arinc622
arinc622: dict[str, Any]
err
err: bool

Decoder error flag

crc_ok
crc_ok: bool

Decoded ACARS CRC status

more
more: bool

True when more message blocks follow

reg
reg: str

Aircraft registration, often with leading '.', e.g. '.VH-XZD'

mode
mode: str

ACARS mode character; typically '2'

label
label: str

ACARS message label, e.g. 'H1', 'Q0', 'SA'

blk_id
blk_id: str
ack
ack: str

One-character decoded ACK/NAK/control field, e.g. '!'

msg_text
msg_text: str

ACARS application text; may be empty

Vdl2AddressRequired

Bases: TypedDict

addr
addr: str

VDL2 AVLC address, e.g. '7C77F7'

type
type: Literal['Aircraft', 'Ground station'] | str

Vdl2Address

Bases: Vdl2AddressRequired

status
status: Literal['Airborne', 'On ground'] | str
addr
addr: str

VDL2 AVLC address, e.g. '7C77F7'

type
type: Literal['Aircraft', 'Ground station'] | str

Vdl2Param

Bases: TypedDict

name
name: str

VDL2/XID parameter name

value
value: Any

VDL2/XID parameter value; heterogeneous scalar/list/dict

Vdl2XidRequired

Bases: TypedDict

err
err: bool

Decoder error flag; typically False

type
type: Literal['GSIF', 'XID_CMD_HO', 'XID_CMD_LCR', 'XID_CMD_LE', 'XID_RSP_HO', 'XID_RSP_LCR', 'XID_RSP_LE'] | str

XID subtype

type_descr
type_descr: str

Human-readable XID subtype description

vdl_params
vdl_params: list[Vdl2Param]

VDL-specific XID params; heterogeneous values

Vdl2Xid

Bases: Vdl2XidRequired

pub_params
pub_params: list[Vdl2Param]

Public XID params; heterogeneous values

err
err: bool

Decoder error flag; typically False

type
type: Literal['GSIF', 'XID_CMD_HO', 'XID_CMD_LCR', 'XID_CMD_LE', 'XID_RSP_HO', 'XID_RSP_LCR', 'XID_RSP_LE'] | str

XID subtype

type_descr
type_descr: str

Human-readable XID subtype description

vdl_params
vdl_params: list[Vdl2Param]

VDL-specific XID params; heterogeneous values

Vdl2AvlcRequired

Bases: TypedDict

cr
cr: Literal['Command', 'Response'] | str

AVLC command/response bit decoded as text

dst
frame_type
frame_type: Literal['I', 'U'] | str

AVLC frame type; typically information and unnumbered frames

src

Vdl2Avlc

Bases: Vdl2AvlcRequired

rseq
rseq: int

I-frame receive sequence number

sseq
sseq: int

I-frame send sequence number

poll
poll: bool

I-frame poll bit

cmd
cmd: Literal['DISC', 'DM', 'FRMR', 'UA', 'XID'] | str

U-frame command

pf
pf: bool

U-frame poll/final bit

xid
xid: Vdl2Xid
acars
cr
cr: Literal['Command', 'Response'] | str

AVLC command/response bit decoded as text

dst
frame_type
frame_type: Literal['I', 'U'] | str

AVLC frame type; typically information and unnumbered frames

src

Vdl2Payload

Bases: TypedDict

app
avlc
avlc: Vdl2Avlc
burst_len_octets
burst_len_octets: int

VDL2 burst length in octets; typically 13..1056

freq
freq: FrequencyHz[int]

RF frequency in Hz, e.g. 136975000

idx
idx: int

Decoder input/channel index; typically 0..3

freq_skew
freq_skew: float

Decoder-estimated frequency skew/offset

hdr_bits_fixed
hdr_bits_fixed: int

Header bits corrected by decoder; typically 0..1

noise_level
noise_level: float
octets_corrected_by_fec
octets_corrected_by_fec: int

Octets corrected by forward error correction; typically 0..8

sig_level
sig_level: float

Decoder signal level metric

station
station: str

Receiver station id, e.g. 'RK-YBAF-VDL2'

Bases: TypedDict

vdl2

HfdlCodeDescription

Bases: TypedDict

code
code: int

Protocol/decoder numeric code

descr
descr: str

Human-readable code description

HfdlTypeCode

Bases: TypedDict

name
name: str

Decoded type name, e.g. 'Frequency data' or 'Logon resume'

id
id: int

Protocol numeric type id

HfdlAircraftInfo

Bases: TypedDict

icao
icao: str

ICAO hex address

manuf
manuf: str

Aircraft manufacturer

model
model: str

Aircraft model

opercode
opercode: str
owner
owner: str
regnr
regnr: Registration
typecode
typecode: str

Aircraft type

HfdlEndpointRequired

Bases: TypedDict

type
type: Literal['Aircraft', 'Ground station'] | str
id
id: int

HFDL endpoint id; aircraft ids are dynamic within HFDL

HfdlEndpoint

Bases: HfdlEndpointRequired

name
name: str

Ground station name when known, e.g. 'Shannon, Ireland'

ac_info
type
type: Literal['Aircraft', 'Ground station'] | str
id
id: int

HFDL endpoint id; aircraft ids are dynamic within HFDL

HfdlFrequency

Bases: TypedDict

id
id: int

Frequency table id

freq
freq: float

HfdlPosition

Bases: TypedDict

lat
lon

HfdlClockTime

Bases: TypedDict

UTC

hour
hour: int
min
min: int
sec
sec: int

HfdlFrequencySearchCount

Bases: TypedDict

cur_leg
cur_leg: int
prev_leg
prev_leg: int

HfdlDisabledDuration

Bases: TypedDict

this_leg
this_leg: int
prev_leg
prev_leg: int

BitRateKey

BitRateKey = str

HfdlPduStats

Bases: TypedDict

mpdus_rx_ok_cnt
mpdus_rx_ok_cnt: dict[BitRateKey, int]
mpdus_rx_err_cnt
mpdus_rx_err_cnt: dict[BitRateKey, int]
mpdus_tx_cnt
mpdus_tx_cnt: dict[BitRateKey, int]
mpdus_delivered_cnt
mpdus_delivered_cnt: dict[BitRateKey, int]
spdus_rx_ok_cnt
spdus_rx_ok_cnt: int
spdus_missed_cnt
spdus_missed_cnt: int

HfdlFrequencyData

Bases: TypedDict

gs
heard_on_freqs
heard_on_freqs: list[HfdlFrequency]
listening_on_freqs
listening_on_freqs: list[HfdlFrequency]

HfdlHfnpduRequired

Bases: TypedDict

err
err: bool
type

HFNPDU type, e.g. Frequency data, Enveloped data, Performance data

HfdlHfnpdu

Bases: HfdlHfnpduRequired

flight_id
flight_id: str
pos
utc_time
utc_time: HfdlClockTime
freq_data
freq_data: list[HfdlFrequencyData]
acars
version
version: int
time
flight_leg_num
flight_leg_num: int
gs
frequency
frequency: HfdlFrequency
freq_search_cnt
freq_search_cnt: HfdlFrequencySearchCount
hfdl_disabled_duration
hfdl_disabled_duration: HfdlDisabledDuration
pdu_stats
pdu_stats: HfdlPduStats
last_freq_change_cause
last_freq_change_cause: HfdlCodeDescription
err
err: bool
type

HFNPDU type, e.g. Frequency data, Enveloped data, Performance data

HfdlLpduRequired

Bases: TypedDict

err
err: bool
dst
src
type

HfdlLpdu

Bases: HfdlLpduRequired

hfnpdu
hfnpdu: HfdlHfnpdu
ac_info
assigned_ac_id
assigned_ac_id: int

Aircraft id assigned by ground station during logon confirm

reason
err
err: bool
dst
src
type

HfdlGsStatus

Bases: TypedDict

gs
utc_sync
utc_sync: bool

Whether ground station is UTC-synchronized

freqs

HfdlSpdu

Bases: TypedDict

err
err: bool
src
spdu_version
spdu_version: int
rls
rls: int
iso
iso: int
change_note
change_note: int
frame_index
frame_index: int
frame_offset
frame_offset: int
min_priority
min_priority: int
systable_version
systable_version: int
gs_status
gs_status: list[HfdlGsStatus]

HfdlPayloadRequired

Bases: TypedDict

app
freq
freq: FrequencyHz[int]

RF frequency, e.g. 8942000

noise_level
noise_level: float
sig_level
sig_level: float
station
station: str

Receiver station id, e.g. 'SS-EGBE-HFDL1'

bit_rate
bit_rate: BitPS[int]

HFDL bitrate; typically 300, 600, 1200

freq_skew
freq_skew: float
slot
slot: Literal['S', 'D'] | str

HfdlPayload

Bases: HfdlPayloadRequired

lpdu
lpdu: HfdlLpdu
spdu
spdu: HfdlSpdu
app
freq
freq: FrequencyHz[int]

RF frequency, e.g. 8942000

noise_level
noise_level: float
sig_level
sig_level: float
station
station: str

Receiver station id, e.g. 'SS-EGBE-HFDL1'

bit_rate
bit_rate: BitPS[int]

HFDL bitrate; typically 300, 600, 1200

freq_skew
freq_skew: float
slot
slot: Literal['S', 'D'] | str

Bases: TypedDict

hfdl

era5

Google Research's Analysis-Ready & Cloud Optimized (ARCO) ERA5 dataset

Format: netcdf, indexed by the specific date and pressure level.

See:

Data License: Copernicus license

Requires extras:

  • httpx, polars
  • gcloud CLI to be installed and authenticated

GOOGLE_STORAGE_URI

GOOGLE_STORAGE_URI = 'gs://gcp-public-data-arco-era5/raw/date-variable-pressure_level'

PRESSURE_LEVELS

PRESSURE_LEVELS: PressureHPA[tuple[int, ...]] = (*(range(100, 275, 25)), *(range(300, 750, 50)), *(range(750, 1025, 25)))

logger

logger = getLogger(__name__)

EcmwfParameter

Bases: NamedTuple

id_
id_: int
name
name: str
short_name
short_name: str
quantity
quantity: str | object

VARIABLES

VARIABLES: list[EcmwfParameter] = [EcmwfParameter(248, 'fraction_of_cloud_cover', 'cc', Dimensionless('fraction')), EcmwfParameter(129, 'geopotential', 'z', M ** 2 * S ** -2), EcmwfParameter(203, 'ozone_mass_mixing_ratio', 'o3', Dimensionless('mass_mixing_ratio')), EcmwfParameter(60, 'potential_vorticity', 'pv', S ** -1), EcmwfParameter(247, 'specific_cloud_ice_water_content', 'ciwc', Dimensionless('mass_mixing_ratio')), EcmwfParameter(246, 'specific_cloud_liquid_water_content', 'clwc', Dimensionless('mass_mixing_ratio')), EcmwfParameter(133, 'specific_humidity', 'q', Dimensionless('mass_mixing_ratio')), EcmwfParameter(130, 'temperature', 't', STATIC_TEMPERATURE(K)), EcmwfParameter(131, 'u_component_of_wind', 'u', WIND_SPEED(M_PERS)), EcmwfParameter(132, 'v_component_of_wind', 'v', WIND_SPEED(M_PERS)), EcmwfParameter(135, 'vertical_velocity', 'w', PA * S ** -1)]

Available variables under the raw bucket.

VARIABLES_MAP

VARIABLES_MAP = {(name): (short_name) for v in VARIABLES}

dates

dates(start: datetime, end: datetime) -> Generator[str, None, None]

Generate dates in the format YYYY/MM/DD from start to end, inclusive.

Source code in src/aerocore/data/era5.py
102
103
104
105
106
107
108
109
def dates(start: datetime, end: datetime) -> Generator[str, None, None]:
    """
    Generate dates in the format `YYYY/MM/DD` from start to end, inclusive.
    """
    curr = start
    while curr <= end:
        yield curr.strftime("%Y/%m/%d")
        curr += timedelta(days=1)

fetch_weather

fetch_weather(date_start: datetime = datetime(2023, 2, 1, tzinfo=utc), date_end: datetime = datetime(2023, 2, 1, tzinfo=utc), *, base_dir: Path, variables: list[str] = list(keys()), pressure_levels: tuple[int, ...] = PRESSURE_LEVELS, gs_base: str = GOOGLE_STORAGE_URI) -> None

Recursively download all global ERA5 data for the specified date interval, pressure levels and variables as NetCDF files.

The directory structure will be mirrored as: {base_dir}/{YYYY}/{MM}/{DD}/{variable_name}/{pressure_level}.nc.

Source code in src/aerocore/data/era5.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def fetch_weather(
    date_start: datetime = datetime(2023, 2, 1, tzinfo=pytz.utc),
    date_end: datetime = datetime(2023, 2, 1, tzinfo=pytz.utc),
    *,
    base_dir: Path,
    variables: list[str] = list(VARIABLES_MAP.keys()),
    pressure_levels: tuple[int, ...] = PRESSURE_LEVELS,
    gs_base: str = GOOGLE_STORAGE_URI,
) -> None:
    """
    Recursively download all global ERA5 data for the specified date
    interval, pressure levels and variables as NetCDF files.

    The directory structure will be mirrored as:
    `{base_dir}/{YYYY}/{MM}/{DD}/{variable_name}/{pressure_level}.nc`.
    """
    for date in dates(date_start, date_end):
        for variable in variables:
            path_out = base_dir / date / variable
            path_out.mkdir(parents=True, exist_ok=True)

            queue = []
            for level in pressure_levels:
                fp_relative = Path(date) / variable / f"{level}.nc"
                if (base_dir / fp_relative).is_file():
                    continue
                queue.append(f"{gs_base}/{fp_relative}".encode())
            if not queue:
                logger.info("%s: skipping, all exists", path_out)
                continue
            logger.info("%s: downloading {len(queue)}", path_out)
            subprocess.check_output(
                ["gcloud", "storage", "cp", "-I", str(path_out)],
                input=b"\n".join(queue),
            )

concat_dataset

concat_dataset(variable: str, base_dir_date: Path) -> Dataset

Concatenates all pressure levels for a given variable and date into a single dataset

Example:

<xarray.Dataset> Size: 5GB
Dimensions:    (isobaricInhPa: 27, time: 24, latitude: 721, longitude: 1440)
Coordinates:
* longitude    (longitude) float32 6kB 0.0 0.25 0.5 ... 359.2 359.5 359.8
* latitude     (latitude) float32 3kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0
* time         (time) datetime64[ns] 192B 2023-02-01 ... 2023-02-01T23:00:00
* isobaricInhPa  (isobaricInhPa) int64 216B 100 1000 125 150 ... 925 950 975
Data variables:
    z      (isobaricInhPa, time, latitude, longitude) float64 5GB dask.array
    <chunksize=(1, 24, 721, 1440), meta=np.ndarray>
Attributes:
    Conventions:  CF-1.6
    history:  2023-06-24 08:54:57 GMT by grib_to_netcdf-2.25.1: /opt/ecmw...
Source code in src/aerocore/data/era5.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def concat_dataset(
    variable: str,
    base_dir_date: Path,
) -> xr.Dataset:
    """
    Concatenates all pressure levels for a given variable and date
    into a single dataset

    Example:

    ```txt
    <xarray.Dataset> Size: 5GB
    Dimensions:    (isobaricInhPa: 27, time: 24, latitude: 721, longitude: 1440)
    Coordinates:
    * longitude    (longitude) float32 6kB 0.0 0.25 0.5 ... 359.2 359.5 359.8
    * latitude     (latitude) float32 3kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0
    * time         (time) datetime64[ns] 192B 2023-02-01 ... 2023-02-01T23:00:00
    * isobaricInhPa  (isobaricInhPa) int64 216B 100 1000 125 150 ... 925 950 975
    Data variables:
        z      (isobaricInhPa, time, latitude, longitude) float64 5GB dask.array
        <chunksize=(1, 24, 721, 1440), meta=np.ndarray>
    Attributes:
        Conventions:  CF-1.6
        history:  2023-06-24 08:54:57 GMT by grib_to_netcdf-2.25.1: /opt/ecmw...
    ```
    """

    weather_variable = base_dir_date / variable
    weather_variable_fps = list(
        sorted(
            weather_variable.glob("*.nc"),
            key=lambda x: int(x.stem),
            reverse=True,
        )
    )
    logger.debug(
        "reading %s, found %s nc files",
        variable,
        len(weather_variable_fps),
    )

    # NOTE: the pressure dimension is not included in each file - we generate
    # placeholders to be later overwritten
    def add_dummy_pressure_dim(ds: xr.Dataset) -> xr.Dataset:
        ds = ds.expand_dims(isobaricInhPa=[random.uniform(100, 1000)])
        return ds

    ds = xr.open_mfdataset(
        weather_variable_fps,
        engine="netcdf4",
        concat_dim="isobaricInhPa",
        combine="nested",
        preprocess=add_dummy_pressure_dim,
    )
    ds.assign_coords(
        isobaricInhPa=[int(fp.stem) for fp in weather_variable_fps]
    )

    return ds

build_path

build_path(base_dir: Path, year: int, month: int, day: int) -> Path
Source code in src/aerocore/data/era5.py
211
212
def build_path(base_dir: Path, year: int, month: int, day: int) -> Path:
    return base_dir / f"{year:04d}" / f"{month:02d}" / f"{day:02d}"

get_data_for_trajectory

get_data_for_trajectory(trajectory: LazyFrame, *, base_dir: Path, year: int, month: int, day: int) -> LazyFrame

Extract weather data for the given trajectory

Returns:

Type Description
LazyFrame

a lazyframe with the weather data

Source code in src/aerocore/data/era5.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def get_data_for_trajectory(
    trajectory: pl.LazyFrame,
    *,
    base_dir: Path,
    year: int,
    month: int,
    day: int,
) -> pl.LazyFrame:
    """
    Extract weather data for the given trajectory

    :return: a lazyframe with the weather data
    """
    base_dir_date = build_path(base_dir, year, month, day)
    time, longitude, latitude, alt = trajectory.select(
        pl.from_epoch(pl.col("timestamp"), time_unit="s"),  # datetime64
        (pl.col("longitude").degrees() + 180),  # [0, 360]
        pl.col("latitude").degrees(),  # [-90, 90]
        pl.col("altitude"),  # meters
    ).collect()

    atmos = atmosphere(alt.to_numpy(), delta_temperature=0, xp=np)

    times_ = xr.DataArray(time.to_numpy(), dims=["points"])
    lons_ = xr.DataArray(longitude, dims=["points"])
    lats_ = xr.DataArray(latitude, dims=["points"])
    pressures_ = xr.DataArray(atmos.pressure / 100, dims=["points"])  # hPa

    weather = {}
    for variable_name, variable_key in VARIABLES_MAP.items():
        ds = concat_dataset(variable_name, base_dir_date)
        values = ds.interp(
            time=times_,
            latitude=lats_,
            longitude=lons_,
            isobaricInhPa=pressures_,
            kwargs={"fill_value": None},
        )
        values_np = values[variable_key].values
        weather[variable_name] = values_np

    lf = pl.LazyFrame(weather)
    return lf

engine_emissions

ICAO Aircraft Engine Emissions Databank

Requires extras:

  • httpx, polars

URL_EMISSIONS

URL_EMISSIONS = 'https://www.easa.europa.eu/en/downloads/131424/en'

EmissionsData

Bases: NamedTuple

data
data: DataFrame
schema
schema: DataFrame

fetch_emissions_data

fetch_emissions_data(client: AsyncClient) -> EmissionsData
Source code in src/aerocore/data/engine_emissions.py
43
44
45
async def fetch_emissions_data(client: httpx.AsyncClient) -> EmissionsData:
    response_content = await _fetch_data(client)
    return _parse_data(response_content)

aircraft_types

List of aircraft types, from ICAO DOC8643

Requires extras:

  • httpx, polars

SCHEMA_AIRCRAFT_TYPES

SCHEMA_AIRCRAFT_TYPES = {'ModelFullName': String(), 'Description': String(), 'WTC': Enum(['H', 'M', 'L', 'J', 'L/M']), 'WTG': Enum(['E', 'Z', 'F', 'C', 'D', 'G', 'A', 'B']), 'Designator': String(), 'ManufacturerCode': String(), 'ShowInPart3Only': Boolean(), 'AircraftDescription': Enum(['Helicopter', 'SeaPlane', 'LandPlane', 'Tiltrotor', 'Gyrocopter', 'Amphibian']), 'EngineCount': String(), 'EngineType': Enum(['Piston', 'Turboprop/Turboshaft', 'Jet', 'Rocket', 'Electric'])}

Schema for aircraft types dataset.

SCHEMA_MANUFACTURERS

SCHEMA_MANUFACTURERS = {'Code': String(), 'Names': List(String()), 'StateName': String()}

Schema for manufacturers dataset.

URL_BASE_DOC8643

URL_BASE_DOC8643 = 'https://doc8643.icao.int/External'

fetch_aircraft_types

fetch_aircraft_types(client: AsyncClient) -> DataFrame
Source code in src/aerocore/data/aircraft_types.py
63
64
65
async def fetch_aircraft_types(client: httpx.AsyncClient) -> pl.DataFrame:
    df = await _post_and_parse_json(client, f"{URL_BASE_DOC8643}/AircraftTypes")
    return df.cast(SCHEMA_AIRCRAFT_TYPES)  # type: ignore

fetch_manufacturers

fetch_manufacturers(client: AsyncClient) -> DataFrame
Source code in src/aerocore/data/aircraft_types.py
68
69
70
async def fetch_manufacturers(client: httpx.AsyncClient) -> pl.DataFrame:
    df = await _post_and_parse_json(client, f"{URL_BASE_DOC8643}/Manufacturers")
    return df.cast(SCHEMA_MANUFACTURERS)  # type: ignore

airports

List of airports, from ourairports

Requires extras:

  • httpx, polars

SCHEMA_AIRPORTS

SCHEMA_AIRPORTS = {'id': Int32(), 'ident': String(), 'type': String(), 'name': String(), 'latitude_deg': Float32(), 'longitude_deg': Float32(), 'elevation_ft': Int16(), 'continent': String(), 'iso_country': String(), 'iso_region': String(), 'municipality': String(), 'scheduled_service': String(), 'gps_code': String(), 'iata_code': String(), 'local_code': String(), 'home_link': String(), 'wikipedia_link': String(), 'keywords': String()}

Schema for airports dataset.

scan_airports

scan_airports(fp: Path) -> LazyFrame

Lazily load list of airports from parquet file.

Schema: aerocore.data.airports.SCHEMA_AIRPORTS

Source code in src/aerocore/data/airports.py
42
43
44
45
46
47
48
49
50
51
52
def scan_airports(fp: Path) -> pl.LazyFrame:
    """
    Lazily load list of airports from parquet file.

    Schema: [aerocore.data.airports.SCHEMA_AIRPORTS][]
    """
    if not fp.exists():
        raise FileNotFoundError(
            "cannot find airports\nhelp: download it first."
        )
    return pl.scan_parquet(fp, schema=SCHEMA_AIRPORTS)

URL_BASE

URL_BASE = 'https://davidmegginson.github.io/ourairports-data'

fetch_airports

fetch_airports(client: AsyncClient) -> DataFrame

Download all airports from ourairports.

Schema: aerocore.data.airports.SCHEMA_AIRPORTS

Source code in src/aerocore/data/airports.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
async def fetch_airports(client: httpx.AsyncClient) -> pl.DataFrame:
    """
    Download all airports from ourairports.

    Schema: [aerocore.data.airports.SCHEMA_AIRPORTS][]
    """
    response = await client.get(f"{URL_BASE}/airports.csv")
    data = BytesIO(response.content)

    airports = (
        pl.read_csv(
            data, schema=SCHEMA_AIRPORTS, truncate_ragged_lines=True
        ).cast(SCHEMA_AIRPORTS)  # type: ignore
    )
    return airports