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.
Files changed (41) hide show
  1. flightdata-0.2.7/PKG-INFO +29 -0
  2. flightdata-0.2.7/README.md +14 -0
  3. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/collection.py +7 -4
  4. flightdata-0.2.7/flightdata/base/labeling.py +9 -0
  5. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/table.py +19 -28
  6. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/flight.py +101 -52
  7. flightdata-0.2.7/flightdata/flight/parameters.py +12 -0
  8. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/origin.py +17 -18
  9. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/state.py +7 -9
  10. flightdata-0.2.7/flightdata.egg-info/PKG-INFO +29 -0
  11. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata.egg-info/SOURCES.txt +4 -0
  12. flightdata-0.2.7/flightdata.egg-info/requires.txt +5 -0
  13. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata.egg-info/top_level.txt +1 -0
  14. flightdata-0.2.7/scripts/collect_logs.py +28 -0
  15. flightdata-0.2.7/scripts/flightline.py +57 -0
  16. {flightdata-0.2.5 → flightdata-0.2.7}/setup.cfg +10 -2
  17. {flightdata-0.2.5 → flightdata-0.2.7}/test/test_flight.py +22 -8
  18. {flightdata-0.2.5 → flightdata-0.2.7}/test/test_origin.py +2 -2
  19. flightdata-0.2.5/PKG-INFO +0 -171
  20. flightdata-0.2.5/README.md +0 -158
  21. flightdata-0.2.5/flightdata.egg-info/PKG-INFO +0 -171
  22. flightdata-0.2.5/flightdata.egg-info/requires.txt +0 -3
  23. {flightdata-0.2.5 → flightdata-0.2.7}/LICENSE +0 -0
  24. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/__init__.py +0 -0
  25. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/__init__.py +0 -0
  26. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/constructs.py +0 -0
  27. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/base/numpy_encoder.py +0 -0
  28. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/coefficients.py +0 -0
  29. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/environment/__init__.py +0 -0
  30. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/environment/environment.py +0 -0
  31. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/environment/wind.py +0 -0
  32. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/__init__.py +0 -0
  33. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/ardupilot.py +0 -0
  34. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flight/fields.py +0 -0
  35. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/flow.py +0 -0
  36. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/model/__init__.py +0 -0
  37. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/model/aerodynamic.py +0 -0
  38. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata/model/thrust.py +0 -0
  39. {flightdata-0.2.5 → flightdata-0.2.7}/flightdata.egg-info/dependency_links.txt +0 -0
  40. {flightdata-0.2.5 → flightdata-0.2.7}/setup.py +0 -0
  41. {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 Dict, List, Union, Any, Self, TypeVar, Iterable
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) -> List[T]:
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())
@@ -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
@@ -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 Union, Self, Tuple
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, ["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):
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 c in self.constructs.cols()]
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) -> Union[pd.DataFrame, Base]:
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, 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
@@ -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)-2<min_len):
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) -> Union[int, float]:
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 scipy.signal import butter, filtfilt
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: list = None, origin: Origin = None, primary_pos_source='gps'):
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
- 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])
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) -> Self:
69
- if isinstance(sli, int) or isinstance(sli, float):
70
- return self.data.iloc[self.data.index.get_loc(sli)]
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
- return Flight(self.data.loc[sli], self.parameters, self.origin, self.primary_pos_source)
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.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
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.reset_index(drop=True)
88
- .set_index('time_flight', drop=False)
89
- .loc[sli],
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
- with open(file, 'r') as f:
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('time_index', drop=False)
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.tail(1).index.item()
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 as ex:
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
- 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:
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 None
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.rate.timestamp,
267
- axisrate_roll = parser.rate.R,
268
- axisrate_pitch = parser.rate.P,
269
- axisrate_yaw = parser.rate.Y,
270
- desrate_roll = parser.rate.RDes,
271
- desrate_pitch = parser.rate.PDes,
272
- 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,
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), parser.parms, origin, ppsorce)
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
@@ -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
+
@@ -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
- oformat = lambda val: "{}".format(val)
101
-
102
- return "\n".join([
103
- "Emailed box data for F3A Zone Pro - please DON'T modify!",
104
- self.name,
105
- oformat(self.pilot_position.lat[0]),
106
- oformat(self.pilot_position.long[0]),
107
- oformat(centre.lat[0]),
108
- oformat(centre.long[0]),
109
- "120"
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]), 0),
123
- g.GPS(float(lines[4]), float(lines[5]), 0)
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']), 0),
131
- g.GPS(float(data['centerLat']), float(data['centerLng']), 0)
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(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
 
@@ -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