Skip to content

Commit 62caa42

Browse files
committed
Merge branch 'main' into NPI-3945-fix-gpsdate-to-datetime
2 parents b2a6d9b + c19a173 commit 62caa42

File tree

5 files changed

+239
-24
lines changed

5 files changed

+239
-24
lines changed

gnssanalysis/gn_frame.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def get_frame_of_day(
3535
):
3636
"""Main function to propagate frame into datetime of interest"""
3737

38-
if isinstance(date_or_j2000, (int, _np.int64)):
38+
if isinstance(date_or_j2000, (int, _np.int64)): # TODO check: np.int64 is meant to be a class not a type
3939
date_J2000 = date_or_j2000
4040
else:
4141
date_J2000 = _gn_datetime.datetime2j2000(_np.datetime64(date_or_j2000))

gnssanalysis/gn_io/sinex.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def _get_valid_stypes(stypes: Union[list[str], set[str]]) -> _List[str]:
327327
"""Returns only stypes in allowed list
328328
Fastest if stypes size is small"""
329329
allowed_stypes = ["EST", "APR", "NEQ"]
330-
stypes = set(stypes) if not isinstance(stypes, set) else stypes
330+
stypes = set(stypes) if not isinstance(stypes, set) else stypes # Convert to set if not one.
331331
ok_stypes = sorted(stypes.intersection(allowed_stypes), key=allowed_stypes.index) # need EST to always be first
332332
if len(ok_stypes) != len(stypes):
333333
not_ok_stypes = stypes.difference(allowed_stypes)
@@ -544,12 +544,17 @@ def _get_snx_vector(
544544
if isinstance(path_or_bytes, str):
545545
path = path_or_bytes
546546
snx_bytes = _gn_io.common.path2bytes(path)
547-
# TODO Removed this very broken code path, not sure what happened
548-
# elif isinstance(path_or_bytes, list):
549-
# path, stypes, format, verbose = path_or_bytes
550-
# snx_bytes = _gn_io.common.path2bytes(path)
551-
else:
547+
# Very weird code path, should be removed if possible
548+
elif isinstance(path_or_bytes, list):
549+
_logging.error(
550+
f"path_or_bytes was a list! Using legacy code path. Please update this! Input values: {path_or_bytes}"
551+
)
552+
path, stypes, format, verbose = path_or_bytes
553+
snx_bytes = _gn_io.common.path2bytes(path)
554+
elif isinstance(path_or_bytes, bytes):
552555
snx_bytes = path_or_bytes
556+
else:
557+
raise ValueError(f"Unexpected type for path_or_bytes: {type(path_or_bytes)}. Value: {path_or_bytes}")
553558

554559
if snx_header == {}:
555560
snx_header = _get_snx_header(
@@ -560,7 +565,9 @@ def _get_snx_vector(
560565
"Indices are likely inconsistent between ESTIMATE and APRIORI in the EMR AC files hence files might be parsed incorrectly"
561566
)
562567

568+
_logging.info(f"Passing stypes through SType validator: {stypes}. Input path if available: {path}")
563569
stypes = _get_valid_stypes(stypes) # EST is always first as APR may have skips
570+
_logging.info(f"STypes after validator: {stypes}. Input path if available: {path}")
564571

565572
extracted = _snx_extract(snx_bytes=snx_bytes, stypes=stypes, obj_type="VECTOR", verbose=verbose)
566573
if extracted is None:
@@ -764,7 +771,7 @@ def _get_snx_vector_gzchunks(filename: str, block_name="SOLUTION/ESTIMATE", size
764771
stop = True
765772
i += 1
766773

767-
return _get_snx_vector(path_or_bytes=block_bytes, stypes=set("EST"), format=format)
774+
return _get_snx_vector(path_or_bytes=block_bytes, stypes=set(["EST"]), format=format)
768775

769776

770777
def _get_snx_id(path):

gnssanalysis/gn_io/sp3.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,35 +1133,30 @@ def parse_sp3_header(header: bytes, warn_on_negative_sv_acc_values: bool = True)
11331133

11341134
def clean_sp3_orb(sp3_df: _pd.DataFrame, use_offline_sat_removal: bool) -> _pd.DataFrame:
11351135
"""
1136-
Clean SP3 orbit data in order to remove duplicates, leading and ending, and/or any satellites with nodata values
1137-
elsewhere in the DataFrame.
1136+
Clean SP3 orbit data: remove duplicates, remove leading or trailing rows of NA values, optionally remove satellites
1137+
with *any* missing position values.
11381138
11391139
:param _pd.DataFrame sp3_df: The input SP3 DataFrame.
11401140
:param bool use_offline_sat_removal: Flag indicating whether to remove satellites which are offline / have some
11411141
nodata position values.
1142-
:return _pd.Series: A pandas Series containing the parsed information from the SP3 header.
1142+
:return _pd.DataFrame: A cleaned version of the SP3 DataFrame
11431143
"""
1144-
sp3_df = sp3_df.filter(items=[("EST", "X"), ("EST", "Y"), ("EST", "Z")])
1144+
# Trim DataFrame to position estimate columns
1145+
sp3_df_updated: _pd.DataFrame = sp3_df.filter(items=[("EST", "X"), ("EST", "Y"), ("EST", "Z")])
11451146

11461147
# Drop any duplicates in the index
1147-
sp3_df = sp3_df[~sp3_df.index.duplicated(keep="first")]
1148+
sp3_df_updated = sp3_df_updated[~sp3_df_updated.index.duplicated(keep="first")]
11481149

11491150
# Trim the leading and ending epochs that are empty (i.e. all values are NaN) to avoid dropping all data
1150-
valid_rows = sp3_df.dropna(how="all")
1151+
valid_rows = sp3_df_updated.dropna(how="all")
11511152
first_valid_epoch = valid_rows.index[0][0]
11521153
last_valid_epoch = valid_rows.index[-1][0]
1153-
sp3_df = sp3_df.loc[first_valid_epoch:last_valid_epoch]
1154-
sp3_df_cleaned = sp3_df
1154+
sp3_df_updated = sp3_df_updated.loc[first_valid_epoch:last_valid_epoch]
11551155

1156-
# Drop any satellites (SVs) which are offline or partially offline.
1157-
# Note: this currently removes SVs with ANY nodata values for position, so a single glitch will remove
1158-
# the SV from the whole file.
1159-
# This step was added after velocity interpolation failures due to non-finite (NaN) values from offline SVs.
11601156
if use_offline_sat_removal:
1161-
sp3_baseline = remove_offline_sats(sp3_baseline, df_friendly_name="baseline")
1162-
sp3_test = remove_offline_sats(sp3_test, df_friendly_name="test")
1157+
sp3_df_updated = remove_offline_sats(sp3_df_updated)
11631158

1164-
return sp3_df_cleaned
1159+
return sp3_df_updated
11651160

11661161

11671162
def getVelSpline(sp3Df: _pd.DataFrame) -> _pd.DataFrame:

tests/test_datasets/sp3_test_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
EOF
5959
"""
6060

61-
# Based on SP3c.txt example 2. SP3d is PDF formatted so alignment is hard to preserve.
61+
# Based on SP3c.txt example 2. SP3d is PDF formatted so alignment is hard to preserve. #TODO check this is actually right
6262
# https://files.igs.org/pub/data/format/sp3c.txt
6363
# Truncated and manually modified to reflect:
6464
# Epochs: 1

tests/test_sp3.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,186 @@ def test_read_sp3_correct_svs_read_when_ev_ep_present(self, mock_file):
197197
# TODO Add test(s) for correctly reading header fundamentals (ACC, ORB_TYPE, etc.)
198198
# TODO add tests for correctly reading the actual content of the SP3 in addition to the header.
199199

200+
@staticmethod
201+
def get_example_dataframe(template_name: str = "normal", include_simple_header: bool = True) -> pd.DataFrame:
202+
203+
dataframe_templates = {
204+
# "normal": { # TODO fill in
205+
# "data_vals": [],
206+
# "index_vals": [],
207+
# },
208+
"dupe_epoch_offline_sat_empty_epoch": {
209+
"data_vals": [
210+
# Epoch 1 ---------------------------------
211+
# EST, X EST, Y EST, Z
212+
[4510.358405, -23377.282442, -11792.723580], # --- < G01
213+
[4510.358405, -23377.282442, -11792.723580], # --- < G02
214+
[0.000000, 0.000000, 0.000000], # ---------------- < G03 (offline)
215+
# Epoch 2 ---------------------------------
216+
[4510.358405, -23377.282442, -11792.723580], # --- < G01
217+
[4510.358405, -23377.282442, -11792.723580], # --- < G02
218+
[0.000000, 0.000000, 0.000000], # ---------------- < G03 (offline)
219+
# Epoch 3 --------------------------------- Effectively missing epoch, to test trimming.
220+
[np.nan, np.nan, np.nan],
221+
[np.nan, np.nan, np.nan],
222+
[np.nan, np.nan, np.nan],
223+
],
224+
"index_vals": [[774619200, 774619200, 774619201], ["G01", "G02", "G03"]],
225+
},
226+
"offline_sat_nan": {
227+
"data_vals": [
228+
# Epoch 1 ---------------------------------
229+
# EST, X EST, Y EST, Z
230+
[4510.358405, -23377.282442, -11792.723580], # --- < G01
231+
[4510.358405, -23377.282442, -11792.723580], # --- < G02
232+
[np.nan, np.nan, np.nan], # ---------------- < G03 (offline)
233+
# Epoch 2 ---------------------------------
234+
[4510.358405, -23377.282442, -11792.723580], # --- < G01
235+
[4510.358405, -23377.282442, -11792.723580], # --- < G02
236+
[np.nan, np.nan, np.nan], # ---------------- < G03 (offline)
237+
# Epoch 3 ---------------------------------
238+
[4510.358405, -23377.282442, -11792.723580],
239+
[4510.358405, -23377.282442, -11792.723580],
240+
[np.nan, np.nan, np.nan],
241+
],
242+
"index_vals": [[774619200, 774619200, 774619201], ["G01", "G02", "G03"]],
243+
},
244+
"offline_sat_zero": {
245+
"data_vals": [
246+
# Epoch 1 ---------------------------------
247+
# EST, X EST, Y EST, Z
248+
[4510.358405, -23377.282442, -11792.723580], # --- < G01
249+
[4510.358405, -23377.282442, -11792.723580], # --- < G02
250+
[0.000000, 0.000000, 0.000000], # ---------------- < G03 (offline)
251+
# Epoch 2 ---------------------------------
252+
[4510.358405, -23377.282442, -11792.723580], # --- < G01
253+
[4510.358405, -23377.282442, -11792.723580], # --- < G02
254+
[0.000000, 0.000000, 0.000000], # ---------------- < G03 (offline)
255+
# Epoch 3 ---------------------------------
256+
[4510.358405, -23377.282442, -11792.723580],
257+
[4510.358405, -23377.282442, -11792.723580],
258+
[0.000000, 0.000000, 0.000000],
259+
],
260+
"index_vals": [[774619200, 774619200, 774619201], ["G01", "G02", "G03"]],
261+
},
262+
}
263+
264+
if template_name not in dataframe_templates:
265+
raise ValueError(f"Unsupported template name: {template_name}")
266+
267+
# Worked example for defining MultiIndex
268+
# # Build a MultiIndex of J2000 then PRN values
269+
# # ----------------------------- Epochs: ---------- | PRNs within each of those Epochs:
270+
# # ------------------ Epoch 1 -- Epoch 2 -- Epoch 3 - PRN 1 PRN 2 PRN 3
271+
# index_elements = [[774619200, 774619200, 774619201], ["G01", "G02", "G03"]]
272+
273+
# Define columns: top level 'EST' and nested under that, 'X', 'Y', 'Z'
274+
frame_columns = [["EST", "EST", "EST"], ["X", "Y", "Z"]]
275+
276+
# Load template
277+
template = dataframe_templates[template_name]
278+
frame_data = template["data_vals"]
279+
index_elements = template["index_vals"]
280+
281+
index_names = ["J2000", "PRN"]
282+
multi_index = pd.MultiIndex.from_product(index_elements, names=index_names)
283+
284+
# Compose it all into a DataFrame
285+
df = pd.DataFrame(frame_data, index=multi_index, columns=frame_columns)
286+
287+
if include_simple_header:
288+
# Build SV table
289+
head_svs = ["G01", "G02", "G03"] # SV header entries
290+
head_svs_std = [0, 0, 0] # Accuracy codes for those SVs
291+
sv_tbl = pd.Series(head_svs_std, index=head_svs)
292+
293+
# Build header
294+
header_array = np.asarray(
295+
[
296+
"d",
297+
"P",
298+
"Time TODO",
299+
"3", # Num epochs
300+
"Data TODO",
301+
"coords TODO",
302+
"orb type TODO",
303+
"GAA",
304+
"SP3", # Probably
305+
"Time sys TODO",
306+
"3", # Stated SVs
307+
]
308+
).astype(str)
309+
sp3_heading = pd.Series(
310+
data=header_array,
311+
index=[
312+
"VERSION",
313+
"PV_FLAG",
314+
"DATETIME",
315+
"N_EPOCHS",
316+
"DATA_USED",
317+
"COORD_SYS",
318+
"ORB_TYPE",
319+
"AC",
320+
"FILE_TYPE",
321+
"TIME_SYS",
322+
"SV_COUNT_STATED",
323+
],
324+
)
325+
326+
# Merge SV table and header, and store as 'HEADER'
327+
df.attrs["HEADER"] = pd.concat([sp3_heading, sv_tbl], keys=["HEAD", "SV_INFO"], axis=0)
328+
return df
329+
330+
def test_clean_sp3_orb(self):
331+
"""
332+
Tests cleaning an SP3 DataFrame of duplicates, leading or trailing nodata values, and offline sats
333+
"""
334+
335+
# Create dataframe manually, as read function does deduplication itself. This also makes the test more self-contained
336+
sp3_df = TestSP3.get_example_dataframe("dupe_epoch_offline_sat_empty_epoch")
337+
338+
self.assertTrue(
339+
# Alterantively you can use all(array == array) to do an elementwise equality check
340+
np.array_equal(sp3_df.index.get_level_values(0).unique(), [774619200, 774619201]),
341+
"Sample data should have 2 unique epochs (one of which is empty)",
342+
)
343+
self.assertTrue(
344+
np.array_equal(sp3_df.index.get_level_values(1).unique(), ["G01", "G02", "G03"]),
345+
"Sample data should have 3 sats",
346+
)
347+
348+
# There should be duplicates of each sat in the first epoch
349+
# Note: syntax of loc here uses a tuple describing levels within the row MultiIndex, then column MultiIndex,
350+
# i.e. (row, row), (column, column).
351+
self.assertTrue(
352+
np.array_equal(sp3_df.loc[(774619200, "G01"), ("EST", "X")].values, [4510.358405, 4510.358405]),
353+
"Expect dupe in first epoch",
354+
)
355+
356+
# Test cleaning function without offline sat removal
357+
sp3_df_no_offline_removal = sp3.clean_sp3_orb(sp3_df, False)
358+
359+
self.assertTrue(
360+
np.array_equal(sp3_df_no_offline_removal.index.get_level_values(0).unique(), [774619200]),
361+
"After cleaning there should be a single unique epoch",
362+
)
363+
364+
# This checks both (indirectly) that there is only one epoch (as the multi-index will repeat second level
365+
# values, and the input doesn't change sats in successive epochs), and that those second level values
366+
# aren't duplicated.
367+
self.assertTrue(
368+
np.array_equal(sp3_df_no_offline_removal.index.get_level_values(1), ["G01", "G02", "G03"]),
369+
"After cleaning there should be no dupe PRNs. As offline sat removal is off, offline sat should remain",
370+
)
371+
372+
# Now check with offline sat removal enabled too
373+
sp3_df_with_offline_removal = sp3.clean_sp3_orb(sp3_df, True)
374+
# Check that we still seem to have one epoch with no dupe sats, and now with the offline sat removed
375+
self.assertTrue(
376+
np.array_equal(sp3_df_with_offline_removal.index.get_level_values(1), ["G01", "G02"]),
377+
"After cleaning there should be no dupe PRNs (and with offline removal, offline sat should be gone)",
378+
)
379+
200380
def test_gen_sp3_fundamentals(self):
201381
"""
202382
Tests that the SP3 header and content generation functions produce output that (apart from trailing
@@ -737,6 +917,39 @@ def test_velinterpolation(self, mock_file):
737917
self.assertIsNotNone(r)
738918
self.assertIsNotNone(r2)
739919

920+
def test_sp3_offline_sat_removal_standalone(self):
921+
"""
922+
Standalone test for remove_offline_sats() using manually constructed DataFrame to
923+
avoid dependency on read_sp3()
924+
"""
925+
sp3_df_nans = TestSP3.get_example_dataframe("offline_sat_nan")
926+
sp3_df_zeros = TestSP3.get_example_dataframe("offline_sat_zero")
927+
928+
self.assertEqual(
929+
sp3_df_zeros.index.get_level_values(1).unique().array.tolist(),
930+
["G01", "G02", "G03"],
931+
"Should start with 3 SVs",
932+
)
933+
self.assertEqual(
934+
sp3_df_nans.index.get_level_values(1).unique().array.tolist(),
935+
["G01", "G02", "G03"],
936+
"Should start with 3 SVs",
937+
)
938+
939+
sp3_df_zeros_removed = sp3.remove_offline_sats(sp3_df_zeros)
940+
sp3_df_nans_removed = sp3.remove_offline_sats(sp3_df_nans)
941+
942+
self.assertEqual(
943+
sp3_df_zeros_removed.index.get_level_values(1).unique().array.tolist(),
944+
["G01", "G02"],
945+
"Should be two SVs after removing offline ones",
946+
)
947+
self.assertEqual(
948+
sp3_df_nans_removed.index.get_level_values(1).unique().array.tolist(),
949+
["G01", "G02"],
950+
"Should be two SVs after removing offline ones",
951+
)
952+
740953
@patch("builtins.open", new_callable=mock_open, read_data=offline_sat_test_data)
741954
def test_sp3_offline_sat_removal(self, mock_file):
742955
sp3_df = sp3.read_sp3("mock_path", pOnly=False)

0 commit comments

Comments
 (0)