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.
Files changed (38) hide show
  1. {flightdata-0.2.6 → flightdata-0.2.8}/PKG-INFO +3 -4
  2. {flightdata-0.2.6 → flightdata-0.2.8}/README.md +0 -1
  3. flightdata-0.2.8/flightdata/base/labeling.py +9 -0
  4. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/table.py +10 -20
  5. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/flight.py +101 -51
  6. flightdata-0.2.8/flightdata/flight/parameters.py +12 -0
  7. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/state.py +7 -9
  8. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/PKG-INFO +3 -4
  9. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/SOURCES.txt +2 -0
  10. flightdata-0.2.8/flightdata.egg-info/requires.txt +5 -0
  11. {flightdata-0.2.6 → flightdata-0.2.8}/scripts/flightline.py +5 -3
  12. {flightdata-0.2.6 → flightdata-0.2.8}/setup.cfg +3 -3
  13. {flightdata-0.2.6 → flightdata-0.2.8}/test/test_flight.py +22 -8
  14. {flightdata-0.2.6 → flightdata-0.2.8}/test/test_origin.py +2 -2
  15. flightdata-0.2.6/flightdata.egg-info/requires.txt +0 -5
  16. {flightdata-0.2.6 → flightdata-0.2.8}/LICENSE +0 -0
  17. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/__init__.py +0 -0
  18. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/__init__.py +0 -0
  19. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/collection.py +0 -0
  20. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/constructs.py +0 -0
  21. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/base/numpy_encoder.py +0 -0
  22. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/coefficients.py +0 -0
  23. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/environment/__init__.py +0 -0
  24. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/environment/environment.py +0 -0
  25. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/environment/wind.py +0 -0
  26. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/__init__.py +0 -0
  27. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/ardupilot.py +0 -0
  28. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flight/fields.py +0 -0
  29. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/flow.py +0 -0
  30. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/model/__init__.py +0 -0
  31. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/model/aerodynamic.py +0 -0
  32. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/model/thrust.py +0 -0
  33. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata/origin.py +0 -0
  34. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/dependency_links.txt +0 -0
  35. {flightdata-0.2.6 → flightdata-0.2.8}/flightdata.egg-info/top_level.txt +0 -0
  36. {flightdata-0.2.6 → flightdata-0.2.8}/scripts/collect_logs.py +0 -0
  37. {flightdata-0.2.6 → flightdata-0.2.8}/setup.py +0 -0
  38. {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.6
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.4
14
- Requires-Dist: ardupilot-log-reader>=0.2.1
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
 
@@ -0,0 +1,9 @@
1
+
2
+
3
+
4
+ def get_appended_id(source: str, seperator='_'):
5
+ try:
6
+ sloc = source.rfind(seperator)
7
+ return source[:sloc], source[sloc+1:]
8
+ except Exception:
9
+ return source, None
@@ -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, ["t", "dt"] , make_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, Number):
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 scipy.signal import butter, filtfilt
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: list = None, origin: Origin = None, primary_pos_source='gps'):
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
- try:
52
- if isinstance(cols, Field):
53
- return self.data[cols.col]
54
- else:
55
- return self.data.loc[:, [f.col for f in cols if f.col in self.data.columns]]
56
- except KeyError:
57
- if isinstance(cols, Field):
58
- cols = [cols]
59
- return pd.DataFrame(data=np.empty((len(self), len(cols))),columns=[f.col for f in cols])
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) -> Self:
69
- if isinstance(sli, int) or isinstance(sli, float):
70
- return self.data.iloc[self.data.index.get_loc(sli)]
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
- return Flight(self.data.loc[sli], self.parameters, self.origin, self.primary_pos_source)
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.reset_index(drop=True)
80
- .set_index('time_actual', drop=False)
81
- .loc[sli].set_index("time_flight", drop=False),
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.reset_index(drop=True)
88
- .set_index('time_flight', drop=False)
89
- .loc[sli],
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('time_index', drop=False)
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.tail(1).index.item()
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 as ex:
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
- if parser.parms['AHRS_EKF_TYPE'] == 2:
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 None
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.rate.timestamp,
266
- axisrate_roll = parser.rate.R,
267
- axisrate_pitch = parser.rate.P,
268
- axisrate_yaw = parser.rate.Y,
269
- desrate_roll = parser.rate.RDes,
270
- desrate_pitch = parser.rate.PDes,
271
- desrate_yaw = parser.rate.YDes,
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), parser.parms, origin, ppsorce)
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
@@ -0,0 +1,12 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class Parameters:
8
+ parms: dict[str, pd.DataFrame]
9
+
10
+ def __getattr(self, name: str) -> np.Any:
11
+ return self.parms[name]
12
+
@@ -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(float, Self):
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.6
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.4
14
- Requires-Dist: ardupilot-log-reader>=0.2.1
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
@@ -0,0 +1,5 @@
1
+ numpy
2
+ pandas
3
+ simplejson
4
+ pfc-geometry>=0.2.5
5
+ ardupilot-log-reader>=0.3.1
@@ -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 = np.cumsum(c6on.time_flight.diff() >=1)
11
- return Origin.from_points("new", GPS(c6on.gps[groups==0])[-1], GPS(c6on.gps[groups==1])[-1])
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.6
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.4
23
- ardupilot-log-reader >=0.2.1
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/test_inputs/00000137.BIN',
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('test/test_inputs/00000137.BIN')
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/test_inputs/00000137.json')
20
+ return Flight.from_fc_json('test/data/p23_fc.json')
22
21
 
23
22
  def test_duration(fl):
24
- assert fl.duration == approx(685, rel=1e-3)
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
- assert fl2.parameters == approx(fl.parameters)
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")
@@ -1,5 +0,0 @@
1
- numpy
2
- pandas
3
- simplejson
4
- pfc-geometry>=0.2.4
5
- ardupilot-log-reader>=0.2.1
File without changes
File without changes