flightdata 0.2.16__tar.gz → 0.2.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flightdata-0.2.16/flightdata.egg-info → flightdata-0.2.18}/PKG-INFO +3 -3
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/state_analysis/axes.py +1 -1
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/base/collection.py +16 -4
- flightdata-0.2.18/flightdata/base/table.py +471 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/flight/fields.py +7 -1
- flightdata-0.2.18/flightdata/flight/flight.py +728 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/model/__init__.py +2 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/scripts/flightline.py +25 -8
- flightdata-0.2.18/flightdata/state.py +650 -0
- {flightdata-0.2.16 → flightdata-0.2.18/flightdata.egg-info}/PKG-INFO +3 -3
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata.egg-info/SOURCES.txt +0 -2
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata.egg-info/requires.txt +2 -2
- {flightdata-0.2.16 → flightdata-0.2.18}/requirements-dev.txt +1 -1
- flightdata-0.2.18/requirements.txt +5 -0
- flightdata-0.2.18/test/base/test_table.py +142 -0
- flightdata-0.2.16/examples/axis_rates.ipynb +0 -172029
- flightdata-0.2.16/examples/flight_data.py +0 -11
- flightdata-0.2.16/flightdata/base/table.py +0 -404
- flightdata-0.2.16/flightdata/flight/flight.py +0 -543
- flightdata-0.2.16/flightdata/state.py +0 -563
- flightdata-0.2.16/requirements.txt +0 -5
- flightdata-0.2.16/test/base/test_table.py +0 -116
- {flightdata-0.2.16 → flightdata-0.2.18}/.github/workflows/publish_pypi.yml +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/.gitignore +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/.vscode/launch.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/.vscode/settings.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/LICENSE +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/README.md +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/data/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/data/manual_F3A_F23_22_04_28_00000231.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/data/manual_F3A_P23_22_05_31_00000350.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/data/manual_F3A_P23_23_08_11_00000094.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/flight_dynamics/00000150.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/flight_dynamics/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/flight_dynamics/box.f3a +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/flight_dynamics/param_id.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/state_analysis/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/examples/state_analysis/state_fill_plot.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/base/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/base/constructs.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/base/labeling.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/base/numpy_encoder.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/coefficients.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/environment/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/environment/environment.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/environment/wind.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/flight/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/flight/ardupilot.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/flight/parameters.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/flow.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/model/aerodynamic.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/model/thrust.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/origin.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata/scripts/collect_logs.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata.egg-info/dependency_links.txt +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata.egg-info/entry_points.txt +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/flightdata.egg-info/top_level.txt +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/pyproject.toml +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/setup.cfg +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/EmailedBox.f3a +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/base/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/base/test_base_constructs.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/conftest.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/make_inputs.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/manual_F3A_P23.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/p23.BIN +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/p23.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/p23_box.f3a +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/p23_fc.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/p23_flight.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/vtol_hover.bin +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/data/vtol_hover.json +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_environment/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_environment/test_environment.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_environment/test_environment_wind.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_fields.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_flight.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_model/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_model/test_model_coefficients.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_model/test_model_flow.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_origin.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_state/__init__.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_state/test_state.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_state/test_state_builders.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/test/test_state/test_state_conversions.py +0 -0
- {flightdata-0.2.16 → flightdata-0.2.18}/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.
|
|
3
|
+
Version: 0.2.18
|
|
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.
|
|
14
|
+
Requires-Dist: pfc-geometry>=0.2.10
|
|
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.
|
|
20
|
+
Requires-Dist: pfc-geometry>=0.2.9; extra == "dev"
|
|
21
21
|
Requires-Dist: ardupilot-log-reader>=0.3.3; extra == "dev"
|
|
22
22
|
Requires-Dist: pytest; extra == "dev"
|
|
23
23
|
|
|
@@ -2,7 +2,7 @@ from flightdata import State, Environment
|
|
|
2
2
|
from geometry import Transformation, P0, Euldeg, PY, PX, Point, Time
|
|
3
3
|
from flightplotting import plotsec
|
|
4
4
|
import numpy as np
|
|
5
|
-
from flightdata import Coefficients, Environment,
|
|
5
|
+
from flightdata import Coefficients, Environment, Flow
|
|
6
6
|
from flightdata.model import cold_draft as constants
|
|
7
7
|
import plotly.express as px
|
|
8
8
|
|
|
@@ -15,6 +15,8 @@ class Collection:
|
|
|
15
15
|
self.data = data
|
|
16
16
|
elif isinstance(data, self.__class__):
|
|
17
17
|
self.data = data.data
|
|
18
|
+
elif isinstance(data, self.__class__.VType):
|
|
19
|
+
self.data = {getattr(data, self.__class__.uid): data}
|
|
18
20
|
elif data is None:
|
|
19
21
|
pass
|
|
20
22
|
else:
|
|
@@ -40,6 +42,9 @@ class Collection:
|
|
|
40
42
|
return self.data[getattr(key, self.__class__.uid)]
|
|
41
43
|
raise ValueError(f"Invalid Key or Indexer {key}")
|
|
42
44
|
|
|
45
|
+
def subset(self, keys: list[str]) -> Self:
|
|
46
|
+
return self.__class__([getattr(self, k) for k in keys])
|
|
47
|
+
|
|
43
48
|
def __iter__(self) -> Iterable[T]:
|
|
44
49
|
for v in self.data.values():
|
|
45
50
|
yield v
|
|
@@ -61,12 +66,19 @@ class Collection:
|
|
|
61
66
|
def from_dict(cls, vals: dict[str, dict[str, Any]]) -> Self:
|
|
62
67
|
return cls([cls.VType.from_dict(v) for v in vals.values()])
|
|
63
68
|
|
|
64
|
-
def add(self, v: Union[T, Self]) -> Self:
|
|
69
|
+
def add(self, v: Union[T, Self], inplace=True) -> Self:
|
|
70
|
+
odata = self.data.copy()
|
|
65
71
|
if isinstance(v, self.VType):
|
|
66
|
-
|
|
72
|
+
odata[getattr(v, self.uid)] = v
|
|
67
73
|
elif isinstance(v, self.__class__):
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
odata = dict(**odata, **v.data)
|
|
75
|
+
elif isinstance(v, list):
|
|
76
|
+
odata = dict(**odata, **{getattr(d, self.uid): d for d in v})
|
|
77
|
+
if inplace:
|
|
78
|
+
self.data = odata
|
|
79
|
+
return v
|
|
80
|
+
else:
|
|
81
|
+
return self.__class__(odata)
|
|
70
82
|
|
|
71
83
|
def concat(self, vs: list[Self]) -> Self:
|
|
72
84
|
coll = self.__class__([])
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
from geometry import Base, Time
|
|
6
|
+
from typing import Self, Tuple, Annotated, Literal
|
|
7
|
+
from .constructs import SVar, Constructs
|
|
8
|
+
from numbers import Number
|
|
9
|
+
from time import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Table:
|
|
13
|
+
constructs = Constructs(
|
|
14
|
+
[SVar("time", Time, ["t", "dt"], lambda tab: Time.from_t(tab.t))]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def __init__(self, data: pd.DataFrame, fill=True, min_len=1):
|
|
18
|
+
if len(data) < min_len:
|
|
19
|
+
raise Exception(
|
|
20
|
+
f"State constructor length check failed, data length = {len(data)}, min_len = {min_len}"
|
|
21
|
+
)
|
|
22
|
+
self.base_cols = [c for c in data.columns if c in self.constructs.cols()]
|
|
23
|
+
self.label_cols = [c for c in data.columns if c not in self.constructs.cols()]
|
|
24
|
+
|
|
25
|
+
self.data = data
|
|
26
|
+
|
|
27
|
+
if fill:
|
|
28
|
+
missing = self.constructs.missing(self.data.columns)
|
|
29
|
+
for svar in missing:
|
|
30
|
+
newdata = (
|
|
31
|
+
svar.builder(self)
|
|
32
|
+
.to_pandas(columns=svar.keys, index=self.data.index)
|
|
33
|
+
.loc[:, [key for key in svar.keys if key not in self.data.columns]]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.data = pd.concat([self.data, newdata], axis=1)
|
|
37
|
+
bcs = self.constructs.cols()
|
|
38
|
+
else:
|
|
39
|
+
bcs = self.base_cols
|
|
40
|
+
if np.any(np.isnan(self.data.loc[:, bcs])):
|
|
41
|
+
raise ValueError("nan values in data")
|
|
42
|
+
|
|
43
|
+
def __getattr__(self, name: str) -> npt.NDArray | Base:
|
|
44
|
+
if name in self.data.columns:
|
|
45
|
+
return self.data[name].to_numpy()
|
|
46
|
+
elif name in self.constructs.data.keys():
|
|
47
|
+
con = self.constructs.data[name]
|
|
48
|
+
return con.obj(self.data.loc[:, con.keys])
|
|
49
|
+
else:
|
|
50
|
+
raise AttributeError(f"Unknown column or construct {name}")
|
|
51
|
+
|
|
52
|
+
def to_csv(self, filename):
|
|
53
|
+
self.data.to_csv(filename)
|
|
54
|
+
return filename
|
|
55
|
+
|
|
56
|
+
def to_dict(self):
|
|
57
|
+
return self.data.to_dict(orient="records")
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_dict(Cls, data):
|
|
61
|
+
if "data" in data:
|
|
62
|
+
data = data["data"]
|
|
63
|
+
return Cls(pd.DataFrame.from_dict(data).set_index("t", drop=False))
|
|
64
|
+
|
|
65
|
+
def __len__(self):
|
|
66
|
+
return len(self.data)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def duration(self):
|
|
70
|
+
return self.data.index[-1] - self.data.index[0]
|
|
71
|
+
|
|
72
|
+
def iloc(self, sli):
|
|
73
|
+
return self.__class__(self.data.iloc[sli])
|
|
74
|
+
|
|
75
|
+
def __getitem__(self, sli):
|
|
76
|
+
if isinstance(sli, slice):
|
|
77
|
+
return self.__class__(
|
|
78
|
+
self.data.loc[
|
|
79
|
+
slice(
|
|
80
|
+
sli.start if sli.start else self.data.index[0],
|
|
81
|
+
sli.stop if sli.stop else self.data.index[-1],
|
|
82
|
+
sli.step,
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
elif isinstance(sli, Number):
|
|
87
|
+
if sli < 0:
|
|
88
|
+
return self.__class__(self.data.iloc[[int(sli)], :])
|
|
89
|
+
|
|
90
|
+
return self.__class__(
|
|
91
|
+
self.data.iloc[
|
|
92
|
+
self.data.index.get_indexer(
|
|
93
|
+
[sli], method="nearest"
|
|
94
|
+
),
|
|
95
|
+
:,
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
raise TypeError(f"Expected Number or slice, got {sli.__class__.__name__}")
|
|
100
|
+
|
|
101
|
+
def slice_raw_t(self, sli):
|
|
102
|
+
inds = (
|
|
103
|
+
self.data.reset_index(names="t2").set_index("t").loc[sli].t2.to_numpy()
|
|
104
|
+
) # set_index("t", drop=False).columns
|
|
105
|
+
|
|
106
|
+
return self.__class__(self.data.loc[inds])
|
|
107
|
+
|
|
108
|
+
def __iter__(self):
|
|
109
|
+
for ind in list(self.data.index):
|
|
110
|
+
yield self[ind - self.data.index[0]]
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_constructs(cls, *args, **kwargs) -> Self:
|
|
114
|
+
kwargs = dict(
|
|
115
|
+
**{list(cls.constructs.data.keys())[i]: arg for i, arg in enumerate(args)},
|
|
116
|
+
**kwargs,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
df = pd.concat(
|
|
120
|
+
[
|
|
121
|
+
x.to_pandas(columns=cls.constructs[key].keys, index=kwargs["time"].t)
|
|
122
|
+
for key, x in kwargs.items()
|
|
123
|
+
if x is not None
|
|
124
|
+
],
|
|
125
|
+
axis=1,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return cls(df)
|
|
129
|
+
|
|
130
|
+
def __repr__(self):
|
|
131
|
+
return f"{self.__class__.__name__} Table(duration = {self.duration})"
|
|
132
|
+
|
|
133
|
+
def copy(self, *args, **kwargs):
|
|
134
|
+
kwargs = dict(
|
|
135
|
+
kwargs,
|
|
136
|
+
**{list(self.constructs.data.keys())[i]: arg for i, arg in enumerate(args)},
|
|
137
|
+
) # add the args to the kwargs
|
|
138
|
+
old_constructs = {
|
|
139
|
+
key: self.__getattr__(key)
|
|
140
|
+
for key in self.constructs.existing(self.data.columns).data
|
|
141
|
+
if not key in kwargs
|
|
142
|
+
}
|
|
143
|
+
new_constructs = {
|
|
144
|
+
key: value
|
|
145
|
+
for key, value in list(kwargs.items()) + list(old_constructs.items())
|
|
146
|
+
}
|
|
147
|
+
return self.__class__.from_constructs(**new_constructs).label(
|
|
148
|
+
**self.labels.to_dict(orient="list")
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def append(self, other, timeoption: str = "dt"):
|
|
152
|
+
if timeoption in ["now", "t"]:
|
|
153
|
+
t = np.array([time()]) if timeoption == "now" else other.t
|
|
154
|
+
dt = other.dt
|
|
155
|
+
dt[0] = t[0] - self.t[-1]
|
|
156
|
+
new_time = Time(t, dt)
|
|
157
|
+
elif timeoption == "dt":
|
|
158
|
+
new_time = Time(other.t + self[-1].t - other[0].t + other[0].dt, other.dt)
|
|
159
|
+
|
|
160
|
+
return self.__class__(
|
|
161
|
+
pd.concat(
|
|
162
|
+
[self.data, other.copy(new_time).data], axis=0, ignore_index=True
|
|
163
|
+
).set_index("t", drop=False)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def zero_index(self):
|
|
167
|
+
data = self.data.copy()
|
|
168
|
+
return self.__class__(data.set_index(data.index - data.index[0]))
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def stack(Cls, sts: list, overlap: int = 1) -> Self:
|
|
172
|
+
"""Stack a list of Tables on top of each other.
|
|
173
|
+
The overlap is the number of rows to overlap between each st
|
|
174
|
+
"""
|
|
175
|
+
t0 = sts[0].data.index[0]
|
|
176
|
+
sts = [st.zero_index() for st in sts]
|
|
177
|
+
if overlap > 0:
|
|
178
|
+
offsets = np.cumsum([0] + [s0.data.index[-overlap] for s0 in sts[:-1]])
|
|
179
|
+
dfs = [st.data.iloc[:-overlap] for st in sts[:-1]] + [sts[-1].data]
|
|
180
|
+
elif overlap == 0:
|
|
181
|
+
offsets = np.cumsum([0] + [sec.duration + sec.dt[-1] for sec in sts[:-1]])
|
|
182
|
+
dfs = [st.data for st in sts]
|
|
183
|
+
else:
|
|
184
|
+
raise AttributeError("Overlap must be >= 0")
|
|
185
|
+
|
|
186
|
+
for df, offset in zip(dfs, offsets):
|
|
187
|
+
df.index = np.array(df.index) - df.index[0] + offset
|
|
188
|
+
combo = pd.concat(dfs)
|
|
189
|
+
combo.index.name = "t"
|
|
190
|
+
combo.index = combo.index + t0
|
|
191
|
+
combo["t"] = combo.index
|
|
192
|
+
|
|
193
|
+
return Cls(combo)
|
|
194
|
+
|
|
195
|
+
def label(self, **kwargs) -> Self:
|
|
196
|
+
return self.__class__(self.data.assign(**kwargs))
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def label_keys(self):
|
|
200
|
+
return self.label_cols
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def labels(self) -> dict[str, npt.NDArray]:
|
|
204
|
+
return self.data.loc[:, self.label_cols]
|
|
205
|
+
|
|
206
|
+
def remove_labels(self) -> Self:
|
|
207
|
+
return self.__class__(self.data.drop(self.label_keys, axis=1, errors="ignore"))
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def labselect(
|
|
211
|
+
data: pd.DataFrame, test: str = None, offset=False, **kwargs
|
|
212
|
+
) -> pd.DataFrame:
|
|
213
|
+
"""Select rows from a dataframe based on the values in the kwargs
|
|
214
|
+
in kwargs, keys are column names and values are the values to select
|
|
215
|
+
if test is not None, it is a string that is a pandas string method .
|
|
216
|
+
if offset is True the row after the last selected row for each kwarg is included.
|
|
217
|
+
"""
|
|
218
|
+
sel = np.full(len(data), True)
|
|
219
|
+
for k, v in kwargs.items():
|
|
220
|
+
if test:
|
|
221
|
+
sel = getattr(data[k].str, test)(v)
|
|
222
|
+
else:
|
|
223
|
+
sel = sel & (data[k] == v)
|
|
224
|
+
if offset:
|
|
225
|
+
return data.loc[sel + (sel.astype(int).diff() == -1)]
|
|
226
|
+
else:
|
|
227
|
+
return data.loc[sel]
|
|
228
|
+
|
|
229
|
+
def get_label_id(self, test: str = None, **kwargs) -> int | float:
|
|
230
|
+
dfo = Table.labselect(self.unique_labels(), test, **kwargs)
|
|
231
|
+
return dfo.index[0] if len(dfo) > 0 else list(dfo.index)
|
|
232
|
+
|
|
233
|
+
def get_subset_df(
|
|
234
|
+
self, test: str | None = None, offset: bool = True, **kwargs
|
|
235
|
+
) -> pd.DataFrame:
|
|
236
|
+
return Table.labselect(self.data, test, offset, **kwargs)
|
|
237
|
+
|
|
238
|
+
def get_label_subset(self, min_len=1, test: str | None = None, **kwargs) -> Self:
|
|
239
|
+
return self.__class__(self.get_subset_df(test, **kwargs), min_len=min_len)
|
|
240
|
+
|
|
241
|
+
def get_label_len(self, test: str = None, offset=False, **kwargs) -> int:
|
|
242
|
+
try:
|
|
243
|
+
return len(self.get_subset_df(test, offset, **kwargs))
|
|
244
|
+
except Exception:
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
def unique_labels(self, cols=None) -> pd.DataFrame:
|
|
248
|
+
if cols is None:
|
|
249
|
+
cols = self.label_cols
|
|
250
|
+
elif isinstance(cols, str):
|
|
251
|
+
cols = [cols]
|
|
252
|
+
return (
|
|
253
|
+
self.data.loc[:, cols]
|
|
254
|
+
.reset_index(drop=True)
|
|
255
|
+
.drop_duplicates()
|
|
256
|
+
.reset_index(drop=True)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def shift_label(self, offset: int, min_len=None, test=None, **kwargs) -> Self:
|
|
260
|
+
"""Shift the end of a label forwards or backwards by offset rows
|
|
261
|
+
Do not allow a label to be reduced to less than min_len"""
|
|
262
|
+
if min_len is None:
|
|
263
|
+
min_len = 1
|
|
264
|
+
ranges = self.label_ranges()
|
|
265
|
+
|
|
266
|
+
i = self.get_label_id(test, **kwargs)
|
|
267
|
+
labels: pd.DataFrame = self.labels.copy()
|
|
268
|
+
labcols = [labels.columns.get_loc(c) for c in kwargs.keys()]
|
|
269
|
+
if offset > 0 and i < len(ranges):
|
|
270
|
+
offset = min(offset, ranges.iloc[i + 1, -1] - min_len)
|
|
271
|
+
if offset > 0:
|
|
272
|
+
labels.iloc[
|
|
273
|
+
ranges.iloc[i + 1].start : ranges.iloc[i + 1].start + offset,
|
|
274
|
+
labcols,
|
|
275
|
+
] = pd.Series(kwargs)
|
|
276
|
+
elif offset < 0:
|
|
277
|
+
offset = max(offset, -ranges.iloc[i, -1] + min_len)
|
|
278
|
+
if offset < 0:
|
|
279
|
+
labels.iloc[
|
|
280
|
+
ranges.iloc[i].end + offset : ranges.iloc[i].end + 1, labcols
|
|
281
|
+
] = ranges.iloc[i + 1].loc[kwargs.keys()]
|
|
282
|
+
return self.label(**labels.to_dict(orient="list"))
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def shift_multi(
|
|
286
|
+
Cls, steps: int, tb1: Self, tb2: Self, min_len=1
|
|
287
|
+
) -> Tuple[Self, Self]:
|
|
288
|
+
"""Take datapoints off the start of tb2 and add to the end tb1"""
|
|
289
|
+
# if (steps>0 and len(tb2)-min_len<steps) or (steps<0 and min_len - len(tb1) > steps):
|
|
290
|
+
# raise ValueError(f'Cannot Squash a Table to less than {min_len} datapoints')
|
|
291
|
+
tj = Cls.stack([tb1, tb2]).shift_label(
|
|
292
|
+
steps, min_len, **dict(tb1.labels.iloc[0])
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return Cls(tj.get_subset_df(**dict(tb1.labels.iloc[0]))), Cls(
|
|
296
|
+
tj.get_subset_df(**dict(tb2.labels.iloc[0]))
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def shift_label_ratio(self, ratio: float, min_len=None, **kwargs) -> Self:
|
|
300
|
+
"""shift a label within its allowable bounds, with a ratio of
|
|
301
|
+
1 representing the maximum allowabe movement forwards or backwards
|
|
302
|
+
without squashing a label"""
|
|
303
|
+
ranges = self.label_ranges()
|
|
304
|
+
i = self.get_label_id(**kwargs)
|
|
305
|
+
if ratio > 0:
|
|
306
|
+
limit = ranges.iloc[i + 1, -1] - 2
|
|
307
|
+
else:
|
|
308
|
+
limit = ranges.iloc[i, -1] - 2
|
|
309
|
+
|
|
310
|
+
return self.shift_label(int(limit * ratio), min_len, **kwargs)
|
|
311
|
+
|
|
312
|
+
def shift_labels_ratios(self, ratios: list[float], min_len: int) -> Self:
|
|
313
|
+
assert len(ratios) == len(self.unique_labels()) - 1
|
|
314
|
+
res = self
|
|
315
|
+
for lab, ratio in zip(
|
|
316
|
+
[r[1] for r in self.unique_labels()[:-1].iterrows()], ratios
|
|
317
|
+
):
|
|
318
|
+
res = res.shift_label_ratio(ratio, min_len, **lab)
|
|
319
|
+
return res
|
|
320
|
+
|
|
321
|
+
def label_range(self, t=False, **kwargs) -> tuple[int]:
|
|
322
|
+
"""Get the first and last index of a label.
|
|
323
|
+
If t is True this gives the time, if False it gives the index"""
|
|
324
|
+
labs = self.get_subset_df(**kwargs)
|
|
325
|
+
if not t:
|
|
326
|
+
return self.data.index.get_indexer([labs.index[0]])[
|
|
327
|
+
0
|
|
328
|
+
], self.data.index.get_indexer([labs.index[-1]])[0]
|
|
329
|
+
else:
|
|
330
|
+
return labs.index[0], labs.index[-1]
|
|
331
|
+
|
|
332
|
+
def label_ranges(self, cols: list[str] = None, t=False) -> pd.DataFrame:
|
|
333
|
+
"""get the first and last index for each unique label"""
|
|
334
|
+
if cols is None:
|
|
335
|
+
cols = self.label_cols
|
|
336
|
+
df: pd.DataFrame = self.unique_labels(cols)
|
|
337
|
+
res = []
|
|
338
|
+
for row in df.iterrows():
|
|
339
|
+
res.append(list(self.label_range(t=t, **row[1].to_dict())))
|
|
340
|
+
df = pd.concat([df, pd.DataFrame(res, columns=["start", "end"])], axis=1)
|
|
341
|
+
df["length"] = df.end - df.start
|
|
342
|
+
return df
|
|
343
|
+
|
|
344
|
+
def single_labels(self) -> list[str]:
|
|
345
|
+
return ["_".join(r[1]) for r in self.data.loc[:, self.label_cols].iterrows()]
|
|
346
|
+
|
|
347
|
+
def label_lens(self) -> dict[str, int]:
|
|
348
|
+
return {k: len(v) for k, v in self.split_labels().items()}
|
|
349
|
+
|
|
350
|
+
def extract_single_label(self, lab) -> Self:
|
|
351
|
+
labs = np.array(self.single_labels())
|
|
352
|
+
return self.__class__(self.data[labs == lab])
|
|
353
|
+
|
|
354
|
+
def split_labels(self, cols: list[str] | str = None) -> dict[str, Self]:
|
|
355
|
+
"""Split into multiple tables based on the labels"""
|
|
356
|
+
res = {}
|
|
357
|
+
for label in self.unique_labels(cols).iterrows():
|
|
358
|
+
ld = label[1].to_dict()
|
|
359
|
+
res["_".join(ld.values())] = self.get_label_subset(**ld)
|
|
360
|
+
return res
|
|
361
|
+
|
|
362
|
+
def cumulative_labels(self, *cols) -> Self:
|
|
363
|
+
"""Return a string concatenation of the requested labels. append an indexer to the end
|
|
364
|
+
of the string for repeat descrete groups of the same label."""
|
|
365
|
+
cols = self.label_cols if len(cols) == 0 else cols
|
|
366
|
+
labs = self.data.loc[:, cols].stack().groupby(level=0).apply("_".join)
|
|
367
|
+
|
|
368
|
+
changes = labs.shift() != labs
|
|
369
|
+
new_labels = labs.loc[changes]
|
|
370
|
+
uls = []
|
|
371
|
+
for i, nl in enumerate(new_labels):
|
|
372
|
+
uls.append(sum(new_labels.iloc[:i] == nl))
|
|
373
|
+
|
|
374
|
+
df = pd.DataFrame(labs).assign(indexer=np.array(uls)[changes.cumsum() - 1])
|
|
375
|
+
strdf = df.copy()
|
|
376
|
+
strdf["indexer"] = strdf["indexer"].astype(int).astype(str)
|
|
377
|
+
strdf = strdf.stack().groupby(level=0).apply("_".join)
|
|
378
|
+
return strdf.values
|
|
379
|
+
|
|
380
|
+
def str_replace_label(self, **kwargs: dict[str, npt.NDArray[np.str_]]) -> Self:
|
|
381
|
+
"""perform a string replace for labels"""
|
|
382
|
+
dfo = self.data.copy()
|
|
383
|
+
for k, v in kwargs.items():
|
|
384
|
+
for rep in v:
|
|
385
|
+
dfo[k] = dfo[k].str.replace(*rep)
|
|
386
|
+
return self.__class__(dfo)
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def copy_labels(
|
|
390
|
+
template: Self,
|
|
391
|
+
flown: Self,
|
|
392
|
+
path: Annotated[npt.NDArray[np.integer], Literal["N", 2]] = None,
|
|
393
|
+
min_len=0,
|
|
394
|
+
) -> Self:
|
|
395
|
+
"""Copy the labels from template to flown along the index warping path
|
|
396
|
+
If path is None, the labels are copied directly from the template to the flown
|
|
397
|
+
min_len prevents the labels from being shortened to less than min_len rows,
|
|
398
|
+
even if the label dows not exist in the warping path the order of labels in template
|
|
399
|
+
will be preserved.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
flown = flown.remove_labels()
|
|
403
|
+
|
|
404
|
+
if not path:
|
|
405
|
+
return flown.__class__(
|
|
406
|
+
pd.concat(
|
|
407
|
+
[
|
|
408
|
+
flown.data.reset_index(drop=True),
|
|
409
|
+
template.data.loc[:, template.label_cols].reset_index(
|
|
410
|
+
drop=True
|
|
411
|
+
),
|
|
412
|
+
],
|
|
413
|
+
axis=1,
|
|
414
|
+
).set_index("t", drop=False)
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
mans = (
|
|
418
|
+
pd.DataFrame(path, columns=["template", "flight"])
|
|
419
|
+
.set_index("template")
|
|
420
|
+
.join(template.data.reset_index(drop=True).loc[:, template.label_cols])
|
|
421
|
+
.groupby(["flight"])
|
|
422
|
+
.last()
|
|
423
|
+
.reset_index()
|
|
424
|
+
.set_index("flight")
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
st: Self = flown.__class__(flown.data).label(**mans.to_dict(orient="list"))
|
|
428
|
+
|
|
429
|
+
if min_len > 0:
|
|
430
|
+
unique_labels = template.unique_labels()
|
|
431
|
+
|
|
432
|
+
for i, row in unique_labels.iterrows():
|
|
433
|
+
lens = st.label_lens()
|
|
434
|
+
labels = st.labels.copy()
|
|
435
|
+
|
|
436
|
+
def get_len(_i: int):
|
|
437
|
+
key = "_".join(list(unique_labels.iloc[_i].to_dict().values()))
|
|
438
|
+
return lens[key] if key in lens else 0
|
|
439
|
+
|
|
440
|
+
def get_range(_i: int):
|
|
441
|
+
return st.label_range(**unique_labels.iloc[_i].to_dict())
|
|
442
|
+
|
|
443
|
+
if get_len(i) < min_len:
|
|
444
|
+
max_bck = get_len(i - 1) - min_len if i > 0 else 0
|
|
445
|
+
max_fwd = (
|
|
446
|
+
get_len(i + 1) - min_len
|
|
447
|
+
if i < len(unique_labels) - 1
|
|
448
|
+
else 0
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if max_bck + max_fwd + get_len(i) < min_len:
|
|
452
|
+
raise Exception(
|
|
453
|
+
f"{row.iloc[0]},{row.iloc[1]} too short and cannot shorten adjacent labels further"
|
|
454
|
+
)
|
|
455
|
+
else:
|
|
456
|
+
_extend = int(np.ceil((min_len - get_len(i)) / 2))
|
|
457
|
+
ebck = min(max_bck, int(np.floor(_extend)))
|
|
458
|
+
efwd = min(max_fwd, int(np.floor(_extend)) + 1)
|
|
459
|
+
|
|
460
|
+
if ebck > 0:
|
|
461
|
+
rng = get_range(i - 1)
|
|
462
|
+
labels.iloc[rng[1] - ebck : rng[1] + 1] = (
|
|
463
|
+
unique_labels.iloc[i]
|
|
464
|
+
)
|
|
465
|
+
if efwd > 0:
|
|
466
|
+
rng = get_range(i + 1)
|
|
467
|
+
labels.iloc[rng[0] : rng[0] + efwd + 1] = (
|
|
468
|
+
unique_labels.iloc[i]
|
|
469
|
+
)
|
|
470
|
+
st = st.label(**labels.to_dict(orient="list"))
|
|
471
|
+
return st
|
|
@@ -57,7 +57,8 @@ class Fields:
|
|
|
57
57
|
#raise AttributeError(f'Field {name} not found')
|
|
58
58
|
|
|
59
59
|
def get_fields(self, names: list[str]) -> list[Field]:
|
|
60
|
-
_l
|
|
60
|
+
def _l(v):
|
|
61
|
+
return [v] if isinstance(v, Field) else v
|
|
61
62
|
return list(chain(*[_l(getattr(self, n)) for n in names]))
|
|
62
63
|
|
|
63
64
|
def get_cols(self, names: list[str]) -> list[str]:
|
|
@@ -80,6 +81,11 @@ fields = Fields([
|
|
|
80
81
|
Field('gps_latitude', 'latitude, degrees'),
|
|
81
82
|
Field('gps_longitude', 'longitude, degrees'),
|
|
82
83
|
Field('gps_altitude', 'altitude, meters'),
|
|
84
|
+
Field('gps_satellites', 'number of satellites'),
|
|
85
|
+
Field('gps_hdop', 'number precision'),
|
|
86
|
+
Field('pos_latitude', 'latitude, degrees'),
|
|
87
|
+
Field('pos_longitude', 'longitude, degrees'),
|
|
88
|
+
Field('pos_altitude', 'altitude, meters'),
|
|
83
89
|
Field('attitude_roll', 'roll angle, radians'),
|
|
84
90
|
Field('attitude_pitch', 'pitch angle, radians'),
|
|
85
91
|
Field('attitude_yaw', 'yaw angle, radians'),
|