Skip to content

Commit 20d8996

Browse files
authored
Merge pull request #76 from Stanford-NavLab/derek/concat
Derek/concat
2 parents 0240c80 + 897c0b5 commit 20d8996

4 files changed

Lines changed: 296 additions & 52 deletions

File tree

gnss_lib_py/parsers/navdata.py

Lines changed: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,25 @@
1515

1616
class NavData():
1717
"""gnss_lib_py specific class for handling data.
18-
Uses numpy for speed combined with pandas like intuitive indexing
18+
19+
Uses numpy for speed combined with pandas like intuitive indexing.
20+
21+
Can either be initialized empty, with a csv file by setting
22+
``csv_path``, a Pandas DataFrame by setting ``pandas_df`` or by a
23+
Numpy array by setting ``numpy_array``.
24+
25+
Parameters
26+
----------
27+
csv_path : string
28+
Path to csv file containing data
29+
pandas_df : pd.DataFrame
30+
Data used to initialize NavData instance.
31+
numpy_array : np.ndarray
32+
Numpy array containing data used to initialize NavData
33+
instance.
34+
**kwargs : args
35+
Additional arguments (e.g. ``sep`` or ``header``) passed into
36+
``pd.read_csv`` if csv_path is not None.
1937
2038
Attributes
2139
----------
@@ -89,7 +107,8 @@ def from_pandas_df(self, pandas_df):
89107
90108
Parameters
91109
----------
92-
pandas_df : pd.DataFrame of data
110+
pandas_df : pd.DataFrame
111+
Data used to initialize NavData instance.
93112
"""
94113

95114
if not isinstance(pandas_df, pd.DataFrame):
@@ -112,54 +131,112 @@ def from_numpy_array(self, numpy_array):
112131
Parameters
113132
----------
114133
numpy_array : np.ndarray
115-
Numpy array containing data
134+
Numpy array containing data used to initialize NavData
135+
instance.
116136
117137
"""
118138

139+
119140
if not isinstance(numpy_array, np.ndarray):
120141
raise TypeError("numpy_array must be np.ndarray")
121142

143+
122144
self._build_navdata()
123145

146+
numpy_array = np.atleast_2d(numpy_array)
124147
for row_num in range(numpy_array.shape[0]):
125148
self[str(row_num)] = numpy_array[row_num,:]
126149

127-
def add(self, csv_path=None, pandas_df=None, numpy_array=None):
128-
"""Add new timesteps to existing array
150+
def concat(self, navdata=None, axis=1, inplace=False):
151+
"""Concatenates second NavData instance by row or column.
152+
153+
Concatenates a second NavData instance to the existing NavData
154+
instance by either row or column.
155+
156+
Each type of data is included in a row, so adding new rows with
157+
``axis=0``, means adding new types of data. Concat requires that
158+
the new NavData matches the length of the existing NavData. Row
159+
concatenation assumes the same ordering across both NavData
160+
instances (e.g. sorted by timestamp) and does not perform any
161+
matching/sorting itself.
162+
163+
You can also concatenate new columns ``axis=1``. If the row
164+
names of the new NavData instance don't match the row names of
165+
the existing NavData instance, the mismatched values will be
166+
filled with np.nan.
129167
130168
Parameters
131169
----------
132-
csv_path : string
133-
Path to csv file containing data to add
134-
pandas_df : pd.DataFrame
135-
DataFrame containing data to add
136-
numpy_array : np.ndarray
137-
Array containing only numeric data to add
170+
navdata : gnss_lib_py.parsers.navdata.NavData
171+
Navdata instance to concatenate.
172+
axis : int
173+
Either add new rows (type) of data ``axis=0`` or new columns
174+
(e.g. timesteps) of data ``axis=1``.
175+
inplace : bool
176+
If False, will return new concatenated NavData instance.
177+
If True, will concatenate data to the current NavData
178+
instance.
179+
180+
Returns
181+
-------
182+
new_navdata : gnss_lib_py.parsers.navdata.NavData or None
183+
If inplace is False, returns NavData instance after
184+
concatenating specified data. If inplace is True, returns
185+
None.
186+
138187
"""
139-
old_row_num = len(self.map)
140-
old_len = len(self)
141-
new_data_cols = slice(old_len, None)
142-
if numpy_array is not None:
143-
if old_row_num == 0:
144-
self.from_numpy_array(numpy_array)
145-
else:
146-
if len(numpy_array.shape)==1:
147-
numpy_array = np.reshape(numpy_array, [1, -1])
148-
self.array = np.hstack((self.array, np.empty_like(numpy_array,
149-
dtype=self.arr_dtype)))
150-
self[:, new_data_cols] = numpy_array
151-
if csv_path is not None:
152-
if old_row_num == 0:
153-
self.from_csv_path(csv_path)
154-
else:
155-
pandas_df = pd.read_csv(csv_path)
156-
if pandas_df is not None:
157-
if old_row_num == 0:
158-
self.from_pandas_df(pandas_df)
159-
else:
160-
self.array = np.hstack((self.array, np.empty(pandas_df.shape).T))
161-
for col in pandas_df.columns:
162-
self[col, new_data_cols] = np.asarray(pandas_df[col].values)
188+
189+
if not isinstance(navdata,NavData):
190+
raise TypeError("concat input data must be a NavData instance.")
191+
192+
if axis == 0: # concatenate new rows
193+
if len(self) != len(navdata):
194+
raise RuntimeError("concat input data must be same " \
195+
+ "length to concatenate new rows.")
196+
if not inplace:
197+
new_navdata = self.copy()
198+
for row in navdata.rows:
199+
new_row_name = row
200+
suffix = None
201+
while new_row_name in self.rows:
202+
if suffix is None:
203+
suffix = 0
204+
else:
205+
suffix += 1
206+
new_row_name = row + "_" + str(suffix)
207+
if inplace:
208+
self[new_row_name] = navdata[row]
209+
else:
210+
new_navdata[new_row_name] = navdata[row]
211+
212+
elif axis == 1: # concatenate new columns
213+
new_navdata = NavData()
214+
# get unique list of row names
215+
combined_rows = set(self.rows + navdata.rows)
216+
217+
for row in combined_rows:
218+
combined_row = np.array([])
219+
# combine data from existing and new instance
220+
for data in [self, navdata]:
221+
if row in data.rows:
222+
new_row = data[row]
223+
elif len(data) == 0:
224+
continue
225+
else:
226+
# add np.nan for missing values
227+
new_row = np.empty((len(data),))
228+
new_row.fill(np.nan)
229+
combined_row = np.concatenate((combined_row,
230+
new_row))
231+
new_navdata[row] = combined_row
232+
if inplace:
233+
self.array = new_navdata.array
234+
self.map = new_navdata.map
235+
self.str_map = new_navdata.str_map
236+
237+
if inplace:
238+
return None
239+
return new_navdata
163240

164241
def where(self, key_idx, value, condition="eq"):
165242
"""Return NavData where conditions are met for the given row

notebooks/tutorials/navdata.ipynb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,7 @@
313313
"cell_type": "markdown",
314314
"metadata": {},
315315
"source": [
316-
"To add new columns, use the `NavData.add()` method with the `csv_path`, \n",
317-
"`pandas_df` and `numpy_array` flags depending on the input type."
316+
"To add new columns, use the `NavData.concat()` method which concatenates two `NavData` instances."
318317
]
319318
},
320319
{
@@ -341,7 +340,7 @@
341340
"metadata": {},
342341
"outputs": [],
343342
"source": [
344-
"nav_data_np.add(numpy_array=np_array)\n",
343+
"nav_data_np.concat(NavData(numpy_array=np_array),inplace=True)\n",
345344
"nav_data_np[:]"
346345
]
347346
},

tests/parsers/test_android.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,42 @@ def test_get_and_set_str(derived):
291291

292292
np.testing.assert_equal(derived[key, :], value)
293293

294+
def test_android_concat(derived, pd_df):
295+
"""Test concat on Android data.
296+
297+
Parameters
298+
----------
299+
derived : AndroidDerived2021
300+
Instance of AndroidDerived2021 for testing
301+
pd_df : pytest.fixture
302+
pd.DataFrame for testing measurements
303+
"""
304+
305+
# remove first timestamp to match
306+
pd_df = pd_df[pd_df['millisSinceGpsEpoch'] != pd_df.loc[0,'millisSinceGpsEpoch']]
307+
308+
# extract and combine gps and glonass data
309+
gps_data = derived.where("gnss_id","gps")
310+
glonass_data = derived.where("gnss_id","glonass")
311+
gps_glonass_navdata = gps_data.concat(glonass_data)
312+
glonass_gps_navdata = glonass_data.concat(gps_data)
313+
314+
# combine using pandas
315+
gps_df = pd_df[pd_df["constellationType"]==1]
316+
glonass_df = pd_df[pd_df["constellationType"]==3]
317+
gps_glonass_df = pd.concat((gps_df,glonass_df))
318+
glonass_gps_df = pd.concat((glonass_df,gps_df))
319+
320+
for combined_navdata, combined_df in [(gps_glonass_navdata, gps_glonass_df),
321+
(glonass_gps_navdata, glonass_gps_df)]:
322+
323+
# check a few rows to make sure they're equal
324+
np.testing.assert_array_equal(combined_navdata["raw_pr_m"],
325+
combined_df["rawPrM"])
326+
np.testing.assert_array_equal(combined_navdata["raw_pr_sigma_m"],
327+
combined_df["rawPrUncM"])
328+
np.testing.assert_array_equal(combined_navdata["intersignal_bias_m"],
329+
combined_df["isrbM"])
294330

295331
def test_imu_raw(android_raw_path):
296332
"""Test that AndroidRawImu initialization

0 commit comments

Comments
 (0)