Skip to content

Commit aa64213

Browse files
committed
Merge branch 'main' into NPI-3500-misc-code-cleanup
2 parents 1f80ab8 + c19a173 commit aa64213

35 files changed

+5204
-9248
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ __pycache__
1111
# IDE specific details
1212
.vscode
1313
.idea
14+
1415
# Other files
1516
scratch/

gnssanalysis/filenames.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def nominal_span_string(span_seconds: float) -> str:
378378
# That is, if a span is longer than a day, then we will ignore any deviation from an
379379
# integer day that is smaller than an hour. But a time of 2 days, 3 hours and 30
380380
# minutes will be reported as 27 hours.
381-
# If this would result in more than 99 periods, we return the 00U invalid code instead.
381+
# If this would result in a value above 99 in the determined unit, we return the 00U invalid code instead.
382382
# We ignore months, because they're a little weird and not overly helpful.
383383
if span_seconds >= sec_in_year:
384384
if (span_seconds % sec_in_year) < gn_const.SEC_IN_WEEK:
@@ -619,10 +619,10 @@ def determine_snx_name_props(file_path: pathlib.Path) -> Dict[str, Any]:
619619
if "SOLUTION/EPOCHS" in snx_blocks:
620620
with open(file_path, mode="rb") as f:
621621
blk = gn_io.sinex._snx_extract_blk(f.read(), "SOLUTION/EPOCHS")
622-
if blk:
622+
if blk is not None:
623623
soln_df = pd.read_csv(
624624
io.BytesIO(blk[0]),
625-
delim_whitespace=True,
625+
sep="\\s+", # delim_whitespace is deprecated
626626
comment="*",
627627
names=["CODE", "PT", "SOLN", "T", "START_EPOCH", "END_EPOCH", "MEAN_EPOCH"],
628628
converters={
@@ -754,7 +754,7 @@ def determine_properties_from_filename(filename: str) -> Dict[str, Any]:
754754
basename,
755755
re.VERBOSE,
756756
)
757-
if long_match:
757+
if long_match is not None:
758758
return {
759759
"analysis_center": long_match["analysis_center"].upper(),
760760
"content_type": long_match["content_type"].upper(),
@@ -776,6 +776,8 @@ def determine_properties_from_filename(filename: str) -> Dict[str, Any]:
776776
"project": long_match["project"],
777777
}
778778
else:
779+
# TODO we could add support for raising an exception in this case, if a long filename was expected. This
780+
# could include use of StrictMode
779781
logging.captureWarnings(True) # Probably unnecessary, but for safety's sake...
780782
warnings.warn(
781783
"(Via warnings system) Extracting long filename properties (via regex) failed. "

gnssanalysis/gn_datetime.py

Lines changed: 137 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Base time conversion functions"""
22

3+
import logging
4+
35
from datetime import datetime as _datetime
6+
from datetime import date as _date
47
from datetime import timedelta as _timedelta
58
from io import StringIO as _StringIO
69
from typing import Optional, overload, Union
@@ -11,6 +14,9 @@
1114
from . import gn_const as _gn_const
1215

1316

17+
logger = logging.getLogger(__name__)
18+
19+
1420
def gpsweekD(yr, doy, wkday_suff=False):
1521
"""
1622
Convert year, day-of-year to GPS week format: WWWWD or WWWW
@@ -58,57 +64,71 @@ class GPSDate:
5864
Usage:
5965
today = GPSDate("today")
6066
tomorrow = today.next
61-
print(f"today year: {today.year}, doy: {today.dy}, GPS week and weekday: {today.gpswkD}")
62-
print(f"tomorrow year: {tomorrow.year}, doy: {tomorrow.dy}, GPS week and weekday: {tomorrow.gpswkD}")
67+
print(f"today year: {today.year}, doy: {today.day_of_year}, GPS week and weekday: {today.gps_week_and_day_of_week}")
68+
print(f"tomorrow year: {tomorrow.year}, doy: {tomorrow.day_of_year}, GPS week and weekday: {tomorrow.gps_week_and_day_of_week}")
6369
"""
6470

65-
def __init__(self, ts: _np.datetime64):
66-
if isinstance(ts, str):
67-
ts = _np.datetime64(ts)
71+
# For compatibility, we have accessors called 'ts' and 'timestamp'.
72+
_internal_dt64: _np.datetime64
73+
74+
def __init__(self, time: Union[_np.datetime64, _datetime, _date, str]):
75+
if isinstance(time, _np.datetime64):
76+
self._internal_dt64 = time
77+
elif isinstance(time, (_datetime, _date, str)):
78+
# Something we may be able to convert to a datetime64
79+
self._internal_dt64 = _np.datetime64(time)
80+
else:
81+
raise TypeError("GPSDate() takes only Numpy datetime64, datetime, date, or str representation of a date")
82+
83+
@property
84+
def get_datetime64(self) -> _np.datetime64:
85+
return self._internal_dt64
6886

69-
self.ts = ts
87+
@property # For backwards compatibility
88+
def ts(self) -> _np.datetime64:
89+
return self.get_datetime64
7090

7191
@property
72-
def as_datetime(self):
73-
"""Convert to Python `datetime` object."""
74-
return self.ts.astype(_datetime)
92+
def as_datetime(self) -> _datetime:
93+
"""Convert to Python `datetime` object. Note that as GPSDate has no hour, neither will this datetime"""
94+
return _pd.Timestamp(self._internal_dt64).to_pydatetime()
7595

7696
@property
77-
def yr(self):
97+
def yr(self) -> str:
7898
"""Year"""
7999
return self.as_datetime.strftime("%Y")
80100

81101
@property
82-
def dy(self):
102+
def dy(self) -> str:
83103
"""Day of year"""
84104
return self.as_datetime.strftime("%j")
85105

86106
@property
87-
def gpswk(self):
107+
def gpswk(self) -> str:
88108
"""GPS week"""
89109
return gpsweekD(self.yr, self.dy, wkday_suff=False)
90110

91111
@property
92-
def gpswkD(self):
112+
def gpswkD(self) -> str:
93113
"""GPS week with weekday suffix"""
94114
return gpsweekD(self.yr, self.dy, wkday_suff=True)
95115

96116
@property
97117
def next(self):
98118
"""The following day"""
99-
return GPSDate(self.ts + 1)
119+
return GPSDate(self._internal_dt64 + 1)
100120

101121
@property
102122
def prev(self):
103123
"""The previous day"""
104-
return GPSDate(self.ts - 1)
124+
return GPSDate(self._internal_dt64 - 1)
105125

106-
def __str__(self):
126+
def __str__(self) -> str:
107127
"""Same string representation as the underlying numpy datetime64 object"""
108-
return str(self.ts)
128+
return str(self._internal_dt64)
109129

110130

111-
def dt2gpswk(dt, wkday_suff=False, both=False):
131+
def dt2gpswk(dt, wkday_suff=False, both=False) -> Union[str, tuple[str, str]]:
112132
"""
113133
Convert the given datetime object to a GPS week (option to include day suffix)
114134
"""
@@ -120,13 +140,41 @@ def dt2gpswk(dt, wkday_suff=False, both=False):
120140
return gpsweekD(yr, doy, wkday_suff=False), gpsweekD(yr, doy, wkday_suff=True)
121141

122142

123-
def gpswkD2dt(gpswkD):
143+
# TODO DEPRECATED
144+
def gpswkD2dt(gpswkD: str) -> _datetime:
124145
"""
125-
Convert from GPS-Week-Day (WWWWDD) format to datetime object
146+
DEPRECATED. This is a compatibility wrapper. Use gps_week_day_to_datetime() instead.
147+
"""
148+
return gps_week_day_to_datetime(gpswkD)
149+
150+
151+
def gps_week_day_to_datetime(gps_week_and_weekday: str) -> _datetime:
152+
"""
153+
Convert from GPS-Week-Day (including day: WWWWD or just week: WWWW) format to datetime object.
154+
If a day-of-week is not provided, 0 (Sunday) is assumed.
155+
156+
param: str gps_week_and_weekday: A date expressed in GPS weeks with (optionally) day-of-week.
157+
returns _datetime: The date expressed as a datetime.datetime object
126158
"""
127-
if type(gpswkD) != str:
128-
gpswkD = str(gpswkD)
129-
dt_64 = _gn_const.GPS_ORIGIN + _np.timedelta64(int(gpswkD[:-1]), "W") + _np.timedelta64(int(gpswkD[-1]), "D")
159+
if not isinstance(gps_week_and_weekday, str):
160+
raise TypeError("GPS Week-Day must be a string")
161+
162+
if not len(gps_week_and_weekday) in (4, 5):
163+
raise ValueError(
164+
"GPS Week-Day must be either a 4 digit week (WWWW), or a 4 digit week plus 1 digit day-of-week (WWWWD)"
165+
)
166+
167+
if len(gps_week_and_weekday) == 5: # GPS week with day-of-week (WWWWD)
168+
# Split into 4 digit week number, and 1 digit day number.
169+
# Parse each as a time delta and add them to the GPS origin date.
170+
dt_64 = (
171+
_gn_const.GPS_ORIGIN
172+
+ _np.timedelta64(int(gps_week_and_weekday[:-1]), "W")
173+
+ _np.timedelta64(int(gps_week_and_weekday[-1]), "D")
174+
)
175+
else: # 4 digit week, no trimming needed
176+
dt_64 = _gn_const.GPS_ORIGIN + _np.timedelta64(int(gps_week_and_weekday), "W")
177+
130178
return dt_64.astype(_datetime)
131179

132180

@@ -256,10 +304,10 @@ def mjd2datetime(mjd: _np.ndarray, seconds_frac: _np.ndarray, pea_partials=False
256304

257305

258306
def mjd_to_pydatetime(mjd: float) -> _datetime:
259-
"""Convert python datetime object to corresponding Modified Julian Date
307+
"""Convert Modified Julian Date to corresponding python datetime object
260308
261-
:param datetime.datetime dt: Python datetime of interest
262-
:return float: Corresponding Modified Julian Date
309+
:param float mjd: Modified Julian Date of interest
310+
:return datetime.datetime: Corresponding Python datetime
263311
"""
264312
mjd_epoch_dt = _datetime(2000, 1, 1)
265313
return mjd_epoch_dt + _timedelta(days=mjd - 51544.00)
@@ -270,11 +318,72 @@ def mjd2j2000(mjd: _np.ndarray, seconds_frac: _np.ndarray, pea_partials=False) -
270318
return datetime2j2000(datetime)
271319

272320

321+
def j2000_to_igs_dt(j2000_secs: _np.ndarray) -> _np.ndarray:
322+
"""
323+
Converts array of j2000 times to format string representation used by many IGS formats including Rinex and SP3.
324+
E.g. 674913600 -> '2021-05-22T00:00:00' -> '2021 5 22 0 0 0.00000000'
325+
:param _np.ndarray j2000_secs: Numpy NDArray of (typically epoch) times in J2000 seconds.
326+
:return _np.ndarray: Numpy NDArray with those same times as strings.
327+
"""
328+
datetime = j20002datetime(j2000_secs)
329+
year = datetime.astype("datetime64[Y]")
330+
month = datetime.astype("datetime64[M]")
331+
day = datetime.astype("datetime64[D]")
332+
hour = datetime.astype("datetime64[h]")
333+
minute = datetime.astype("datetime64[m]")
334+
335+
date_y = _pd.Series(year.astype(str)).str.rjust(4).values
336+
date_m = _pd.Series(((month - year).astype("int64") + 1).astype(str)).str.rjust(3).values
337+
date_d = _pd.Series(((day - month).astype("int64") + 1).astype(str)).str.rjust(3).values
338+
339+
time_h = _pd.Series((hour - day).astype("int64").astype(str)).str.rjust(3).values
340+
time_m = _pd.Series((minute - hour).astype("int64").astype(str)).str.rjust(3).values
341+
# Width 12 due to one extra leading space (for easier concatenation next), then _0.00000000 format per SP3d spec:
342+
time_s = (_pd.Series((datetime - minute)).view("int64") / 1e9).apply("{:.8f}".format).str.rjust(12).values
343+
return date_y + date_m + date_d + time_h + time_m + time_s
344+
345+
346+
def j2000_to_igs_epoch_row_header_dt(j2000_secs: _np.ndarray) -> _np.ndarray:
347+
"""
348+
Utility wrapper function to format J2000 time values (typically epoch values) to be written as epoch header lines
349+
within the body of SP3, Rinex, etc. files.
350+
E.g. 674913600 -> '2021-05-22T00:00:00' -> '* 2021 5 22 0 0 0.00000000\n'
351+
:param _np.ndarray j2000_secs: Numpy NDArray of (typically epoch) times in J2000 seconds.
352+
:return _np.ndarray: Numpy NDArray with those same times as strings, including epoch line lead-in and newline.
353+
"""
354+
# Add leading "* "s and trailing newlines around all values
355+
return "* " + j2000_to_igs_dt(j2000_secs) + "\n"
356+
357+
358+
def j2000_to_sp3_head_dt(j2000secs: _np.ndarray) -> str:
359+
"""
360+
Utility wrapper function to format a J2000 time value for the SP3 header. Takes NDArray, but only expects one value
361+
in it.
362+
:param _np.ndarray j2000_secs: Numpy NDArray containing a *single* time value in J2000 seconds.
363+
:return str: The provided time value as a string.
364+
"""
365+
formatted_times = j2000_to_igs_dt(j2000secs)
366+
367+
# If making a header there should be one value. If not it's a mistake, or at best inefficient.
368+
if len(formatted_times) != 1:
369+
logger.warning(
370+
"More than one time value passed through. This function is meant to be used to format a single value "
371+
"in the SP3 header."
372+
)
373+
return formatted_times[0]
374+
375+
376+
# TODO DEPRECATED.
273377
def j20002rnxdt(j2000secs: _np.ndarray) -> _np.ndarray:
274378
"""
275-
Converts j2000 array to rinex format string representation
379+
DEPRECATED since about version 0.0.58
380+
TODO remove in version 0.0.59
381+
Converts array of j2000 times to rinex format string representation
382+
NOTE: the following is incorrect by SP3d standard; there should be another space before the seconds.
276383
674913600 -> '2021-05-22T00:00:00' -> '* 2021 5 22 0 0 0.00000000\n'
277384
"""
385+
logger.warning("j20002rnxdt() is deprecated. Please use j2000_to_igs_epoch_row_header_dt() instead.")
386+
278387
datetime = j20002datetime(j2000secs)
279388
year = datetime.astype("datetime64[Y]")
280389
month = datetime.astype("datetime64[M]")
@@ -288,6 +397,7 @@ def j20002rnxdt(j2000secs: _np.ndarray) -> _np.ndarray:
288397

289398
time_h = _pd.Series((hour - day).astype("int64").astype(str)).str.rjust(3).values
290399
time_m = _pd.Series((minute - hour).astype("int64").astype(str)).str.rjust(3).values
400+
# NOTE: The following may be wrong by the SP3d spec. Again, please use j2000_to_igs_epoch_row_header_dt() instead.
291401
time_s = (_pd.Series((datetime - minute)).view("int64") / 1e9).apply("{:.8f}\n".format).str.rjust(13).values
292402
return date_y + date_m + date_d + time_h + time_m + time_s
293403

0 commit comments

Comments
 (0)