flightdata 0.2.5__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flightdata-0.2.7/PKG-INFO +29 -0
- flightdata-0.2.7/README.md +14 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/collection.py +7 -4
- flightdata-0.2.7/flightdata/base/labeling.py +9 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/table.py +19 -28
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/flight.py +101 -52
- flightdata-0.2.7/flightdata/flight/parameters.py +12 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/origin.py +17 -18
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/state.py +7 -9
- flightdata-0.2.7/flightdata.egg-info/PKG-INFO +29 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata.egg-info/SOURCES.txt +4 -0
- flightdata-0.2.7/flightdata.egg-info/requires.txt +5 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata.egg-info/top_level.txt +1 -0
- flightdata-0.2.7/scripts/collect_logs.py +28 -0
- flightdata-0.2.7/scripts/flightline.py +57 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/setup.cfg +10 -2
- {flightdata-0.2.5 → flightdata-0.2.7}/test/test_flight.py +22 -8
- {flightdata-0.2.5 → flightdata-0.2.7}/test/test_origin.py +2 -2
- flightdata-0.2.5/PKG-INFO +0 -171
- flightdata-0.2.5/README.md +0 -158
- flightdata-0.2.5/flightdata.egg-info/PKG-INFO +0 -171
- flightdata-0.2.5/flightdata.egg-info/requires.txt +0 -3
- {flightdata-0.2.5 → flightdata-0.2.7}/LICENSE +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/__init__.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/__init__.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/constructs.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/numpy_encoder.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/coefficients.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/environment/__init__.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/environment/environment.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/environment/wind.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/__init__.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/ardupilot.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/fields.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flow.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/model/__init__.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/model/aerodynamic.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/model/thrust.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/flightdata.egg-info/dependency_links.txt +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/setup.py +0 -0
- {flightdata-0.2.5 → flightdata-0.2.7}/test/test_fields.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: flightdata
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: Module for handling UAV flight log data
|
|
5
|
+
Home-page: https://github.com/PyFlightCoach/FlightData
|
|
6
|
+
Author: Thomas David
|
|
7
|
+
Author-email: thomasdavid0@gmail.com
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: simplejson
|
|
13
|
+
Requires-Dist: pfc-geometry>=0.2.5
|
|
14
|
+
Requires-Dist: ardupilot-log-reader>=0.3.1
|
|
15
|
+
|
|
16
|
+
## FlightData
|
|
17
|
+
This repo is contains a set of datastructures and tools for handling flight log data.
|
|
18
|
+
|
|
19
|
+
### Flight
|
|
20
|
+
The Flight object represents the data logged by a flight controller. The class wraps a pandas dataframe which is indexed on a single time axis. Where data is logged at different rates for different sensors it is mapped to the closest time index. Attribute access provides individual columns or sets of columns in the groups defined in Fields. Item access subsets the data in the time axis.
|
|
21
|
+
|
|
22
|
+
### Table
|
|
23
|
+
The Table is the base type for most of the datastructures. It allows attribute access to individual columns. Attribute access is also available to return basic entities subclassed from the base type in the pfc-geometry package. For example in the state object table.x provides the x position, table.pos provides a Point representing the xyz position. columns that are not represented by geometric base types are considered to be labels for the data.
|
|
24
|
+
|
|
25
|
+
### State
|
|
26
|
+
The State object is a table representing the position and orientation of the aircraft along with their derivatives, it can be constructed from a Flight or from scratch by extrapolating in lines or around arcs. Many tools are provided to manipulate the data. The position and attitude are in a reference frame (with Z up), the derivatives move with the aircraft in either the body, wind, stability or track (like the wind axis but with no wind) frame.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Further documentation will be provided here: https://pfcdocumentation.readthedocs.io/pyflightcoach/flightdata.html
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## FlightData
|
|
2
|
+
This repo is contains a set of datastructures and tools for handling flight log data.
|
|
3
|
+
|
|
4
|
+
### Flight
|
|
5
|
+
The Flight object represents the data logged by a flight controller. The class wraps a pandas dataframe which is indexed on a single time axis. Where data is logged at different rates for different sensors it is mapped to the closest time index. Attribute access provides individual columns or sets of columns in the groups defined in Fields. Item access subsets the data in the time axis.
|
|
6
|
+
|
|
7
|
+
### Table
|
|
8
|
+
The Table is the base type for most of the datastructures. It allows attribute access to individual columns. Attribute access is also available to return basic entities subclassed from the base type in the pfc-geometry package. For example in the state object table.x provides the x position, table.pos provides a Point representing the xyz position. columns that are not represented by geometric base types are considered to be labels for the data.
|
|
9
|
+
|
|
10
|
+
### State
|
|
11
|
+
The State object is a table representing the position and orientation of the aircraft along with their derivatives, it can be constructed from a Flight or from scratch by extrapolating in lines or around arcs. Many tools are provided to manipulate the data. The position and attitude are in a reference frame (with Z up), the derivatives move with the aircraft in either the body, wind, stability or track (like the wind axis but with no wind) frame.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Further documentation will be provided here: https://pfcdocumentation.readthedocs.io/pyflightcoach/flightdata.html
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
2
|
-
import numpy as np
|
|
1
|
+
from typing import Union, Any, Self, TypeVar, Iterable
|
|
3
2
|
import pandas as pd
|
|
4
3
|
|
|
5
4
|
|
|
@@ -45,7 +44,7 @@ class Collection:
|
|
|
45
44
|
for v in self.data.values():
|
|
46
45
|
yield v
|
|
47
46
|
|
|
48
|
-
def to_list(self) ->
|
|
47
|
+
def to_list(self) -> list[T]:
|
|
49
48
|
return list(self.data.values())
|
|
50
49
|
|
|
51
50
|
def to_dicts(self, *args, **kwargs) -> list[dict[str, Any]]:
|
|
@@ -99,4 +98,8 @@ class Collection:
|
|
|
99
98
|
return str(pd.Series({k: repr(v) for k, v in self.data.items()}))
|
|
100
99
|
|
|
101
100
|
def __len__(self) -> int:
|
|
102
|
-
return len(self.data)
|
|
101
|
+
return len(self.data)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def uids(self) -> list[str]:
|
|
105
|
+
return list(self.data.keys())
|
|
@@ -3,35 +3,29 @@ import numpy as np
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import numpy.typing as npt
|
|
5
5
|
from geometry import Base, Time
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Self, Tuple
|
|
7
7
|
from .constructs import SVar, Constructs
|
|
8
8
|
from numbers import Number
|
|
9
9
|
from time import time
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def make_time(tab):
|
|
13
|
-
return Time.from_t(tab.t)
|
|
14
|
-
|
|
15
|
-
|
|
16
12
|
class Table:
|
|
17
13
|
constructs = Constructs([
|
|
18
|
-
SVar("time", Time,
|
|
14
|
+
SVar("time", Time, ["t", "dt"], lambda tab: Time.from_t(tab.t))
|
|
19
15
|
])
|
|
20
16
|
|
|
21
17
|
def __init__(self, data: pd.DataFrame, fill=True, min_len=1):
|
|
22
18
|
if len(data) < min_len:
|
|
23
19
|
raise Exception(f"State constructor length check failed, data length = {len(data)}, min_len = {min_len}")
|
|
24
20
|
self.base_cols = [c for c in data.columns if c in self.constructs.cols()]
|
|
25
|
-
self.label_cols = [c for c in data.columns if not
|
|
21
|
+
self.label_cols = [c for c in data.columns if c not in self.constructs.cols()]
|
|
26
22
|
|
|
27
23
|
self.data = data
|
|
28
|
-
|
|
29
|
-
self.data.index = self.data.index - self.data.index[0]
|
|
24
|
+
#self.data.index = self.data.index - self.data.index[0]
|
|
30
25
|
|
|
31
26
|
if fill:
|
|
32
27
|
missing = self.constructs.missing(self.data.columns)
|
|
33
|
-
for svar in missing:
|
|
34
|
-
|
|
28
|
+
for svar in missing:
|
|
35
29
|
newdata = svar.builder(self).to_pandas(
|
|
36
30
|
columns=svar.keys,
|
|
37
31
|
index=self.data.index
|
|
@@ -45,7 +39,7 @@ class Table:
|
|
|
45
39
|
raise ValueError("nan values in data")
|
|
46
40
|
|
|
47
41
|
|
|
48
|
-
def __getattr__(self, name: str) ->
|
|
42
|
+
def __getattr__(self, name: str) -> npt.NDArray | Base:
|
|
49
43
|
if name in self.data.columns:
|
|
50
44
|
return self.data[name].to_numpy()
|
|
51
45
|
elif name in self.constructs.data.keys():
|
|
@@ -73,17 +67,20 @@ class Table:
|
|
|
73
67
|
@property
|
|
74
68
|
def duration(self):
|
|
75
69
|
return self.data.index[-1] - self.data.index[0]
|
|
76
|
-
|
|
70
|
+
|
|
77
71
|
def __getitem__(self, sli):
|
|
78
|
-
if isinstance(sli,
|
|
72
|
+
if isinstance(sli, slice):
|
|
73
|
+
return self.__class__(self.data.loc[slice(sli.start + self.data.index[0], sli.stop + self.data.index[0], sli.step)])
|
|
74
|
+
elif isinstance(sli, Number):
|
|
79
75
|
if sli<0:
|
|
80
76
|
return self.__class__(self.data.iloc[[int(sli)], :])
|
|
81
77
|
|
|
82
78
|
return self.__class__(
|
|
83
|
-
self.data.iloc[self.data.index.get_indexer([sli], method="nearest"), :]
|
|
79
|
+
self.data.iloc[self.data.index.get_indexer([sli + self.data.index[0]], method="nearest"), :]
|
|
84
80
|
)
|
|
81
|
+
else:
|
|
82
|
+
raise TypeError(f"Expected Number or slice, got {sli.__class__.__name__}")
|
|
85
83
|
|
|
86
|
-
return self.__class__(self.data.loc[sli])
|
|
87
84
|
|
|
88
85
|
def slice_raw_t(self, sli):
|
|
89
86
|
inds = self.data.reset_index(names="t2").set_index("t").loc[sli].t2.to_numpy()#set_index("t", drop=False).columns
|
|
@@ -176,13 +173,7 @@ class Table:
|
|
|
176
173
|
return self.data.loc[:, self.label_cols]
|
|
177
174
|
|
|
178
175
|
def remove_labels(self) -> Self:
|
|
179
|
-
return self.__class__(
|
|
180
|
-
self.data.drop(
|
|
181
|
-
self.label_keys,
|
|
182
|
-
axis=1,
|
|
183
|
-
errors="ignore"
|
|
184
|
-
)
|
|
185
|
-
)
|
|
176
|
+
return self.__class__(self.data.drop(self.label_keys, axis=1, errors="ignore"))
|
|
186
177
|
|
|
187
178
|
def get_subset_df(self, **kwargs) -> pd.DataFrame:
|
|
188
179
|
dfo = self.data
|
|
@@ -200,9 +191,10 @@ class Table:
|
|
|
200
191
|
return 0
|
|
201
192
|
|
|
202
193
|
def unique_labels(self, cols = None) -> pd.DataFrame:
|
|
203
|
-
'''TODO Fix This'''
|
|
204
194
|
if cols is None:
|
|
205
195
|
cols = self.label_cols
|
|
196
|
+
elif isinstance(cols, str):
|
|
197
|
+
cols = [cols]
|
|
206
198
|
return self.data.loc[:, cols].reset_index(drop=True).drop_duplicates().reset_index(drop=True)
|
|
207
199
|
|
|
208
200
|
def shift_labels(self, col, elname, offset, allow_label_loss=True) -> Self:
|
|
@@ -254,7 +246,7 @@ class Table:
|
|
|
254
246
|
@classmethod
|
|
255
247
|
def shift_multi(Cls, steps: int, tb1: Self, tb2: Self, min_len=2) -> Tuple(Self, Self):
|
|
256
248
|
'''Take datapoints off the start of tb2 and add to the end tb1'''
|
|
257
|
-
if (steps>0 and len(tb2)-min_len<steps) or (steps<0 and len(tb1)
|
|
249
|
+
if (steps>0 and len(tb2)-min_len<steps) or (steps<0 and min_len - len(tb1) > steps):
|
|
258
250
|
raise ValueError(f'Cannot Squash a Table to less than {min_len} datapoints')
|
|
259
251
|
dfj = Cls.stack([tb1, tb2], overlap=0).data
|
|
260
252
|
return Cls(dfj.iloc[:len(tb1)+steps, :]), \
|
|
@@ -280,7 +272,7 @@ class Table:
|
|
|
280
272
|
res = res.shift_label_ratio(ratio, min_len, **lab)
|
|
281
273
|
return res
|
|
282
274
|
|
|
283
|
-
def get_label_id(self, **kwargs) ->
|
|
275
|
+
def get_label_id(self, **kwargs) -> int | float:
|
|
284
276
|
dfo = self.unique_labels()
|
|
285
277
|
for k, v in kwargs.items():
|
|
286
278
|
dfo = dfo.loc[dfo[k] == v, :]
|
|
@@ -317,7 +309,7 @@ class Table:
|
|
|
317
309
|
labs = np.array(self.single_labels())
|
|
318
310
|
return self.__class__(self.data[labs == lab])
|
|
319
311
|
|
|
320
|
-
def split_labels(self, cols=None) -> dict[str, Self]:
|
|
312
|
+
def split_labels(self, cols:list[str]|str = None) -> dict[str, Self]:
|
|
321
313
|
'''Split into multiple tables based on the labels'''
|
|
322
314
|
res = {}
|
|
323
315
|
for label in self.unique_labels(cols).iterrows():
|
|
@@ -341,7 +333,6 @@ class Table:
|
|
|
341
333
|
strdf = df.copy()
|
|
342
334
|
strdf['indexer'] = strdf['indexer'].astype(int).astype(str)
|
|
343
335
|
strdf = strdf.stack().groupby(level=0).apply('_'.join)
|
|
344
|
-
strdf.loc[df.indexer==0] = df[0]
|
|
345
336
|
return strdf.values
|
|
346
337
|
|
|
347
338
|
@staticmethod
|
|
@@ -21,10 +21,9 @@ from time import time
|
|
|
21
21
|
from json import load, dump
|
|
22
22
|
from ardupilot_log_reader.reader import Ardupilot
|
|
23
23
|
from flightdata.base.numpy_encoder import NumpyEncoder
|
|
24
|
-
from flightdata.flight.fields import Fields
|
|
25
24
|
from .ardupilot import flightmodes
|
|
26
25
|
from flightdata import Origin
|
|
27
|
-
from
|
|
26
|
+
from numbers import Number
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
class Flight:
|
|
@@ -34,30 +33,52 @@ class Flight:
|
|
|
34
33
|
'ARSP', 'GPS', 'RCIN', 'RCOU', 'BARO', 'MODE',
|
|
35
34
|
'RPM', 'MAG', 'BAT', 'BAT2', 'VEL', 'ORGN', 'ESC', 'CURRENT']
|
|
36
35
|
|
|
37
|
-
def __init__(self, data: pd.DataFrame, parameters:
|
|
36
|
+
def __init__(self, data: pd.DataFrame, parameters: pd.DataFrame = None, origin: Origin = None, primary_pos_source='gps'):
|
|
38
37
|
self.data = data
|
|
39
38
|
self.parameters = parameters
|
|
40
|
-
self.data.index = self.data.index - self.data.index[0]
|
|
41
|
-
self.data.index.name = 'time_index'
|
|
42
39
|
self.origin = origin
|
|
43
40
|
self.primary_pos_source = primary_pos_source
|
|
44
41
|
|
|
45
42
|
def __getattr__(self, name):
|
|
43
|
+
if self.parameters is not None:
|
|
44
|
+
if name in self.parameters.parameter.unique():
|
|
45
|
+
df = self.parameters.loc[self.parameters.parameter==name]
|
|
46
|
+
return df.loc[df.value != df.value.shift()]
|
|
46
47
|
cols = getattr(fields, name)
|
|
47
48
|
if cols is None:
|
|
48
49
|
cols = [f for f in self.data.columns if f.startswith(name)]
|
|
49
50
|
if len(cols) > 0:
|
|
50
51
|
return self.data[cols]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
cols
|
|
59
|
-
|
|
52
|
+
else:
|
|
53
|
+
try:
|
|
54
|
+
if isinstance(cols, Field):
|
|
55
|
+
return self.data[cols.col]
|
|
56
|
+
else:
|
|
57
|
+
return self.data.loc[:, [f.col for f in cols if f.col in self.data.columns]]
|
|
58
|
+
except KeyError:
|
|
59
|
+
if isinstance(cols, Field):
|
|
60
|
+
cols = [cols]
|
|
61
|
+
return pd.DataFrame(data=np.empty((len(self), len(cols))),columns=[f.col for f in cols])
|
|
62
|
+
raise AttributeError(f"'Flight' object has no attribute '{name}'")
|
|
63
|
+
|
|
64
|
+
def make_param_labels(self, pname: str, prefix:str=None, suffix:str=None, unknown=''):
|
|
65
|
+
'''Make a series with the parameter values at the correct times.'''
|
|
66
|
+
ser = pd.Series(np.nan, index=self.data.index, name=pname)
|
|
67
|
+
param = getattr(self, pname)
|
|
68
|
+
ser.iloc[ser.index.get_indexer(param.index, 'nearest')] = param.value
|
|
69
|
+
ser = ser.ffill()
|
|
60
70
|
|
|
71
|
+
if prefix or suffix:
|
|
72
|
+
sout = pd.Series(unknown, index=self.data.index, name=pname)
|
|
73
|
+
sout[~np.isnan(ser)] = (prefix or '') + ser[~np.isnan(ser)].astype(str) + (suffix or '')
|
|
74
|
+
return sout
|
|
75
|
+
else:
|
|
76
|
+
return ser
|
|
77
|
+
|
|
78
|
+
def make_param_df(self, pnames: list[str]):
|
|
79
|
+
'''Make a dataframe of parameter values'''
|
|
80
|
+
return pd.DataFrame([self.make_param_labels(p) for p in pnames]).T
|
|
81
|
+
|
|
61
82
|
def contains(self, name: Union[str, list[str]]):
|
|
62
83
|
cols = getattr(fields, name)
|
|
63
84
|
if isinstance(cols, Field):
|
|
@@ -65,35 +86,55 @@ class Flight:
|
|
|
65
86
|
else:
|
|
66
87
|
return [f.column in self.data.columns for f in cols]
|
|
67
88
|
|
|
68
|
-
def __getitem__(self, sli) ->
|
|
69
|
-
if isinstance(sli,
|
|
70
|
-
|
|
89
|
+
def __getitem__(self, sli: Number | slice) -> Flight:
|
|
90
|
+
if isinstance(sli, Number):
|
|
91
|
+
if sli < 0:
|
|
92
|
+
return self.data.iloc[sli]
|
|
93
|
+
else:
|
|
94
|
+
gl = self.data.index.get_loc(sli + self.data.index[0])
|
|
95
|
+
return Flight(
|
|
96
|
+
self.data.iloc[gl],
|
|
97
|
+
self.parameters.loc[:gl],
|
|
98
|
+
)
|
|
99
|
+
elif isinstance(sli, slice):
|
|
100
|
+
return Flight(
|
|
101
|
+
self.data.loc[slice(
|
|
102
|
+
None if sli.start is None else sli.start + self.data.index[0],
|
|
103
|
+
None if sli.stop is None else sli.stop + self.data.index[0],
|
|
104
|
+
sli.step
|
|
105
|
+
)],
|
|
106
|
+
self.parameters.loc[:None if sli.stop is None else sli.stop + self.data.index[0]],
|
|
107
|
+
self.origin, self.primary_pos_source
|
|
108
|
+
)
|
|
71
109
|
else:
|
|
72
|
-
|
|
110
|
+
raise TypeError(f'Expected a number or a slice, got a {sli.__class__.__name__}')
|
|
73
111
|
|
|
74
112
|
def __len__(self):
|
|
75
113
|
return len(self.data)
|
|
76
114
|
|
|
77
|
-
def slice_raw_t(self, sli):
|
|
115
|
+
def slice_raw_t(self, sli: Number | slice) -> Flight:
|
|
116
|
+
def opp(df: pd.DataFrame, indexer: Number | slice):
|
|
117
|
+
return df.reset_index(drop=True) \
|
|
118
|
+
.set_index('time_actual', drop=False) \
|
|
119
|
+
.loc[indexer].set_index("time_flight", drop=False)
|
|
120
|
+
|
|
78
121
|
return Flight(
|
|
79
|
-
self.data
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.parameters, self.origin, self.primary_pos_source
|
|
122
|
+
opp(self.data, sli),
|
|
123
|
+
opp(self.parameters, slice(None, sli if isinstance(sli, Number) else sli.stop, None)),
|
|
124
|
+
self.origin, self.primary_pos_source
|
|
83
125
|
)
|
|
84
126
|
|
|
85
|
-
def slice_time_flight(self, sli):
|
|
127
|
+
def slice_time_flight(self, sli) -> Flight:
|
|
86
128
|
return Flight(
|
|
87
|
-
self.data.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
self.parameters, self.origin, self.primary_pos_source
|
|
129
|
+
self.data.loc[sli],
|
|
130
|
+
self.parameters.loc[:sli if isinstance(sli, Number) else sli.stop],
|
|
131
|
+
self.origin, self.primary_pos_source
|
|
91
132
|
)
|
|
92
133
|
|
|
93
|
-
def copy(self, **kwargs):
|
|
134
|
+
def copy(self, **kwargs) -> Flight:
|
|
94
135
|
return Flight(
|
|
95
136
|
kwargs['data'] if 'data' in kwargs else self.data.copy() ,
|
|
96
|
-
kwargs['parameters'] if 'parameters' in kwargs else self.parameters.copy() if self.parameters else None,
|
|
137
|
+
kwargs['parameters'] if 'parameters' in kwargs else self.parameters.copy() if self.parameters is not None else None,
|
|
97
138
|
kwargs['origin'] if 'origin' in kwargs else self.origin.copy(),
|
|
98
139
|
kwargs['primary_pos_source'] if 'primary_pos_source' in kwargs else self.primary_pos_source
|
|
99
140
|
)
|
|
@@ -101,16 +142,16 @@ class Flight:
|
|
|
101
142
|
def to_dict(self):
|
|
102
143
|
return {
|
|
103
144
|
'data': self.data.to_dict('list'),
|
|
104
|
-
'parameters': self.parameters,
|
|
145
|
+
'parameters': self.parameters.to_dict('list'),
|
|
105
146
|
'origin': self.origin.to_dict(),
|
|
106
147
|
'primary_pos_source': self.primary_pos_source
|
|
107
148
|
}
|
|
108
149
|
|
|
109
150
|
@staticmethod
|
|
110
|
-
def from_dict(data: dict):
|
|
151
|
+
def from_dict(data: dict) -> Flight:
|
|
111
152
|
return Flight(
|
|
112
153
|
data=pd.DataFrame.from_dict(data['data']).set_index('time_flight', drop=False),
|
|
113
|
-
parameters=data['parameters'],
|
|
154
|
+
parameters=pd.DataFrame.from_dict(data['parameters']).set_index('time_flight', drop=False),
|
|
114
155
|
origin=Origin.from_dict(data['origin']),
|
|
115
156
|
primary_pos_source=data['primary_pos_source']
|
|
116
157
|
)
|
|
@@ -122,11 +163,10 @@ class Flight:
|
|
|
122
163
|
|
|
123
164
|
@staticmethod
|
|
124
165
|
def from_json(file: str) -> Self:
|
|
125
|
-
|
|
126
|
-
return Flight.from_dict(load(f))
|
|
166
|
+
return Flight.from_dict(load(open(file, 'r')))
|
|
127
167
|
|
|
128
168
|
@staticmethod
|
|
129
|
-
def build_cols(**kwargs):
|
|
169
|
+
def build_cols(**kwargs) -> pd.DataFrame:
|
|
130
170
|
df = pd.DataFrame(columns=list(fields.data.keys()))
|
|
131
171
|
for k, v in kwargs.items():
|
|
132
172
|
df[k] = v
|
|
@@ -151,7 +191,7 @@ class Flight:
|
|
|
151
191
|
otf,
|
|
152
192
|
fl.data.reset_index(),
|
|
153
193
|
on='time_actual'
|
|
154
|
-
).set_index('
|
|
194
|
+
).set_index('time_flight', drop=False)
|
|
155
195
|
))
|
|
156
196
|
|
|
157
197
|
return flos
|
|
@@ -173,7 +213,7 @@ class Flight:
|
|
|
173
213
|
|
|
174
214
|
@property
|
|
175
215
|
def duration(self):
|
|
176
|
-
return self.data.
|
|
216
|
+
return self.data.iloc[-1].name - self.data.iloc[0].name
|
|
177
217
|
|
|
178
218
|
def flying_only(self, minalt=5, minv=10):
|
|
179
219
|
vs = abs(Point(self.velocity))
|
|
@@ -195,29 +235,36 @@ class Flight:
|
|
|
195
235
|
pd.testing.assert_frame_equal(self.data, other.data)
|
|
196
236
|
assert_almost_equal(self.origin.pos, other.origin.pos)
|
|
197
237
|
assert self.origin.heading == other.origin.heading
|
|
238
|
+
pd.testing.assert_frame_equal(self.parameters, other.parameters)
|
|
198
239
|
return True
|
|
199
|
-
except Exception
|
|
240
|
+
except Exception:
|
|
200
241
|
return False
|
|
201
242
|
|
|
202
243
|
@staticmethod
|
|
203
244
|
def from_log(log:Union[Ardupilot, str], extra_types: list[str] = None, **kwargs) -> Flight:
|
|
204
245
|
"""Constructor from an ardupilot bin file."""
|
|
205
|
-
|
|
206
246
|
extra_types = [] if extra_types is None else extra_types
|
|
207
247
|
|
|
208
248
|
if isinstance(log, str) or isinstance(log, Path):
|
|
209
|
-
parser = Ardupilot(str(log), types=list(set(Flight.ardupilot_types + extra_types)))
|
|
249
|
+
parser = Ardupilot.parse(str(log), types=list(set(Flight.ardupilot_types + extra_types)))
|
|
210
250
|
else:
|
|
211
251
|
parser = log
|
|
212
|
-
|
|
213
|
-
|
|
252
|
+
|
|
253
|
+
params = Flight.build_cols(
|
|
254
|
+
time_actual = parser.PARM.timestamp,
|
|
255
|
+
time_flight = parser.PARM.TimeUS / 1e6,
|
|
256
|
+
parameter = parser.PARM.Name,
|
|
257
|
+
value = parser.PARM.Value
|
|
258
|
+
).set_index('time_flight', drop=False)
|
|
259
|
+
|
|
260
|
+
if params.loc[params.parameter=='AHRS_EKF_TYPE'].iloc[0].value == 2:
|
|
214
261
|
ekf1 = 'NKF1'
|
|
215
262
|
ekf2 = 'NKF2'
|
|
216
263
|
else:
|
|
217
264
|
ekf1 = 'XKF1'
|
|
218
265
|
ekf2 = 'XKF2'
|
|
219
266
|
|
|
220
|
-
ekf1 = parser.dfs[ekf1] if ekf1 in parser.dfs else
|
|
267
|
+
ekf1 = parser.dfs[ekf1] if ekf1 in parser.dfs else None
|
|
221
268
|
ekf2 = parser.dfs[ekf2] if ekf2 in parser.dfs else None
|
|
222
269
|
|
|
223
270
|
dfs = []
|
|
@@ -263,13 +310,13 @@ class Flight:
|
|
|
263
310
|
}, 'C')
|
|
264
311
|
if 'RATE' in parser.dfs:
|
|
265
312
|
dfs.append(Flight.build_cols(
|
|
266
|
-
time_actual = parser.
|
|
267
|
-
axisrate_roll = parser.
|
|
268
|
-
axisrate_pitch = parser.
|
|
269
|
-
axisrate_yaw = parser.
|
|
270
|
-
desrate_roll = parser.
|
|
271
|
-
desrate_pitch = parser.
|
|
272
|
-
desrate_yaw = parser.
|
|
313
|
+
time_actual = parser.RATE.timestamp,
|
|
314
|
+
axisrate_roll = parser.RATE.R,
|
|
315
|
+
axisrate_pitch = parser.RATE.P,
|
|
316
|
+
axisrate_yaw = parser.RATE.Y,
|
|
317
|
+
desrate_roll = parser.RATE.RDes,
|
|
318
|
+
desrate_pitch = parser.RATE.PDes,
|
|
319
|
+
desrate_yaw = parser.RATE.YDes,
|
|
273
320
|
))
|
|
274
321
|
|
|
275
322
|
if 'IMU' in parser.dfs:
|
|
@@ -384,7 +431,7 @@ class Flight:
|
|
|
384
431
|
|
|
385
432
|
origin = Origin('ekf_origin', GPS(parser.ORGN.iloc[:,-3:]), 0)
|
|
386
433
|
|
|
387
|
-
return Flight(dfout.set_index('time_flight', drop=False),
|
|
434
|
+
return Flight(dfout.set_index('time_flight', drop=False), params, origin, ppsorce)
|
|
388
435
|
|
|
389
436
|
@staticmethod
|
|
390
437
|
def parse_instances(indf: pd.DataFrame, colmap:dict[str, str], instancecol='Instance'):
|
|
@@ -452,6 +499,7 @@ class Flight:
|
|
|
452
499
|
)
|
|
453
500
|
|
|
454
501
|
def filter(self, b, a):
|
|
502
|
+
from scipy.signal import filtfilt
|
|
455
503
|
dont_filter = [c for c in fields.get_cols(['time', 'flightmode', 'rcin', 'rcout']) if c in self.data.columns]
|
|
456
504
|
unwrap_cols = [c for c in fields.get_cols(['attitude']) if c in self.data.columns]
|
|
457
505
|
|
|
@@ -469,6 +517,7 @@ class Flight:
|
|
|
469
517
|
)
|
|
470
518
|
|
|
471
519
|
def butter_filter(self, cutoff, order=5):
|
|
520
|
+
from scipy.signal import butter
|
|
472
521
|
ts = self.time_flight.to_numpy()
|
|
473
522
|
N = len(self)
|
|
474
523
|
T = (ts[-1] - ts[0]) / N
|
|
@@ -91,25 +91,24 @@ class Origin(object):
|
|
|
91
91
|
np.arctan2(direction.y[0], direction.x[0])
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
def to_f3a_zone(self):
|
|
94
|
+
def to_f3a_zone(self, file: str):
|
|
95
95
|
|
|
96
96
|
centre = self.pilot_position.offset(
|
|
97
97
|
100 * g.Point(np.cos(self.heading), np.sin(self.heading), 0.0)
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
with open(file, 'w') as f:
|
|
101
|
+
for line in [
|
|
102
|
+
"Emailed box data for F3A Zone Pro - please DON'T modify!",
|
|
103
|
+
self.name,
|
|
104
|
+
self.pilot_position.lat[0],
|
|
105
|
+
self.pilot_position.long[0],
|
|
106
|
+
centre.lat[0],
|
|
107
|
+
centre.long[0],
|
|
108
|
+
self.pilot_position.alt[0]
|
|
109
|
+
]:
|
|
110
|
+
print(line, file=f)
|
|
111
|
+
|
|
113
112
|
@staticmethod
|
|
114
113
|
def from_f3a_zone(file_path: str):
|
|
115
114
|
if hasattr(file_path, "read"):
|
|
@@ -119,16 +118,16 @@ class Origin(object):
|
|
|
119
118
|
lines = f.read().splitlines()
|
|
120
119
|
return Origin.from_points(
|
|
121
120
|
lines[1],
|
|
122
|
-
g.GPS(float(lines[2]), float(lines[3]),
|
|
123
|
-
g.GPS(float(lines[4]), float(lines[5]),
|
|
121
|
+
g.GPS(float(lines[2]), float(lines[3]), float(lines[6])),
|
|
122
|
+
g.GPS(float(lines[4]), float(lines[5]), float(lines[6]))
|
|
124
123
|
)
|
|
125
124
|
|
|
126
125
|
@staticmethod
|
|
127
126
|
def from_fcjson_parmameters(data: dict):
|
|
128
127
|
return Origin.from_points(
|
|
129
128
|
"FCJ_box",
|
|
130
|
-
g.GPS(float(data['pilotLat']), float(data['pilotLng']),
|
|
131
|
-
g.GPS(float(data['centerLat']), float(data['centerLng']),
|
|
129
|
+
g.GPS(float(data['pilotLat']), float(data['pilotLng']), float(data['pilotAlt'])),
|
|
130
|
+
g.GPS(float(data['centerLat']), float(data['centerLng']), float(data['centerAlt']))
|
|
132
131
|
)
|
|
133
132
|
|
|
134
133
|
|
|
@@ -103,16 +103,16 @@ class State(Table):
|
|
|
103
103
|
time = g.Time.from_t(np.array(flight.data.time_flight))
|
|
104
104
|
|
|
105
105
|
if all(flight.contains('gps')) and flight.primary_pos_source == 'gps':
|
|
106
|
-
pos = origin.rotation.transform_point(g.GPS(flight.gps) - origin.pos[0])
|
|
106
|
+
pos = origin.rotation.transform_point(g.GPS(flight.gps.ffill().bfill()) - origin.pos[0])
|
|
107
107
|
else:
|
|
108
108
|
pos = origin.rotation.transform_point(
|
|
109
|
-
flight.origin.pos.offset(g.Point(flight.position)) - origin.pos[0]
|
|
109
|
+
flight.origin.pos.offset(g.Point(flight.position.ffill().bfill())) - origin.pos[0]
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
-
att = origin.rotation * g.Euler(flight.attitude)
|
|
113
|
-
vel = att.inverse().transform_point(origin.rotation.transform_point(g.Point(flight.velocity))) if all(flight.contains('velocity')) else None
|
|
114
|
-
rvel = g.Point(flight.axisrate) if all(flight.contains('axisrate')) else None
|
|
115
|
-
acc = g.Point(flight.acceleration) if all(flight.contains('acceleration')) else None
|
|
112
|
+
att = origin.rotation * g.Euler(flight.attitude.ffill().bfill())
|
|
113
|
+
vel = att.inverse().transform_point(origin.rotation.transform_point(g.Point(flight.velocity.ffill().bfill()))) if all(flight.contains('velocity')) else None
|
|
114
|
+
rvel = g.Point(flight.axisrate.ffill().bfill()) if all(flight.contains('axisrate')) else None
|
|
115
|
+
acc = g.Point(flight.acceleration.ffill().bfill()) if all(flight.contains('acceleration')) else None
|
|
116
116
|
|
|
117
117
|
return State.from_constructs(time, pos, att, vel, rvel, acc)
|
|
118
118
|
|
|
@@ -123,7 +123,7 @@ class State(Table):
|
|
|
123
123
|
radius=5, mirror=True,
|
|
124
124
|
weights = g.Point(1,1.2,0.5),
|
|
125
125
|
tp_weights = g.Point(0.6,0.6,0.6),
|
|
126
|
-
) -> Tuple
|
|
126
|
+
) -> Tuple[float, Self]:
|
|
127
127
|
"""Perform a temporal alignment between two sections. return the flown section with labels
|
|
128
128
|
copied from the template along the warped path.
|
|
129
129
|
"""
|
|
@@ -147,8 +147,6 @@ class State(Table):
|
|
|
147
147
|
|
|
148
148
|
return distance, State.copy_labels(template, flown, path, 3)
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
150
|
def splitter_labels(self: State, mans: List[dict], better_names: List[str]=None) -> State:
|
|
153
151
|
"""label the manoeuvres in a State based on the flight coach splitter information
|
|
154
152
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: flightdata
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: Module for handling UAV flight log data
|
|
5
|
+
Home-page: https://github.com/PyFlightCoach/FlightData
|
|
6
|
+
Author: Thomas David
|
|
7
|
+
Author-email: thomasdavid0@gmail.com
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: simplejson
|
|
13
|
+
Requires-Dist: pfc-geometry>=0.2.5
|
|
14
|
+
Requires-Dist: ardupilot-log-reader>=0.3.1
|
|
15
|
+
|
|
16
|
+
## FlightData
|
|
17
|
+
This repo is contains a set of datastructures and tools for handling flight log data.
|
|
18
|
+
|
|
19
|
+
### Flight
|
|
20
|
+
The Flight object represents the data logged by a flight controller. The class wraps a pandas dataframe which is indexed on a single time axis. Where data is logged at different rates for different sensors it is mapped to the closest time index. Attribute access provides individual columns or sets of columns in the groups defined in Fields. Item access subsets the data in the time axis.
|
|
21
|
+
|
|
22
|
+
### Table
|
|
23
|
+
The Table is the base type for most of the datastructures. It allows attribute access to individual columns. Attribute access is also available to return basic entities subclassed from the base type in the pfc-geometry package. For example in the state object table.x provides the x position, table.pos provides a Point representing the xyz position. columns that are not represented by geometric base types are considered to be labels for the data.
|
|
24
|
+
|
|
25
|
+
### State
|
|
26
|
+
The State object is a table representing the position and orientation of the aircraft along with their derivatives, it can be constructed from a Flight or from scratch by extrapolating in lines or around arcs. Many tools are provided to manipulate the data. The position and attitude are in a reference frame (with Z up), the derivatives move with the aircraft in either the body, wind, stability or track (like the wind axis but with no wind) frame.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Further documentation will be provided here: https://pfcdocumentation.readthedocs.io/pyflightcoach/flightdata.html
|