flightdata 0.2.20__tar.gz → 0.2.22__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 (89) hide show
  1. {flightdata-0.2.20/flightdata.egg-info → flightdata-0.2.22}/PKG-INFO +3 -3
  2. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/__init__.py +5 -2
  3. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/base/constructs.py +4 -6
  4. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/bindata.py +26 -12
  5. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/flight/fields.py +1 -1
  6. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/flight/flight.py +55 -65
  7. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/origin.py +44 -16
  8. flightdata-0.2.22/flightdata/schemas/fcj.py +174 -0
  9. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/state.py +10 -10
  10. {flightdata-0.2.20 → flightdata-0.2.22/flightdata.egg-info}/PKG-INFO +3 -3
  11. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata.egg-info/SOURCES.txt +2 -0
  12. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata.egg-info/requires.txt +2 -2
  13. {flightdata-0.2.20 → flightdata-0.2.22}/requirements-dev.txt +1 -1
  14. {flightdata-0.2.20 → flightdata-0.2.22}/requirements.txt +1 -1
  15. {flightdata-0.2.20 → flightdata-0.2.22}/test/conftest.py +4 -2
  16. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_flight.py +5 -8
  17. flightdata-0.2.22/test/test_state/__init__.py +0 -0
  18. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_state/test_state_builders.py +6 -8
  19. {flightdata-0.2.20 → flightdata-0.2.22}/.github/workflows/publish_pypi.yml +0 -0
  20. {flightdata-0.2.20 → flightdata-0.2.22}/.gitignore +0 -0
  21. {flightdata-0.2.20 → flightdata-0.2.22}/.vscode/launch.json +0 -0
  22. {flightdata-0.2.20 → flightdata-0.2.22}/.vscode/settings.json +0 -0
  23. {flightdata-0.2.20 → flightdata-0.2.22}/LICENSE +0 -0
  24. {flightdata-0.2.20 → flightdata-0.2.22}/README.md +0 -0
  25. {flightdata-0.2.20 → flightdata-0.2.22}/examples/__init__.py +0 -0
  26. {flightdata-0.2.20 → flightdata-0.2.22}/examples/data/__init__.py +0 -0
  27. {flightdata-0.2.20 → flightdata-0.2.22}/examples/data/manual_F3A_F23_22_04_28_00000231.json +0 -0
  28. {flightdata-0.2.20 → flightdata-0.2.22}/examples/data/manual_F3A_P23_22_05_31_00000350.json +0 -0
  29. {flightdata-0.2.20 → flightdata-0.2.22}/examples/data/manual_F3A_P23_23_08_11_00000094.json +0 -0
  30. {flightdata-0.2.20 → flightdata-0.2.22}/examples/flight_dynamics/00000150.json +0 -0
  31. {flightdata-0.2.20 → flightdata-0.2.22}/examples/flight_dynamics/__init__.py +0 -0
  32. {flightdata-0.2.20 → flightdata-0.2.22}/examples/flight_dynamics/box.f3a +0 -0
  33. {flightdata-0.2.20 → flightdata-0.2.22}/examples/flight_dynamics/param_id.py +0 -0
  34. {flightdata-0.2.20 → flightdata-0.2.22}/examples/state_analysis/__init__.py +0 -0
  35. {flightdata-0.2.20 → flightdata-0.2.22}/examples/state_analysis/axes.py +0 -0
  36. {flightdata-0.2.20 → flightdata-0.2.22}/examples/state_analysis/state_fill_plot.py +0 -0
  37. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/base/__init__.py +0 -0
  38. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/base/collection.py +0 -0
  39. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/base/labeling.py +0 -0
  40. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/base/numpy_encoder.py +0 -0
  41. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/base/table.py +0 -0
  42. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/coefficients.py +0 -0
  43. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/environment/__init__.py +0 -0
  44. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/environment/environment.py +0 -0
  45. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/environment/wind.py +0 -0
  46. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/flight/__init__.py +0 -0
  47. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/flight/ardupilot.py +0 -0
  48. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/flight/parameters.py +0 -0
  49. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/flow.py +0 -0
  50. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/model/__init__.py +0 -0
  51. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/model/aerodynamic.py +0 -0
  52. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/model/thrust.py +0 -0
  53. {flightdata-0.2.20/test → flightdata-0.2.22/flightdata/schemas}/__init__.py +0 -0
  54. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/scripts/collect_logs.py +0 -0
  55. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata/scripts/flightline.py +0 -0
  56. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata.egg-info/dependency_links.txt +0 -0
  57. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata.egg-info/entry_points.txt +0 -0
  58. {flightdata-0.2.20 → flightdata-0.2.22}/flightdata.egg-info/top_level.txt +0 -0
  59. {flightdata-0.2.20 → flightdata-0.2.22}/pyproject.toml +0 -0
  60. {flightdata-0.2.20 → flightdata-0.2.22}/setup.cfg +0 -0
  61. {flightdata-0.2.20 → flightdata-0.2.22}/test/EmailedBox.f3a +0 -0
  62. {flightdata-0.2.20/test/base → flightdata-0.2.22/test}/__init__.py +0 -0
  63. {flightdata-0.2.20/test/test_environment → flightdata-0.2.22/test/base}/__init__.py +0 -0
  64. {flightdata-0.2.20 → flightdata-0.2.22}/test/base/test_base_constructs.py +0 -0
  65. {flightdata-0.2.20 → flightdata-0.2.22}/test/base/test_table.py +0 -0
  66. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/bin_parser_GPS.csv +0 -0
  67. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/bin_parser_POS.csv +0 -0
  68. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/make_inputs.py +0 -0
  69. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/manual_F3A_P23.json +0 -0
  70. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/p23.BIN +0 -0
  71. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/p23.json +0 -0
  72. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/p23_box.f3a +0 -0
  73. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/p23_fc.json +0 -0
  74. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/p23_flight.json +0 -0
  75. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/vtol_hover.bin +0 -0
  76. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/vtol_hover.json +0 -0
  77. {flightdata-0.2.20 → flightdata-0.2.22}/test/data/web_bin_parse.json +0 -0
  78. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_bindata.py +0 -0
  79. {flightdata-0.2.20/test/test_model → flightdata-0.2.22/test/test_environment}/__init__.py +0 -0
  80. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_environment/test_environment.py +0 -0
  81. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_environment/test_environment_wind.py +0 -0
  82. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_fields.py +0 -0
  83. {flightdata-0.2.20/test/test_state → flightdata-0.2.22/test/test_model}/__init__.py +0 -0
  84. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_model/test_model_coefficients.py +0 -0
  85. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_model/test_model_flow.py +0 -0
  86. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_origin.py +0 -0
  87. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_state/test_state.py +0 -0
  88. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_state/test_state_conversions.py +0 -0
  89. {flightdata-0.2.20 → flightdata-0.2.22}/test/test_state/test_state_measurements.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flightdata
3
- Version: 0.2.20
3
+ Version: 0.2.22
4
4
  Summary: Module for handling UAV flight log data
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
6
  License: GNU GPL v3
@@ -11,13 +11,13 @@ License-File: LICENSE
11
11
  Requires-Dist: numpy
12
12
  Requires-Dist: pandas
13
13
  Requires-Dist: simplejson
14
- Requires-Dist: pfc-geometry>=0.2.11
14
+ Requires-Dist: pfc-geometry>=0.2.12
15
15
  Requires-Dist: json_stream
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: numpy; extra == "dev"
18
18
  Requires-Dist: pandas; extra == "dev"
19
19
  Requires-Dist: simplejson; extra == "dev"
20
- Requires-Dist: pfc-geometry>=0.2.11; extra == "dev"
20
+ Requires-Dist: pfc-geometry>=0.2.12; extra == "dev"
21
21
  Requires-Dist: ardupilot-log-reader>=0.3.3; extra == "dev"
22
22
  Requires-Dist: pytest; extra == "dev"
23
23
 
@@ -1,9 +1,12 @@
1
+ from .schemas import fcj
1
2
  from .base import *
2
3
  from .bindata import BinData
3
- from .origin import Origin
4
+ from .origin import Origin, FCJOrigin
4
5
  from .flight import *
5
6
  from .environment import *
6
7
  from .coefficients import Coefficients
7
8
  from .flow import Flow, Attack
8
9
  from .state import State
9
- from .model import *
10
+ from .model import *
11
+
12
+
@@ -8,9 +8,7 @@
8
8
  rvel = rotational velocity (body axis rates)
9
9
 
10
10
  """
11
- import numpy as np
12
- import pandas as pd
13
- from typing import List, Dict, Union
11
+ from typing import Union
14
12
  from .collection import Collection
15
13
  from itertools import chain
16
14
 
@@ -31,18 +29,18 @@ class Constructs(Collection):
31
29
  super().__init__(data)
32
30
  self._cols = list(chain(*[c.keys for c in self.data.values()]))
33
31
 
34
- def subset(self, names: List[str]):
32
+ def subset(self, names: list[str]):
35
33
  """get a subset of the constructs"""
36
34
  return Constructs({key: value for key, value in self.data.items() if key in names})
37
35
 
38
- def existing(self, vars: List[str]):
36
+ def existing(self, vars: list[str]):
39
37
  """return a subset that is fully populated by the list of keys input"""
40
38
  return self.subset([
41
39
  key for key, value in self.data.items()
42
40
  if all(val in vars for val in value.keys)
43
41
  ])
44
42
 
45
- def missing(self, vars: List[str]):
43
+ def missing(self, vars: list[str]):
46
44
  """return a subset that has not been populated by the list of vars"""
47
45
  return self.subset([
48
46
  key for key, value in self.data.items()
@@ -4,7 +4,8 @@ import numpy as np
4
4
  import pandas as pd
5
5
  import logging
6
6
 
7
- logger = logging.getLogger(__name__)
7
+ logger = logging.getLogger(__name__)
8
+
8
9
 
9
10
  @dataclass
10
11
  class BinData:
@@ -15,6 +16,14 @@ class BinData:
15
16
  return self.dfs[name]
16
17
  raise AttributeError(f"No such attribute: {name}")
17
18
 
19
+ def get_dft(self, df_name: str, t: float, coren: str|None='C', coreid: int=0) -> pd.DataFrame:
20
+ bdx = self.dfs[df_name].copy()
21
+ bdx['TimeUS'] = bdx.TimeUS / 1e6
22
+ if coren and coren in bdx.columns:
23
+ bdx = bdx.loc[bdx[coren]==coreid]
24
+ bdx = bdx.set_index('TimeUS')
25
+ return bdx.iloc[bdx.index.get_indexer([t], method='nearest')]
26
+
18
27
  def parameters(self) -> dict[str, pd.DataFrame]:
19
28
  gb = self.PARM.groupby("Name")
20
29
 
@@ -35,11 +44,10 @@ class BinData:
35
44
  @staticmethod
36
45
  def parse_json(bindata: dict[str, dict[str, list]]) -> BinData:
37
46
  # dfs = {k: pd.DataFrame(v) for k,v in bindata.items()}
38
-
47
+
39
48
  dfs: dict[str, pd.DataFrame] = {}
40
49
  groups = {}
41
50
  for k, v in bindata.items():
42
-
43
51
  if k == "PARM":
44
52
  new_df = pd.DataFrame(
45
53
  [
@@ -49,17 +57,23 @@ class BinData:
49
57
  ],
50
58
  index=["time_boot_s", "Name", "Value"],
51
59
  ).T
52
- if 'Default' in v:
60
+ if "Default" in v:
53
61
  new_df["Default"] = v["Default"]
54
-
55
-
62
+ elif k == "MSG":
63
+ new_df = pd.DataFrame(
64
+ [
65
+ pd.Series(v["time_boot_s"]).reset_index(drop=True),
66
+ pd.Series(v["Message"]),
67
+ ],
68
+ index=["time_boot_s", "Message"],
69
+ ).T
56
70
  else:
57
71
  try:
58
- new_df = pd.DataFrame(v)
72
+ new_df = pd.DataFrame(v)
59
73
  except Exception as ex:
60
74
  logger.info(f"Error parsing {k}: {ex}")
61
75
  new_df = None
62
-
76
+
63
77
  if new_df is not None:
64
78
  if "[" not in k:
65
79
  dfs[k] = new_df
@@ -68,7 +82,7 @@ class BinData:
68
82
  if nk not in groups:
69
83
  groups[nk] = []
70
84
  groups[nk].append(new_df)
71
-
85
+
72
86
  for k, v in groups.items():
73
87
  dfs[k] = pd.concat(v).sort_values("time_boot_s")
74
88
 
@@ -76,12 +90,12 @@ class BinData:
76
90
  start_time = (
77
91
  BinData._gpsTimeToTime(dfs["GPS"].GWk.iloc[0], dfs["GPS"].GMS.iloc[0])
78
92
  - dfs["GPS"].time_boot_s.iloc[0]
79
- )
93
+ )
80
94
  else:
81
- start_time = 0
95
+ start_time = 0
82
96
 
83
97
  def process_df(df: pd.DataFrame) -> pd.DataFrame:
84
- df.insert(0, "TimeUS", np.floor(df.time_boot_s * 1e6)) # ms
98
+ df.insert(0, "TimeUS", np.floor(df.time_boot_s * 1e6)) # ms
85
99
  df.insert(0, "timestamp", start_time + df.time_boot_s)
86
100
 
87
101
  return df.drop(columns="time_boot_s")
@@ -29,7 +29,7 @@ class Fields:
29
29
  self.data = data
30
30
  self.groups = {}
31
31
  for v in data.values():
32
- if not v.field in self.groups:
32
+ if v.field not in self.groups:
33
33
  self.groups[v.field] = []
34
34
  self.groups[v.field].append(v)
35
35
 
@@ -21,9 +21,11 @@ from time import time
21
21
  from json import load, dump
22
22
  from flightdata.base.numpy_encoder import NumpyEncoder
23
23
  from .ardupilot import flightmodes
24
- from flightdata import Origin
24
+ from flightdata import Origin, fcj
25
25
  from numbers import Number
26
26
  from scipy.signal import filtfilt, butter
27
+ from datetime import datetime
28
+
27
29
 
28
30
  def filter(data, cutoff=25, order=5, fs=25):
29
31
  return filtfilt(
@@ -32,6 +34,7 @@ def filter(data, cutoff=25, order=5, fs=25):
32
34
  padlen=len(data) - 1,
33
35
  )
34
36
 
37
+
35
38
  class Flight:
36
39
  ardupilot_types = [
37
40
  "XKF1",
@@ -92,10 +95,11 @@ class Flight:
92
95
  ]
93
96
  except KeyError:
94
97
  if isinstance(cols, Field):
95
- cols = [cols]
96
- return pd.DataFrame(
97
- data=np.empty((len(self), len(cols))), columns=[f.col for f in cols]
98
- )
98
+ return pd.Series(np.full(len(self), np.nan), name=cols.col)
99
+ else:
100
+ return pd.DataFrame(
101
+ data=np.full((len(self), len(cols)), np.nan), columns=[f.col for f in cols]
102
+ )
99
103
  raise AttributeError(f"'Flight' object has no attribute '{name}'")
100
104
 
101
105
  def make_param_labels(
@@ -132,23 +136,17 @@ class Flight:
132
136
  if sli < 0:
133
137
  return self.data.iloc[sli]
134
138
  else:
135
- gl = self.data.index.get_loc(sli + self.data.index[0])
139
+ gl = self.data.index.get_indexer([sli], method="nearest")
136
140
  return Flight(
137
141
  self.data.iloc[gl],
138
- self.parameters.loc[:gl],
142
+ self.parameters[:sli] if self.parameters else None,
143
+ self.origin,
144
+ self.primary_pos_source,
139
145
  )
140
146
  elif isinstance(sli, slice):
141
147
  return Flight(
142
- self.data.loc[
143
- slice(
144
- None if sli.start is None else sli.start + self.data.index[0],
145
- None if sli.stop is None else sli.stop + self.data.index[0],
146
- sli.step,
147
- )
148
- ],
149
- self.parameters.loc[
150
- : None if sli.stop is None else sli.stop + self.data.index[0]
151
- ],
148
+ self.data.loc[slice(sli.start, sli.stop, sli.step)],
149
+ self.parameters[:sli.stop] if self.parameters else None,
152
150
  self.origin,
153
151
  self.primary_pos_source,
154
152
  )
@@ -312,6 +310,10 @@ class Flight:
312
310
  except Exception:
313
311
  return False
314
312
 
313
+ def boot_time(self):
314
+ timestamp = self.time_actual.iloc[0]
315
+ return datetime.fromtimestamp(timestamp) if not np.isnan(timestamp) else None
316
+
315
317
  @staticmethod
316
318
  def from_log(
317
319
  log: str,
@@ -324,8 +326,9 @@ class Flight:
324
326
  ppsource = xkf or pos
325
327
  """
326
328
  parser = log
327
- if not hasattr(log, 'dfs'):
329
+ if not hasattr(log, "dfs"):
328
330
  from ardupilot_log_reader.reader import Ardupilot
331
+
329
332
  parser = Ardupilot.parse(
330
333
  str(log),
331
334
  types=list(
@@ -400,15 +403,13 @@ class Flight:
400
403
  )
401
404
 
402
405
  for i, df in enumerate(newdfs):
403
-
404
406
  ekffs = 1 / np.mean(np.diff(df.time_actual))
405
- ps = '' if i == 0 else f"_{i}"
406
- df[f'velocity_N{ps}'] = filter(df[f'velocity_N{ps}'], 5, 5, ekffs)
407
- df[f'velocity_E{ps}'] = filter(df[f'velocity_E{ps}'], 5, 5, ekffs)
408
- df[f'velocity_D{ps}'] = filter(df[f'velocity_D{ps}'], 5, 5, ekffs)
407
+ ps = "" if i == 0 else f"_{i}"
408
+ df[f"velocity_N{ps}"] = filter(df[f"velocity_N{ps}"], 5, 5, ekffs)
409
+ df[f"velocity_E{ps}"] = filter(df[f"velocity_E{ps}"], 5, 5, ekffs)
410
+ df[f"velocity_D{ps}"] = filter(df[f"velocity_D{ps}"], 5, 5, ekffs)
409
411
  dfs.append(df)
410
412
 
411
-
412
413
  if ekf2 is not None:
413
414
  dfs = dfs + Flight.parse_instances(
414
415
  ekf2,
@@ -439,7 +440,10 @@ class Flight:
439
440
  if ekf1 is not None:
440
441
  if "C" in ekf1.columns:
441
442
  imu = pd.merge_asof(
442
- imu, ekf1.loc[ekf1.C == imu_instance], on="timestamp", direction="nearest"
443
+ imu,
444
+ ekf1.loc[ekf1.C == imu_instance],
445
+ on="timestamp",
446
+ direction="nearest",
443
447
  )
444
448
  else:
445
449
  imu = pd.merge_asof(imu, ekf1, on="timestamp", direction="nearest")
@@ -463,18 +467,18 @@ class Flight:
463
467
  imu["AccX"] = imu.AccX + imu.AX / 100
464
468
  imu["AccY"] = imu.AccY + imu.AY / 100
465
469
  imu["AccZ"] = imu.AccZ + imu.AZ / 100
466
-
470
+
467
471
  _imufs = 1 / np.mean(np.diff(imu.timestamp))
468
-
472
+
469
473
  dfs.append(
470
474
  Flight.build_cols(
471
475
  time_actual=imu.timestamp,
472
- acceleration_x=filter(imu.AccX,10, 5, _imufs),
473
- acceleration_y=filter(imu.AccY,10, 5, _imufs),
474
- acceleration_z=filter(imu.AccZ,10, 5, _imufs),
475
- axisrate_roll=filter(imu.GyrX,10, 5, _imufs),
476
- axisrate_pitch=filter(imu.GyrY,10, 5, _imufs),
477
- axisrate_yaw=filter(imu.GyrZ,10, 5, _imufs),
476
+ acceleration_x=filter(imu.AccX, 10, 5, _imufs),
477
+ acceleration_y=filter(imu.AccY, 10, 5, _imufs),
478
+ acceleration_z=filter(imu.AccZ, 10, 5, _imufs),
479
+ axisrate_roll=filter(imu.GyrX, 10, 5, _imufs),
480
+ axisrate_pitch=filter(imu.GyrY, 10, 5, _imufs),
481
+ axisrate_yaw=filter(imu.GyrZ, 10, 5, _imufs),
478
482
  )
479
483
  )
480
484
 
@@ -608,7 +612,9 @@ class Flight:
608
612
  indf: pd.DataFrame, colmap: dict[str, str], instancecol="Instance"
609
613
  ):
610
614
  """Where an instance column exists in an input df split the values into two columns"""
611
- instances = sorted(indf[instancecol].unique()) if instancecol in indf.columns else [0]
615
+ instances = (
616
+ sorted(indf[instancecol].unique()) if instancecol in indf.columns else [0]
617
+ )
612
618
  dfs = []
613
619
  for i in instances:
614
620
  _subdf = (
@@ -629,11 +635,23 @@ class Flight:
629
635
  return dfs
630
636
 
631
637
  @staticmethod
632
- def parse_fcj_data(df: pd.DataFrame | dict, origin: Origin, shift: Point = None):
633
- df = pd.DataFrame(df, dtype=float) if isinstance(df, dict) else df
638
+ def from_fc_json(fc_json: fcj.FCJ) -> Flight:
639
+ if fc_json.parameters:
640
+ origin = Origin.from_fcjson_parameters(fc_json.parameters)
641
+ shift = origin.rotation.transform_point(
642
+ Point(
643
+ fc_json.parameters.moveEast,
644
+ -fc_json.parameters.moveNorth,
645
+ 0,
646
+ )
647
+ )
648
+ else:
649
+ origin = Origin("dummy_origin", GPS(0, 0, 0), -np.pi / 2)
650
+ shift = P0()
651
+
652
+ df = pd.DataFrame([d.__dict__ for d in fc_json.data], dtype=float)
634
653
 
635
654
  df = Flight.build_cols(
636
- time_actual=df["time"] / 1e6 + int(time()),
637
655
  time_flight=df["time"] / 1e6,
638
656
  attitude_roll=np.radians(df["r"]),
639
657
  attitude_pitch=np.radians(df["p"]),
@@ -653,31 +671,6 @@ class Flight:
653
671
  df["position_D"] = df["position_D"] + shift.z
654
672
  return Flight(df.set_index("time_flight", drop=False), None, origin, "xkf_c0")
655
673
 
656
- @staticmethod
657
- def from_fc_json(fc_json: Union[str, dict, IO]) -> Self:
658
- if isinstance(fc_json, str):
659
- with open(fc_json, "r") as f:
660
- fc_json = load(f)
661
- elif isinstance(fc_json, IO):
662
- fc_json = load(f)
663
-
664
- if "parameters" in fc_json:
665
- origin = Origin.from_fcjson_parameters(fc_json["parameters"])
666
- shift = origin.rotation.transform_point(
667
- Point(
668
- fc_json["parameters"]["moveEast"],
669
- -fc_json["parameters"]["moveNorth"],
670
- 0,
671
- )
672
- )
673
- else:
674
- origin = Origin("dummy_origin", GPS(0, 0, 0), -np.pi / 2)
675
- shift = P0()
676
-
677
- return Flight.parse_fcj_data(
678
- pd.DataFrame(fc_json["data"], dtype=float), origin, shift
679
- )
680
-
681
674
  def remove_time_flutter(self):
682
675
  # I think the best option is just to take the average of the timestep.
683
676
  # FC loop rate seems consistent but there is noise in the time that is recorded
@@ -745,6 +738,3 @@ class Flight:
745
738
  T = (ts[-1] - ts[0]) / N
746
739
  fs = 1 / T
747
740
  return self.filter(*butter(order, cutoff, fs=fs, btype="low", analog=False))
748
-
749
-
750
-
@@ -5,7 +5,8 @@ from typing import Self
5
5
  import json_stream
6
6
  from pathlib import Path
7
7
  from dataclasses import dataclass
8
-
8
+ from pydantic import BaseModel
9
+ from flightdata import fcj
9
10
 
10
11
  @dataclass
11
12
  class Origin(object):
@@ -15,6 +16,18 @@ class Origin(object):
15
16
  pos: g.GPS # position of pilot
16
17
  heading: float# direction pilot faces in radians from North (clockwise)
17
18
 
19
+ @property
20
+ def lat(self):
21
+ return self.pos.lat[0]
22
+
23
+ @property
24
+ def long(self):
25
+ return self.pos.long[0]
26
+
27
+ @property
28
+ def alt(self):
29
+ return self.pos.alt[0]
30
+
18
31
  @property
19
32
  def rotation(self):
20
33
  # converts NED to x right, y heading direction, z up
@@ -72,7 +85,7 @@ class Origin(object):
72
85
  This is a convenient, but not very accurate way to setup the box.
73
86
  '''
74
87
 
75
- position = g.GPS(flight.gps_latitude[0], flight.gps_longitude[0], flight.gps_altitude[0])
88
+ position = g.GPS(flight.gps_latitude.iloc[0], flight.gps_longitude.iloc[0], flight.gps_altitude.iloc[0])
76
89
  heading = g.Euler(flight.attitude)[0].transform_point(g.PX())
77
90
 
78
91
  return Origin('origin', position, np.arctan2(heading.y, heading.x)[0])
@@ -118,26 +131,41 @@ class Origin(object):
118
131
  )
119
132
 
120
133
  @staticmethod
121
- def from_fcjson_parameters(data: dict | str | Path):
122
- if not isinstance(data, dict):
123
- data = json_stream.to_standard_types(
124
- json_stream.load(open(data, 'r'))['parameters']
125
- )
134
+ def from_fcjson_parameters(parms: fcj.Parameters):
126
135
  return Origin(
127
136
  "FCJ_box",
128
137
  g.GPS(
129
- float(data['pilotLat']),
130
- float(data['pilotLng']),
131
- float(data['pilotAlt'])
138
+ float(parms.pilotLat),
139
+ float(parms.pilotLng),
140
+ float(parms.pilotAlt)
132
141
  ),
133
- float(data['rotation'])
142
+ float(parms.rotation)
134
143
  )
135
144
 
136
-
137
-
138
145
  def gps_to_point(self, gps: g.GPS) -> g.Point:
139
- pned = gps - self.pilot_position
140
- return self.rotation.transform_point(g.Point(pned.y, pned.x, -pned.z ))
146
+ return self.rotation.transform_point(gps - self.pos)
141
147
 
142
148
 
143
-
149
+
150
+ class FCJOrigin(BaseModel):
151
+ lat: float
152
+ lng: float
153
+ alt: float
154
+ heading: float
155
+ move_east: float=0
156
+ move_north: float=0
157
+
158
+ def origin(self):
159
+ """Create a flightdata.Origin object from the FCJOrigin object."""
160
+ return Origin(
161
+ "fcj",
162
+ g.GPS(self.lat, self.lng, self.alt).offset(
163
+ g.Point(self.move_north, self.move_east, 0)
164
+ ),
165
+ np.radians(self.heading),
166
+ )
167
+
168
+ def shift(self):
169
+ return self.origin().rotation.transform_point(
170
+ g.Point(self.move_east, -self.move_north, 0)
171
+ )
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
+ import pandas as pd
4
+ import datetime
5
+ import re
6
+
7
+
8
+ class FCJ(BaseModel):
9
+ version: str
10
+ comments: str
11
+ name: str
12
+ view: View
13
+ parameters: Parameters
14
+ scored: bool
15
+ scores: list[float]
16
+ human_scores: list[HumanResult] = []
17
+ fcs_scores: list[Result] = []
18
+ mans: list[Man]
19
+ data: list[Data]
20
+ jhash: int | None = None
21
+
22
+
23
+ def score_df(self):
24
+ return pd.concat(
25
+ {fcjr.fa_version: fcjr.to_df() for fcjr in self.fcs_scores},
26
+ axis=0,
27
+ names=["version", "manoeuvre", "difficulty", "truncate"],
28
+ )
29
+
30
+ def man_df(self):
31
+ return pd.DataFrame(
32
+ [man.__dict__ for man in self.mans[1:-1]],
33
+ index=pd.Index(range(len(self.mans[1:-1])), name="manoeuvre"),
34
+ )
35
+
36
+ def pfc_version_df(self):
37
+ sdf = self.score_df().loc[pd.IndexSlice[:, :, 3, False]]
38
+ return pd.concat(
39
+ [sdf, sdf.mul(self.man_df().k, axis=0)], axis=1, keys=["raw", "kfac"]
40
+ )
41
+
42
+ def version_summary_df(self):
43
+ return self.pfc_version_df().groupby("version").kfac.sum()
44
+
45
+ def latest_version(self):
46
+ return max([fcjr.fa_version for fcjr in self.fcs_scores])
47
+
48
+ @property
49
+ def id(self):
50
+ return re.search(r"\d{8}", self.name)[0]
51
+
52
+ @property
53
+ def created(self):
54
+ try:
55
+ return datetime.datetime.strptime(
56
+ re.search(r"_\d{2}_\d{2}_\d{2}_", self.name)[0], "_%y_%m_%d_"
57
+ )
58
+ except Exception:
59
+ return None
60
+
61
+
62
+ class View(BaseModel):
63
+ position: dict
64
+ target: dict
65
+
66
+ class Parameters(BaseModel):
67
+ rotation: float
68
+ start: int
69
+ stop: int
70
+ moveEast: float
71
+ moveNorth: float
72
+ wingspan: float
73
+ modelwingspan: float
74
+ elevate: float
75
+ originLat: float
76
+ originLng: float
77
+ originAlt: float
78
+ pilotLat: str | float
79
+ pilotLng: str | float
80
+ pilotAlt: str | float
81
+ centerLat: str | float
82
+ centerLng: str | float
83
+ centerAlt: str | float
84
+ schedule: list[str]
85
+
86
+ class HumanResult(BaseModel):
87
+ name: str
88
+ date: datetime.date
89
+ scores: list[float]
90
+
91
+ class Result(BaseModel):
92
+ fa_version: str
93
+ manresults: list[ManResult | None]
94
+
95
+ def to_df(self) -> pd.DataFrame:
96
+ return pd.concat(
97
+ {i: fcjmr.to_df() for i, fcjmr in enumerate(self.manresults[1:]) if fcjmr},
98
+ axis=0,
99
+ names=["manoeuvre", "difficulty", "truncate"],
100
+ )
101
+
102
+ class ManResult(BaseModel):
103
+ els: list[El]
104
+ results: list[Score]
105
+
106
+ def to_df(self) -> pd.DataFrame:
107
+ return pd.DataFrame(
108
+ data=[res.score.__dict__ for res in self.results],
109
+ index=pd.MultiIndex.from_frame(
110
+ pd.DataFrame([res.properties.__dict__ for res in self.results])
111
+ ),
112
+ )
113
+
114
+ def get_score(self, props: ScoreProperties):
115
+ for r in self.results:
116
+ if r.properties == props:
117
+ return r.score
118
+
119
+ class El(BaseModel):
120
+ name: str
121
+ start: int
122
+ stop: int
123
+
124
+ class Score(BaseModel):
125
+ score: ScoreValues
126
+ properties: ScoreProperties
127
+
128
+ class ScoreValues(BaseModel):
129
+ intra: float
130
+ inter: float
131
+ positioning: float
132
+ total: float
133
+
134
+ class ScoreProperties(BaseModel):
135
+ difficulty: int=3
136
+ truncate: bool=False
137
+
138
+ def __eq__(self, other):
139
+ if not isinstance(other, ScoreProperties):
140
+ return False
141
+ return self.difficulty == other.difficulty and self.truncate == other.truncate
142
+
143
+ class Man(BaseModel):
144
+ name: str
145
+ k: float
146
+ id: str
147
+ sp: int
148
+ wd: float
149
+ start: int
150
+ stop: int
151
+ sel: bool
152
+ background: str
153
+
154
+ class Data(BaseModel):
155
+ VN: float = None
156
+ VE: float = None
157
+ VD: float = None
158
+ dPD: float = None #
159
+ r: float
160
+ p: float
161
+ yw: float
162
+ N: float
163
+ E: float
164
+ D: float
165
+ time: int
166
+ roll: float
167
+ pitch: float
168
+ yaw: float
169
+
170
+
171
+
172
+ def get_scores(file: str) -> pd.DataFrame:
173
+ fcj = FCJ.model_validate_json(open(file, "r").read())
174
+ return fcj.pfc_version_df()
@@ -8,7 +8,7 @@ import numpy.typing as npt
8
8
  import pandas as pd
9
9
 
10
10
  import geometry as g
11
- from flightdata import Constructs, Environment, Flight, Flow, Origin, SVar, Table
11
+ from flightdata import Constructs, Environment, Flight, Flow, Origin, SVar, Table, fcj
12
12
 
13
13
 
14
14
  class State(Table):
@@ -199,7 +199,7 @@ class State(Table):
199
199
 
200
200
  def splitter_labels(
201
201
  self: State,
202
- mans: list[dict],
202
+ mans: list[fcj.Man],
203
203
  better_names: list[str] = None,
204
204
  target_col="manoeuvre",
205
205
  t0=0,
@@ -217,31 +217,31 @@ class State(Table):
217
217
  """
218
218
  i0 = self.data.index.get_indexer([t0], "nearest")[0]
219
219
 
220
- takeoff = self.data.iloc[0 : int(mans[0]["stop"]) + i0 + 1]
220
+ takeoff = self.data.iloc[0 : int(mans[0].stop) + i0 + 1]
221
221
 
222
- labels = [mans[0]["name"]]
222
+ labels = [mans[0].name]
223
223
  labelled = [State(takeoff).label(**{target_col: labels[0]})]
224
224
  if better_names:
225
225
  better_names.append("land")
226
226
 
227
227
  for i, split_man in enumerate(mans[1:]):
228
- while split_man["name"] in labels:
229
- split_man["name"] = split_man["name"] + "2"
228
+ while split_man.name in labels:
229
+ split_man.name = split_man.name + "2"
230
230
 
231
- name = better_names[i] if better_names else split_man["name"]
231
+ name = better_names[i] if better_names else split_man.name
232
232
 
233
233
  labelled.append(
234
234
  State(
235
235
  self.data.iloc[
236
- int(split_man["start"]) + i0 : int(split_man["stop"]) + i0 + 1
236
+ int(split_man.start) + i0 : int(split_man.stop) + i0 + 1
237
237
  ]
238
238
  ).label(**{target_col: name})
239
239
  )
240
- labels.append(split_man["name"])
240
+ labels.append(split_man.name)
241
241
 
242
242
  return State.stack(labelled)
243
243
 
244
- def label_els(self, els: dict):
244
+ def label_els(self, els: list[fcj.El]):
245
245
  return self.splitter_labels(
246
246
  pd.DataFrame(els).to_dict('records'), target_col='element'
247
247
  ).str_replace_label(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flightdata
3
- Version: 0.2.20
3
+ Version: 0.2.22
4
4
  Summary: Module for handling UAV flight log data
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
6
  License: GNU GPL v3
@@ -11,13 +11,13 @@ License-File: LICENSE
11
11
  Requires-Dist: numpy
12
12
  Requires-Dist: pandas
13
13
  Requires-Dist: simplejson
14
- Requires-Dist: pfc-geometry>=0.2.11
14
+ Requires-Dist: pfc-geometry>=0.2.12
15
15
  Requires-Dist: json_stream
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: numpy; extra == "dev"
18
18
  Requires-Dist: pandas; extra == "dev"
19
19
  Requires-Dist: simplejson; extra == "dev"
20
- Requires-Dist: pfc-geometry>=0.2.11; extra == "dev"
20
+ Requires-Dist: pfc-geometry>=0.2.12; extra == "dev"
21
21
  Requires-Dist: ardupilot-log-reader>=0.3.3; extra == "dev"
22
22
  Requires-Dist: pytest; extra == "dev"
23
23
 
@@ -48,6 +48,8 @@ flightdata/flight/parameters.py
48
48
  flightdata/model/__init__.py
49
49
  flightdata/model/aerodynamic.py
50
50
  flightdata/model/thrust.py
51
+ flightdata/schemas/__init__.py
52
+ flightdata/schemas/fcj.py
51
53
  flightdata/scripts/collect_logs.py
52
54
  flightdata/scripts/flightline.py
53
55
  test/EmailedBox.f3a
@@ -1,13 +1,13 @@
1
1
  numpy
2
2
  pandas
3
3
  simplejson
4
- pfc-geometry>=0.2.11
4
+ pfc-geometry>=0.2.12
5
5
  json_stream
6
6
 
7
7
  [dev]
8
8
  numpy
9
9
  pandas
10
10
  simplejson
11
- pfc-geometry>=0.2.11
11
+ pfc-geometry>=0.2.12
12
12
  ardupilot-log-reader>=0.3.3
13
13
  pytest
@@ -1,6 +1,6 @@
1
1
  numpy
2
2
  pandas
3
3
  simplejson
4
- pfc-geometry>=0.2.11
4
+ pfc-geometry>=0.2.12
5
5
  ardupilot-log-reader>=0.3.3
6
6
  pytest
@@ -1,5 +1,5 @@
1
1
  numpy
2
2
  pandas
3
3
  simplejson
4
- pfc-geometry>=0.2.11
4
+ pfc-geometry>=0.2.12
5
5
  json_stream
@@ -1,7 +1,9 @@
1
1
  from pytest import fixture
2
- from flightdata import Flight, Origin
3
- from flightdata import State
2
+ from flightdata import Flight, Origin, State, fcj
4
3
 
4
+ @fixture(scope='session')
5
+ def fcjson():
6
+ return Flight.from_fc_json(fcj.FCJ.model_validate_json(open('test/data/p23_fc.json', 'r').read()))
5
7
 
6
8
  @fixture(scope="session")
7
9
  def flight():
@@ -1,10 +1,10 @@
1
1
  from flightdata import Flight, Origin, BinData
2
- from io import open
3
2
  from json import load
4
3
  from pytest import fixture, approx, mark
5
4
  import numpy as np
6
5
  import pandas as pd
7
6
  from ardupilot_log_reader import Ardupilot
7
+ from .conftest import fcjson
8
8
 
9
9
  @fixture(scope='session')
10
10
  def parser():
@@ -14,9 +14,6 @@ def parser():
14
14
  def fl(parser):
15
15
  return Flight.from_log(parser)
16
16
 
17
- @fixture(scope='session')
18
- def fcj():
19
- return Flight.from_fc_json('test/data/p23_fc.json')
20
17
 
21
18
  def test_duration(fl):
22
19
  assert fl.duration == approx(687, rel=1e-3)
@@ -30,10 +27,10 @@ def test_to_from_dict(fl):
30
27
  fl2 = Flight.from_dict(data)
31
28
  assert fl == fl2
32
29
 
33
- def test_from_fc_json(fcj):
34
- assert isinstance(fcj, Flight)
35
- assert fcj.duration > 200
36
- assert fcj.position_D.max() < -10
30
+ def test_from_fc_json(fcjson):
31
+ assert isinstance(fcjson, Flight)
32
+ assert fcjson.duration > 200
33
+ assert fcjson.position_D.max() < -10
37
34
 
38
35
  @mark.skip
39
36
  def test_unique_identifier():
File without changes
@@ -1,11 +1,11 @@
1
- from flightdata import Flight, State, Origin, BinData
1
+ from flightdata import Flight, State, Origin, BinData, fcj
2
2
  from pytest import approx, fixture
3
3
  from geometry import Transformation, PX, PY, P0, Time
4
4
  from geometry.testing import assert_almost_equal
5
5
  import numpy as np
6
6
  from time import sleep, time
7
7
  from json import load
8
- from ..conftest import state, flight, origin
8
+ from ..conftest import state, flight, origin, fcjson
9
9
 
10
10
  def test_extrapolate():
11
11
  initial = State.from_transform(
@@ -16,7 +16,7 @@ def test_extrapolate():
16
16
  extrapolated = initial.extrapolate(10)
17
17
  assert extrapolated.x[-1] == approx(300)
18
18
 
19
- assert len(extrapolated) == 300
19
+ assert len(extrapolated) == 250
20
20
  assert_almost_equal(extrapolated.pos[0], initial.pos)
21
21
 
22
22
 
@@ -49,11 +49,9 @@ def test_from_flight_pos(flight: Flight, state: State, origin: Origin):
49
49
  #pd.testing.assert_frame_equal(state.data, st2.data)
50
50
  assert st2.z.mean() > 0
51
51
 
52
- def test_fc_json():
53
- with open('test/data/manual_F3A_P23.json', 'r') as f:
54
- fcj = load(f)
55
- fl = Flight.from_fc_json(fcj)
56
- origin = Origin.from_fcjson_parmameters(fcj['parameters'])
52
+ def test_fc_json(fcjson: fcj.FCJ):
53
+ fl = Flight.from_fc_json(fcjson)
54
+ origin = Origin.from_fcjson_parameters(fcjson.parameters)
57
55
  st = State.from_flight(fl, origin)
58
56
  assert st.z.mean() > 0
59
57
 
File without changes
File without changes
File without changes
File without changes
File without changes