Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
75f7e0c
WIP develop test for satellite health flag
Jun 4, 2025
4dbce52
Update test_health_flag
Jun 5, 2025
b028caa
Add test_health_flag in test_evaluate
Jun 11, 2025
c0d7419
Add function extract_health_flag
Jun 11, 2025
1713753
update on health_flag tests
Jun 13, 2025
ab68b08
change query creation to use direcly RINEX OBS file (instead of prx f…
plutonheaven Jun 17, 2025
34c76e5
add "health_flag" to sp3.compute
plutonheaven Jun 17, 2025
ba173af
minor
plutonheaven Jun 17, 2025
3f0cf3a
Merge remote-tracking branch 'jtec/main' into 202506_Satellite_Health
eulaliesa Jun 19, 2025
93b3dc2
downloaded sp3 files
eulaliesa Jul 9, 2025
73924f8
Created a folder 'precise_corrections' contianing the folder 'sp3'.
eulaliesa Jul 23, 2025
91cdf29
Files moved from the folder /prx/sp3 to the folder /prx/precise_corre…
eulaliesa Jul 23, 2025
e00fd68
created a test to check that the priority of downloading is respected…
eulaliesa Jul 23, 2025
074dfea
modify priority so that MGX files are chosen + add clk files
eulaliesa Jul 23, 2025
eacc75a
Download files for tests
eulaliesa Jul 23, 2025
3135ec8
Update tests
eulaliesa Jul 23, 2025
3541dac
Add comments for tests and priority list
eulaliesa Jul 24, 2025
74c39a1
- remove untracked files
plutonheaven Jul 25, 2025
cbfe8d4
- ruff
plutonheaven Jul 25, 2025
217888b
- move function timestamp_to_gps_week_and_dow to util.py module
plutonheaven Jul 25, 2025
8c6dd91
- update test files
plutonheaven Jul 25, 2025
24cb7a7
- defined function to check online availability without downloading a…
plutonheaven Jul 25, 2025
1534cbe
- Add typing
plutonheaven Jul 26, 2025
c6daf13
Merge branch 'main' into 202507_sp3_file_discovery
plutonheaven Jul 27, 2025
ba31889
- specify error in try/catch statement
plutonheaven Jul 27, 2025
4a078a6
created files to discover antex files and for the tests
eulaliesa Jul 29, 2025
b86ec99
Add tests for ANTEX file download logic based on GPS week
eulaliesa Jul 29, 2025
4655518
Add functions `ecef_2_satellite` and `compute_sun_ecef_position`
eulaliesa Jul 30, 2025
ac18a28
Completed test coverage and refactored code
eulaliesa Aug 1, 2025
31e4109
ruff format
eulaliesa Aug 1, 2025
a3fc621
Merge branch 'main' into 202507_sp3_file_discovery
jtec Dec 2, 2025
8f8deee
Merge remote-tracking branch 'refs/remotes/origin/main' into 202507_s…
plutonheaven Jan 16, 2026
880510e
Merge remote-tracking branch 'refs/remotes/origin/main' into 202507_a…
plutonheaven Jan 16, 2026
3366206
Merge branch 'refs/heads/202507_sp3_file_discovery' into 202507_antex…
plutonheaven Jan 16, 2026
8c18065
ruff
plutonheaven Jan 16, 2026
6aeabb1
ruff
plutonheaven Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ __pycache__
**/tmp_test*/*
**.profile
**.pstat
src/prx/sp3/sp3_files/*
src/prx/precise_corrections/sp3/sp3_files/2023/001/IGS0OPSFIN_20230010000_01D_15M_ORB.SP3
162 changes: 162 additions & 0 deletions src/prx/precise_corrections/antex/antex_file_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from ftplib import FTP
import ftplib
from pathlib import Path
import re

import georinex
import pandas as pd
import urllib
import fnmatch

from prx import converters
from prx import util
from prx.util import timestamp_to_mid_day

"""
Logic overview:
- The code first extracts the GPS week from the provided RINEX observation file.
- It then identifies the most recent ANTEX file available both locally and remotely.
- If both local and remote ANTEX files are the same, the local version is reused, as the local database is considered up to date.
- Otherwise, the latest remote file is downloaded and used.
- Note: If the most recent ANTEX file has a GPS week that is earlier than the observation file's GPS week
(e.g., the remote database has not yet been updated), a warning is displayed to indicate that the most recent ANTEX file
was still used despite being older than the observation file.
"""

log = util.get_logger(__name__)

atx_filename = "igs20_????.atx"


def date_to_gps_week(date: pd.Timestamp):
"""
Convert a Timestamp to GPS week

GPS week starts on 01/06/1980 (sunday)
"""
gps_epoch = pd.Timestamp("1980-01-06T00:00:00Z")
delta = date.tz_localize("UTC") - gps_epoch
gps_week = delta.days // 7
return gps_week


def extract_gps_week(filename: str) -> int:
"""
Extract GPS week from filenames like 'igsxx_WWWW.atx'.
"""
match = re.search(r"igs...(\d{4})\.atx", filename)
if match:
return int(match.group(1))
return -1


def atx_file_database_folder():
"""
Returns the path to the folder where ATX database files are stored.
"""
db_folder = (
util.prx_repository_root() / "src/prx/precise_corrections/antex/atx_files"
)
db_folder.mkdir(exist_ok=True)
return db_folder


def find_latest_local_antex_file(db_folder=atx_file_database_folder()):
candidates = list(db_folder.glob(atx_filename))
if not candidates:
return None
return max(candidates, key=lambda c: extract_gps_week(c))


def list_ftp_directory(server, folder):
ftp = FTP(server)
ftp.login()
ftp.cwd(folder)
dir_list = []
ftp.dir(dir_list.append)
return [c.split()[-1].strip() for c in dir_list]


def fetch_latest_remote_antex_file():
"""
List the ANTEX files available online and returns the latest
"""
server = "gssc.esa.int"
remote_folder = "/igs/station/general"
candidates = list_ftp_directory(server, remote_folder)
candidates = [c for c in candidates if fnmatch.fnmatch(c, atx_filename)]
if not candidates:
return None
return max(candidates, key=lambda c: extract_gps_week(c))


def check_online_availability(file: str, folder: Path) -> Path | None:
"""
Need to keep the same inputs as try_downloading_sp3_ftp, in order to be able to use `unittest.mock.patch` in tests
"""
server = "gssc.esa.int"
remote_folder = "/igs/station/general"
ftp = FTP(server)
ftp.login()
ftp.cwd(remote_folder)
try:
ftp.size(file)
return folder.joinpath(file)
except ftplib.error_perm:
log.warning(f"{file} not available on {server}")
return None


def try_downloading_atx_ftp(file: str, folder: Path):
"""
Download the wanted remote file
"""
server = "gssc.esa.int"
remote_folder = "/igs/station/general"
ftp_file = f"ftp://{server}/{remote_folder}/{file}"
local_file = folder / file
urllib.request.urlretrieve(ftp_file, local_file)
if not local_file.exists():
log.warning(f"Could not download {ftp_file}")
return None
log.info(f"Downloaded ANTEX file {ftp_file}")
return local_file


def get_atx_file(date: pd.Timestamp, db_folder=atx_file_database_folder()):
gps_week = date_to_gps_week(date)
latest_atx_local = find_latest_local_antex_file(db_folder)
latest_atx_remote = fetch_latest_remote_antex_file()
if latest_atx_remote == latest_atx_local:
return latest_atx_local
elif latest_atx_remote and (
not latest_atx_local
or extract_gps_week(latest_atx_remote) > extract_gps_week(latest_atx_local)
):
# Download the latest file
atx_file = try_downloading_atx_ftp(latest_atx_remote, db_folder)
if atx_file is not None:
if gps_week > extract_gps_week(atx_file.name):
log.warning(
f"No ANTEX file found for the target GPS week {gps_week} — using the most recent available instead."
)
return atx_file

raise FileNotFoundError("No file ANTEX found locally or online.")


def discover_or_download_atx_file(observation_file_path=Path()):
"""
Returns the path to a valid antes file (local or downloaded) corresponding to the observation file.
"""
log.info(f"Finding auxiliary files for {observation_file_path} ...")
rinex_3_obs_file = converters.anything_to_rinex_3(observation_file_path)
header = georinex.rinexheader(rinex_3_obs_file)

t_start = timestamp_to_mid_day(
util.rinex_header_time_string_2_timestamp_ns(header["TIME OF FIRST OBS"])
- pd.Timedelta(200, unit="milliseconds")
)

atx_file = get_atx_file(t_start)
return atx_file
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import os
from pathlib import Path
import shutil
import georinex
import pandas as pd
import pytest
from unittest.mock import patch

import prx
from prx import util
from prx.precise_corrections.antex import antex_file_discovery as atx


@pytest.fixture
def set_up_test():
test_directory = Path(f"./tmp_test_directory_{__name__}").resolve()
if test_directory.exists():
# Make sure the expected files has not been generated before and is still on disk due to e.g. a previous
# test run having crashed:
shutil.rmtree(test_directory)
os.makedirs(test_directory)
test_obs_file = test_directory.joinpath("TLSE00FRA_R_20230010100_10S_01S_MO.crx.gz")
# local_database = atx.atx_file_folder(pd.Timestamp('2023-01-01'), Path('src/prx/precise_corrections/atx/test/datasets'))

shutil.copy(
util.prx_repository_root()
/ f"src/prx/test/datasets/TLSE_2023001/{test_obs_file.name}",
test_obs_file,
)
assert test_obs_file.exists()

yield {"test_obs_file": test_obs_file}
shutil.rmtree(test_directory)


def test_extract_gps_week():
filenames = [
"igs20_2134.atx",
"igs_10.atx",
"igs14_abc.atx",
]
expected_returns = [2134, -1, -1]
gps_week = [atx.extract_gps_week(f) for f in filenames]
assert gps_week == expected_returns


def test_download_if_not_local(set_up_test):
"""
Tests that the ANTEX file is downloaded when no local file is available,
but a valid remote file exists.

Scenario :
- No local ANTEX file available
- A remote ANTEX file exists
-> The function should trigger the download of the remote file.
"""
obs_file = set_up_test["test_obs_file"]
header = georinex.rinexheader(obs_file)
t_start = util.rinex_header_time_string_2_timestamp_ns(
header["TIME OF FIRST OBS"]
) - pd.Timedelta(200, unit="milliseconds")

db_folder = set_up_test["test_obs_file"].parent
downloaded_atx = atx.get_atx_file(t_start, db_folder)

# Ensure the file was downloaded and exists
assert downloaded_atx is not None
assert downloaded_atx.exists()


def test_download_if_remote_is_newer(set_up_test):
"""
Tests that the ANTEX file is downloaded when the remote file has a newer GPS week
than the local one.

Scenario :
- local file = igs20_2370.atx
- remote file = igs20_2375.atx
-> The function should download igs20_2375.atx
"""
latest_local = "igs20_2370.atx"
latest_remote = "igs20_2375.atx"
date = pd.Timestamp("2025-01-01 12:00:00")
db_folder = set_up_test["test_obs_file"].parent

with (
patch( # replace download function by online availability check
"prx.precise_corrections.antex.antex_file_discovery.try_downloading_atx_ftp",
new=prx.precise_corrections.antex.antex_file_discovery.check_online_availability,
),
patch( # simulate that the latest local file found is 'igs20_2370.atx'
"prx.precise_corrections.antex.antex_file_discovery.find_latest_local_antex_file",
return_value=latest_local,
),
patch( # simulate that the latest remote file found is 'igs20_2375.atx'
"prx.precise_corrections.antex.antex_file_discovery.fetch_latest_remote_antex_file",
return_value=latest_remote,
),
):
result = atx.get_atx_file(date, db_folder)
assert result is not None
assert result.name == latest_remote


def test_skip_download_if_same_week(set_up_test):
"""
Tests that the ANTEX file is not downloaded when the remote and local files
are from the same GPS week.

Scenario:
- local file = igs20_2375.atx
- remote file = igs20_2375.atx
→ The function should skip download and return the local file
"""
latest_local = "igs20_2375.atx"
latest_remote = "igs20_2375.atx"
date = pd.Timestamp("2025-01-01 12:00:00")
db_folder = set_up_test["test_obs_file"].parent

with (
patch( # mock download function
"prx.precise_corrections.antex.antex_file_discovery.try_downloading_atx_ftp"
) as mock_download,
patch( # simulate that the latest local file found is 'igs20_2375.atx'
"prx.precise_corrections.antex.antex_file_discovery.find_latest_local_antex_file",
return_value=latest_local,
),
patch( # simulate that the latest remote file found is 'igs20_2375.atx'
"prx.precise_corrections.antex.antex_file_discovery.fetch_latest_remote_antex_file",
return_value=latest_remote,
),
):
result = atx.get_atx_file(date, db_folder)

assert result is not None
# Ensure that the download function was not called
mock_download.assert_not_called()
# Ensure the returned file is the local one
assert result == latest_local
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading