flightdata 0.2.6__tar.gz → 0.2.8__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.6 → flightdata-0.2.8}/PKG-INFO +3 -4
- {flightdata-0.2.6 → flightdata-0.2.8}/README.md +0 -1
- flightdata-0.2.8/flightdata/base/labeling.py +9 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/table.py +10 -20
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/flight.py +101 -51
- flightdata-0.2.8/flightdata/flight/parameters.py +12 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/state.py +7 -9
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/PKG-INFO +3 -4
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/SOURCES.txt +2 -0
- flightdata-0.2.8/flightdata.egg-info/requires.txt +5 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/scripts/flightline.py +5 -3
- {flightdata-0.2.6 → flightdata-0.2.8}/setup.cfg +3 -3
- {flightdata-0.2.6 → flightdata-0.2.8}/test/test_flight.py +22 -8
- {flightdata-0.2.6 → flightdata-0.2.8}/test/test_origin.py +2 -2
- flightdata-0.2.6/flightdata.egg-info/requires.txt +0 -5
- {flightdata-0.2.6 → flightdata-0.2.8}/LICENSE +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/__init__.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/__init__.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/collection.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/constructs.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/numpy_encoder.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/coefficients.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/environment/__init__.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/environment/environment.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/environment/wind.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/__init__.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/ardupilot.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/fields.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flow.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/model/__init__.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/model/aerodynamic.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/model/thrust.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/origin.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/dependency_links.txt +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/top_level.txt +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/scripts/collect_logs.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/setup.py +0 -0
- {flightdata-0.2.6 → flightdata-0.2.8}/test/test_fields.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flightdata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Module for handling UAV flight log data
|
|
5
5
|
Home-page: https://github.com/PyFlightCoach/FlightData
|
|
6
6
|
Author: Thomas David
|
|
@@ -10,8 +10,8 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: pandas
|
|
12
12
|
Requires-Dist: simplejson
|
|
13
|
-
Requires-Dist: pfc-geometry>=0.2.
|
|
14
|
-
Requires-Dist: ardupilot-log-reader>=0.
|
|
13
|
+
Requires-Dist: pfc-geometry>=0.2.5
|
|
14
|
+
Requires-Dist: ardupilot-log-reader>=0.3.1
|
|
15
15
|
|
|
16
16
|
## FlightData
|
|
17
17
|
This repo is contains a set of datastructures and tools for handling flight log data.
|
|
@@ -19,7 +19,6 @@ This repo is contains a set of datastructures and tools for handling flight log
|
|
|
19
19
|
### Flight
|
|
20
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
21
|
|
|
22
|
-
|
|
23
22
|
### Table
|
|
24
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.
|
|
25
24
|
|
|
@@ -4,7 +4,6 @@ This repo is contains a set of datastructures and tools for handling flight log
|
|
|
4
4
|
### Flight
|
|
5
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
6
|
|
|
7
|
-
|
|
8
7
|
### Table
|
|
9
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.
|
|
10
9
|
|
|
@@ -9,13 +9,9 @@ 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):
|
|
@@ -25,13 +21,11 @@ class Table:
|
|
|
25
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
|
|
@@ -75,15 +69,18 @@ class Table:
|
|
|
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
|
|
@@ -342,7 +333,6 @@ class Table:
|
|
|
342
333
|
strdf = df.copy()
|
|
343
334
|
strdf['indexer'] = strdf['indexer'].astype(int).astype(str)
|
|
344
335
|
strdf = strdf.stack().groupby(level=0).apply('_'.join)
|
|
345
|
-
strdf.loc[df.indexer==0] = df[0]
|
|
346
336
|
return strdf.values
|
|
347
337
|
|
|
348
338
|
@staticmethod
|
|
@@ -19,12 +19,10 @@ from geometry.testing import assert_almost_equal
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from time import time
|
|
21
21
|
from json import load, dump
|
|
22
|
-
from ardupilot_log_reader.reader import Ardupilot
|
|
23
22
|
from flightdata.base.numpy_encoder import NumpyEncoder
|
|
24
|
-
from flightdata.flight.fields import Fields
|
|
25
23
|
from .ardupilot import flightmodes
|
|
26
24
|
from flightdata import Origin
|
|
27
|
-
from
|
|
25
|
+
from numbers import Number
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
class Flight:
|
|
@@ -34,30 +32,52 @@ class Flight:
|
|
|
34
32
|
'ARSP', 'GPS', 'RCIN', 'RCOU', 'BARO', 'MODE',
|
|
35
33
|
'RPM', 'MAG', 'BAT', 'BAT2', 'VEL', 'ORGN', 'ESC', 'CURRENT']
|
|
36
34
|
|
|
37
|
-
def __init__(self, data: pd.DataFrame, parameters:
|
|
35
|
+
def __init__(self, data: pd.DataFrame, parameters: pd.DataFrame = None, origin: Origin = None, primary_pos_source='gps'):
|
|
38
36
|
self.data = data
|
|
39
37
|
self.parameters = parameters
|
|
40
|
-
self.data.index = self.data.index - self.data.index[0]
|
|
41
|
-
self.data.index.name = 'time_index'
|
|
42
38
|
self.origin = origin
|
|
43
39
|
self.primary_pos_source = primary_pos_source
|
|
44
40
|
|
|
45
41
|
def __getattr__(self, name):
|
|
42
|
+
if self.parameters is not None:
|
|
43
|
+
if name in self.parameters.parameter.unique():
|
|
44
|
+
df = self.parameters.loc[self.parameters.parameter==name]
|
|
45
|
+
return df.loc[df.value != df.value.shift()]
|
|
46
46
|
cols = getattr(fields, name)
|
|
47
47
|
if cols is None:
|
|
48
48
|
cols = [f for f in self.data.columns if f.startswith(name)]
|
|
49
49
|
if len(cols) > 0:
|
|
50
50
|
return self.data[cols]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
cols
|
|
59
|
-
|
|
51
|
+
else:
|
|
52
|
+
try:
|
|
53
|
+
if isinstance(cols, Field):
|
|
54
|
+
return self.data[cols.col]
|
|
55
|
+
else:
|
|
56
|
+
return self.data.loc[:, [f.col for f in cols if f.col in self.data.columns]]
|
|
57
|
+
except KeyError:
|
|
58
|
+
if isinstance(cols, Field):
|
|
59
|
+
cols = [cols]
|
|
60
|
+
return pd.DataFrame(data=np.empty((len(self), len(cols))),columns=[f.col for f in cols])
|
|
61
|
+
raise AttributeError(f"'Flight' object has no attribute '{name}'")
|
|
62
|
+
|
|
63
|
+
def make_param_labels(self, pname: str, prefix:str=None, suffix:str=None, unknown=''):
|
|
64
|
+
'''Make a series with the parameter values at the correct times.'''
|
|
65
|
+
ser = pd.Series(np.nan, index=self.data.index, name=pname)
|
|
66
|
+
param = getattr(self, pname)
|
|
67
|
+
ser.iloc[ser.index.get_indexer(param.index, 'nearest')] = param.value
|
|
68
|
+
ser = ser.ffill()
|
|
60
69
|
|
|
70
|
+
if prefix or suffix:
|
|
71
|
+
sout = pd.Series(unknown, index=self.data.index, name=pname)
|
|
72
|
+
sout[~np.isnan(ser)] = (prefix or '') + ser[~np.isnan(ser)].astype(str) + (suffix or '')
|
|
73
|
+
return sout
|
|
74
|
+
else:
|
|
75
|
+
return ser
|
|
76
|
+
|
|
77
|
+
def make_param_df(self, pnames: list[str]):
|
|
78
|
+
'''Make a dataframe of parameter values'''
|
|
79
|
+
return pd.DataFrame([self.make_param_labels(p) for p in pnames]).T
|
|
80
|
+
|
|
61
81
|
def contains(self, name: Union[str, list[str]]):
|
|
62
82
|
cols = getattr(fields, name)
|
|
63
83
|
if isinstance(cols, Field):
|
|
@@ -65,35 +85,55 @@ class Flight:
|
|
|
65
85
|
else:
|
|
66
86
|
return [f.column in self.data.columns for f in cols]
|
|
67
87
|
|
|
68
|
-
def __getitem__(self, sli) ->
|
|
69
|
-
if isinstance(sli,
|
|
70
|
-
|
|
88
|
+
def __getitem__(self, sli: Number | slice) -> Flight:
|
|
89
|
+
if isinstance(sli, Number):
|
|
90
|
+
if sli < 0:
|
|
91
|
+
return self.data.iloc[sli]
|
|
92
|
+
else:
|
|
93
|
+
gl = self.data.index.get_loc(sli + self.data.index[0])
|
|
94
|
+
return Flight(
|
|
95
|
+
self.data.iloc[gl],
|
|
96
|
+
self.parameters.loc[:gl],
|
|
97
|
+
)
|
|
98
|
+
elif isinstance(sli, slice):
|
|
99
|
+
return Flight(
|
|
100
|
+
self.data.loc[slice(
|
|
101
|
+
None if sli.start is None else sli.start + self.data.index[0],
|
|
102
|
+
None if sli.stop is None else sli.stop + self.data.index[0],
|
|
103
|
+
sli.step
|
|
104
|
+
)],
|
|
105
|
+
self.parameters.loc[:None if sli.stop is None else sli.stop + self.data.index[0]],
|
|
106
|
+
self.origin, self.primary_pos_source
|
|
107
|
+
)
|
|
71
108
|
else:
|
|
72
|
-
|
|
109
|
+
raise TypeError(f'Expected a number or a slice, got a {sli.__class__.__name__}')
|
|
73
110
|
|
|
74
111
|
def __len__(self):
|
|
75
112
|
return len(self.data)
|
|
76
113
|
|
|
77
|
-
def slice_raw_t(self, sli):
|
|
114
|
+
def slice_raw_t(self, sli: Number | slice) -> Flight:
|
|
115
|
+
def opp(df: pd.DataFrame, indexer: Number | slice):
|
|
116
|
+
return df.reset_index(drop=True) \
|
|
117
|
+
.set_index('time_actual', drop=False) \
|
|
118
|
+
.loc[indexer].set_index("time_flight", drop=False)
|
|
119
|
+
|
|
78
120
|
return Flight(
|
|
79
|
-
self.data
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.parameters, self.origin, self.primary_pos_source
|
|
121
|
+
opp(self.data, sli),
|
|
122
|
+
opp(self.parameters, slice(None, sli if isinstance(sli, Number) else sli.stop, None)),
|
|
123
|
+
self.origin, self.primary_pos_source
|
|
83
124
|
)
|
|
84
125
|
|
|
85
|
-
def slice_time_flight(self, sli):
|
|
126
|
+
def slice_time_flight(self, sli) -> Flight:
|
|
86
127
|
return Flight(
|
|
87
|
-
self.data.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
self.parameters, self.origin, self.primary_pos_source
|
|
128
|
+
self.data.loc[sli],
|
|
129
|
+
self.parameters.loc[:sli if isinstance(sli, Number) else sli.stop],
|
|
130
|
+
self.origin, self.primary_pos_source
|
|
91
131
|
)
|
|
92
132
|
|
|
93
|
-
def copy(self, **kwargs):
|
|
133
|
+
def copy(self, **kwargs) -> Flight:
|
|
94
134
|
return Flight(
|
|
95
135
|
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,
|
|
136
|
+
kwargs['parameters'] if 'parameters' in kwargs else self.parameters.copy() if self.parameters is not None else None,
|
|
97
137
|
kwargs['origin'] if 'origin' in kwargs else self.origin.copy(),
|
|
98
138
|
kwargs['primary_pos_source'] if 'primary_pos_source' in kwargs else self.primary_pos_source
|
|
99
139
|
)
|
|
@@ -101,16 +141,16 @@ class Flight:
|
|
|
101
141
|
def to_dict(self):
|
|
102
142
|
return {
|
|
103
143
|
'data': self.data.to_dict('list'),
|
|
104
|
-
'parameters': self.parameters,
|
|
144
|
+
'parameters': self.parameters.to_dict('list'),
|
|
105
145
|
'origin': self.origin.to_dict(),
|
|
106
146
|
'primary_pos_source': self.primary_pos_source
|
|
107
147
|
}
|
|
108
148
|
|
|
109
149
|
@staticmethod
|
|
110
|
-
def from_dict(data: dict):
|
|
150
|
+
def from_dict(data: dict) -> Flight:
|
|
111
151
|
return Flight(
|
|
112
152
|
data=pd.DataFrame.from_dict(data['data']).set_index('time_flight', drop=False),
|
|
113
|
-
parameters=data['parameters'],
|
|
153
|
+
parameters=pd.DataFrame.from_dict(data['parameters']).set_index('time_flight', drop=False),
|
|
114
154
|
origin=Origin.from_dict(data['origin']),
|
|
115
155
|
primary_pos_source=data['primary_pos_source']
|
|
116
156
|
)
|
|
@@ -125,7 +165,7 @@ class Flight:
|
|
|
125
165
|
return Flight.from_dict(load(open(file, 'r')))
|
|
126
166
|
|
|
127
167
|
@staticmethod
|
|
128
|
-
def build_cols(**kwargs):
|
|
168
|
+
def build_cols(**kwargs) -> pd.DataFrame:
|
|
129
169
|
df = pd.DataFrame(columns=list(fields.data.keys()))
|
|
130
170
|
for k, v in kwargs.items():
|
|
131
171
|
df[k] = v
|
|
@@ -150,7 +190,7 @@ class Flight:
|
|
|
150
190
|
otf,
|
|
151
191
|
fl.data.reset_index(),
|
|
152
192
|
on='time_actual'
|
|
153
|
-
).set_index('
|
|
193
|
+
).set_index('time_flight', drop=False)
|
|
154
194
|
))
|
|
155
195
|
|
|
156
196
|
return flos
|
|
@@ -172,7 +212,7 @@ class Flight:
|
|
|
172
212
|
|
|
173
213
|
@property
|
|
174
214
|
def duration(self):
|
|
175
|
-
return self.data.
|
|
215
|
+
return self.data.iloc[-1].name - self.data.iloc[0].name
|
|
176
216
|
|
|
177
217
|
def flying_only(self, minalt=5, minv=10):
|
|
178
218
|
vs = abs(Point(self.velocity))
|
|
@@ -194,29 +234,37 @@ class Flight:
|
|
|
194
234
|
pd.testing.assert_frame_equal(self.data, other.data)
|
|
195
235
|
assert_almost_equal(self.origin.pos, other.origin.pos)
|
|
196
236
|
assert self.origin.heading == other.origin.heading
|
|
237
|
+
pd.testing.assert_frame_equal(self.parameters, other.parameters)
|
|
197
238
|
return True
|
|
198
|
-
except Exception
|
|
239
|
+
except Exception:
|
|
199
240
|
return False
|
|
200
241
|
|
|
201
242
|
@staticmethod
|
|
202
243
|
def from_log(log:Union[Ardupilot, str], extra_types: list[str] = None, **kwargs) -> Flight:
|
|
203
244
|
"""Constructor from an ardupilot bin file."""
|
|
204
|
-
|
|
245
|
+
from ardupilot_log_reader.reader import Ardupilot
|
|
205
246
|
extra_types = [] if extra_types is None else extra_types
|
|
206
247
|
|
|
207
248
|
if isinstance(log, str) or isinstance(log, Path):
|
|
208
|
-
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)))
|
|
209
250
|
else:
|
|
210
251
|
parser = log
|
|
211
|
-
|
|
212
|
-
|
|
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:
|
|
213
261
|
ekf1 = 'NKF1'
|
|
214
262
|
ekf2 = 'NKF2'
|
|
215
263
|
else:
|
|
216
264
|
ekf1 = 'XKF1'
|
|
217
265
|
ekf2 = 'XKF2'
|
|
218
266
|
|
|
219
|
-
ekf1 = parser.dfs[ekf1] if ekf1 in parser.dfs else
|
|
267
|
+
ekf1 = parser.dfs[ekf1] if ekf1 in parser.dfs else None
|
|
220
268
|
ekf2 = parser.dfs[ekf2] if ekf2 in parser.dfs else None
|
|
221
269
|
|
|
222
270
|
dfs = []
|
|
@@ -262,13 +310,13 @@ class Flight:
|
|
|
262
310
|
}, 'C')
|
|
263
311
|
if 'RATE' in parser.dfs:
|
|
264
312
|
dfs.append(Flight.build_cols(
|
|
265
|
-
time_actual = parser.
|
|
266
|
-
axisrate_roll = parser.
|
|
267
|
-
axisrate_pitch = parser.
|
|
268
|
-
axisrate_yaw = parser.
|
|
269
|
-
desrate_roll = parser.
|
|
270
|
-
desrate_pitch = parser.
|
|
271
|
-
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,
|
|
272
320
|
))
|
|
273
321
|
|
|
274
322
|
if 'IMU' in parser.dfs:
|
|
@@ -383,7 +431,7 @@ class Flight:
|
|
|
383
431
|
|
|
384
432
|
origin = Origin('ekf_origin', GPS(parser.ORGN.iloc[:,-3:]), 0)
|
|
385
433
|
|
|
386
|
-
return Flight(dfout.set_index('time_flight', drop=False),
|
|
434
|
+
return Flight(dfout.set_index('time_flight', drop=False), params, origin, ppsorce)
|
|
387
435
|
|
|
388
436
|
@staticmethod
|
|
389
437
|
def parse_instances(indf: pd.DataFrame, colmap:dict[str, str], instancecol='Instance'):
|
|
@@ -451,6 +499,7 @@ class Flight:
|
|
|
451
499
|
)
|
|
452
500
|
|
|
453
501
|
def filter(self, b, a):
|
|
502
|
+
from scipy.signal import filtfilt
|
|
454
503
|
dont_filter = [c for c in fields.get_cols(['time', 'flightmode', 'rcin', 'rcout']) if c in self.data.columns]
|
|
455
504
|
unwrap_cols = [c for c in fields.get_cols(['attitude']) if c in self.data.columns]
|
|
456
505
|
|
|
@@ -468,6 +517,7 @@ class Flight:
|
|
|
468
517
|
)
|
|
469
518
|
|
|
470
519
|
def butter_filter(self, cutoff, order=5):
|
|
520
|
+
from scipy.signal import butter
|
|
471
521
|
ts = self.time_flight.to_numpy()
|
|
472
522
|
N = len(self)
|
|
473
523
|
T = (ts[-1] - ts[0]) / N
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flightdata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Module for handling UAV flight log data
|
|
5
5
|
Home-page: https://github.com/PyFlightCoach/FlightData
|
|
6
6
|
Author: Thomas David
|
|
@@ -10,8 +10,8 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: pandas
|
|
12
12
|
Requires-Dist: simplejson
|
|
13
|
-
Requires-Dist: pfc-geometry>=0.2.
|
|
14
|
-
Requires-Dist: ardupilot-log-reader>=0.
|
|
13
|
+
Requires-Dist: pfc-geometry>=0.2.5
|
|
14
|
+
Requires-Dist: ardupilot-log-reader>=0.3.1
|
|
15
15
|
|
|
16
16
|
## FlightData
|
|
17
17
|
This repo is contains a set of datastructures and tools for handling flight log data.
|
|
@@ -19,7 +19,6 @@ This repo is contains a set of datastructures and tools for handling flight log
|
|
|
19
19
|
### Flight
|
|
20
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
21
|
|
|
22
|
-
|
|
23
22
|
### Table
|
|
24
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.
|
|
25
24
|
|
|
@@ -15,6 +15,7 @@ flightdata.egg-info/top_level.txt
|
|
|
15
15
|
flightdata/base/__init__.py
|
|
16
16
|
flightdata/base/collection.py
|
|
17
17
|
flightdata/base/constructs.py
|
|
18
|
+
flightdata/base/labeling.py
|
|
18
19
|
flightdata/base/numpy_encoder.py
|
|
19
20
|
flightdata/base/table.py
|
|
20
21
|
flightdata/environment/__init__.py
|
|
@@ -24,6 +25,7 @@ flightdata/flight/__init__.py
|
|
|
24
25
|
flightdata/flight/ardupilot.py
|
|
25
26
|
flightdata/flight/fields.py
|
|
26
27
|
flightdata/flight/flight.py
|
|
28
|
+
flightdata/flight/parameters.py
|
|
27
29
|
flightdata/model/__init__.py
|
|
28
30
|
flightdata/model/aerodynamic.py
|
|
29
31
|
flightdata/model/thrust.py
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from flightdata import Flight, Origin
|
|
2
2
|
from geometry import GPS
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
import numpy as np
|
|
5
4
|
import argparse
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
def box_from_log(log: Flight, channel: int):
|
|
9
8
|
c6on = Flight(log.data.loc[log.data[f'rcin_c{channel}']>=1500])
|
|
10
|
-
groups =
|
|
11
|
-
|
|
9
|
+
groups = (c6on.time_flight.diff() > 1).cumsum()
|
|
10
|
+
pilot = Flight(c6on.data.loc[groups==0])
|
|
11
|
+
centre = Flight(c6on.data.loc[groups==1])
|
|
12
|
+
|
|
13
|
+
return Origin.from_points("new", GPS(pilot.gps)[-1], GPS(centre.gps)[-1])
|
|
12
14
|
|
|
13
15
|
def box_from_logs(pilot: Flight, centre: Flight):
|
|
14
16
|
return Origin.from_points("new", GPS(*pilot.gps.iloc[-1]), GPS(*centre.gps.iloc[-1]))
|
|
@@ -5,7 +5,7 @@ author_email = thomasdavid0@gmail.com
|
|
|
5
5
|
description = Module for handling UAV flight log data
|
|
6
6
|
long_description = file: README.md
|
|
7
7
|
long_description_content_type = text/markdown
|
|
8
|
-
version = 0.2.
|
|
8
|
+
version = 0.2.8
|
|
9
9
|
url = https://github.com/PyFlightCoach/FlightData
|
|
10
10
|
|
|
11
11
|
[options]
|
|
@@ -19,8 +19,8 @@ install_requires =
|
|
|
19
19
|
numpy
|
|
20
20
|
pandas
|
|
21
21
|
simplejson
|
|
22
|
-
pfc-geometry >=0.2.
|
|
23
|
-
ardupilot-log-reader >=0.
|
|
22
|
+
pfc-geometry >=0.2.5
|
|
23
|
+
ardupilot-log-reader >=0.3.1
|
|
24
24
|
|
|
25
25
|
[egg_info]
|
|
26
26
|
tag_build =
|
|
@@ -9,19 +9,18 @@ from ardupilot_log_reader import Ardupilot
|
|
|
9
9
|
|
|
10
10
|
@fixture(scope='session')
|
|
11
11
|
def parser():
|
|
12
|
-
return Ardupilot('test/
|
|
13
|
-
types=Flight.ardupilot_types)
|
|
12
|
+
return Ardupilot.parse('test/data/p23.BIN',types=Flight.ardupilot_types)
|
|
14
13
|
|
|
15
14
|
@fixture(scope='session')
|
|
16
|
-
def fl():
|
|
17
|
-
return Flight.from_log(
|
|
15
|
+
def fl(parser):
|
|
16
|
+
return Flight.from_log(parser)
|
|
18
17
|
|
|
19
18
|
@fixture(scope='session')
|
|
20
19
|
def fcj():
|
|
21
|
-
return Flight.from_fc_json('test/
|
|
20
|
+
return Flight.from_fc_json('test/data/p23_fc.json')
|
|
22
21
|
|
|
23
22
|
def test_duration(fl):
|
|
24
|
-
assert fl.duration == approx(
|
|
23
|
+
assert fl.duration == approx(687, rel=1e-3)
|
|
25
24
|
|
|
26
25
|
def test_slice(fl):
|
|
27
26
|
short_flight = fl[100:200]
|
|
@@ -31,8 +30,7 @@ def test_to_from_dict(fl):
|
|
|
31
30
|
data = fl.to_dict()
|
|
32
31
|
fl2 = Flight.from_dict(data)
|
|
33
32
|
assert fl == fl2
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
def test_from_fc_json(fcj):
|
|
37
35
|
assert isinstance(fcj, Flight)
|
|
38
36
|
assert fcj.duration > 200
|
|
@@ -101,3 +99,19 @@ def test_butter_filter(fl: Flight):
|
|
|
101
99
|
def test_remove_time_flutter(fl: Flight):
|
|
102
100
|
flf = fl.remove_time_flutter()
|
|
103
101
|
assert np.gradient(np.gradient(flf.data.index)) == approx(0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_get_parameter_attr(fl: Flight):
|
|
105
|
+
assert fl.AHRS_EKF_TYPE.iloc[0].value == 3
|
|
106
|
+
|
|
107
|
+
def test_make_param_labels(fl: Flight):
|
|
108
|
+
col = fl.make_param_labels('AHRS_EKF_TYPE')
|
|
109
|
+
|
|
110
|
+
assert len(col) == len(fl)
|
|
111
|
+
assert np.all(col.loc[~np.isnan(col)] == 3)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
col = fl.make_param_labels('AHRS_EKF_TYPE', 'Test')
|
|
115
|
+
|
|
116
|
+
assert np.all(col.loc[col!=''] == 'Test3.0')
|
|
117
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
from flightdata import Origin
|
|
3
3
|
from geometry import GPS
|
|
4
|
-
from pytest import approx
|
|
4
|
+
from pytest import approx, mark
|
|
5
5
|
from .conftest import flight
|
|
6
6
|
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ def test_to_dict(flight):
|
|
|
11
11
|
assert di["name"] == "origin"
|
|
12
12
|
assert di["pos"]['lat'] == origin.pos.lat[0]
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
@mark.skip('writes to file rather than returns string now')
|
|
15
15
|
def test_to_f3azone(origin):
|
|
16
16
|
zone_string = origin.to_f3a_zone()
|
|
17
17
|
lines = zone_string.split("\n")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|