flightdata 0.2.16__tar.gz → 0.2.17__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 (87) hide show
  1. {flightdata-0.2.16/flightdata.egg-info → flightdata-0.2.17}/PKG-INFO +1 -1
  2. {flightdata-0.2.16 → flightdata-0.2.17}/examples/state_analysis/axes.py +1 -1
  3. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/base/collection.py +16 -4
  4. flightdata-0.2.17/flightdata/base/table.py +471 -0
  5. flightdata-0.2.17/flightdata/flight/flight.py +711 -0
  6. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/model/__init__.py +2 -0
  7. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/scripts/flightline.py +25 -8
  8. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/state.py +19 -44
  9. {flightdata-0.2.16 → flightdata-0.2.17/flightdata.egg-info}/PKG-INFO +1 -1
  10. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata.egg-info/SOURCES.txt +0 -2
  11. flightdata-0.2.17/test/base/test_table.py +142 -0
  12. flightdata-0.2.16/examples/axis_rates.ipynb +0 -172029
  13. flightdata-0.2.16/examples/flight_data.py +0 -11
  14. flightdata-0.2.16/flightdata/base/table.py +0 -404
  15. flightdata-0.2.16/flightdata/flight/flight.py +0 -543
  16. flightdata-0.2.16/test/base/test_table.py +0 -116
  17. {flightdata-0.2.16 → flightdata-0.2.17}/.github/workflows/publish_pypi.yml +0 -0
  18. {flightdata-0.2.16 → flightdata-0.2.17}/.gitignore +0 -0
  19. {flightdata-0.2.16 → flightdata-0.2.17}/.vscode/launch.json +0 -0
  20. {flightdata-0.2.16 → flightdata-0.2.17}/.vscode/settings.json +0 -0
  21. {flightdata-0.2.16 → flightdata-0.2.17}/LICENSE +0 -0
  22. {flightdata-0.2.16 → flightdata-0.2.17}/README.md +0 -0
  23. {flightdata-0.2.16 → flightdata-0.2.17}/examples/__init__.py +0 -0
  24. {flightdata-0.2.16 → flightdata-0.2.17}/examples/data/__init__.py +0 -0
  25. {flightdata-0.2.16 → flightdata-0.2.17}/examples/data/manual_F3A_F23_22_04_28_00000231.json +0 -0
  26. {flightdata-0.2.16 → flightdata-0.2.17}/examples/data/manual_F3A_P23_22_05_31_00000350.json +0 -0
  27. {flightdata-0.2.16 → flightdata-0.2.17}/examples/data/manual_F3A_P23_23_08_11_00000094.json +0 -0
  28. {flightdata-0.2.16 → flightdata-0.2.17}/examples/flight_dynamics/00000150.json +0 -0
  29. {flightdata-0.2.16 → flightdata-0.2.17}/examples/flight_dynamics/__init__.py +0 -0
  30. {flightdata-0.2.16 → flightdata-0.2.17}/examples/flight_dynamics/box.f3a +0 -0
  31. {flightdata-0.2.16 → flightdata-0.2.17}/examples/flight_dynamics/param_id.py +0 -0
  32. {flightdata-0.2.16 → flightdata-0.2.17}/examples/state_analysis/__init__.py +0 -0
  33. {flightdata-0.2.16 → flightdata-0.2.17}/examples/state_analysis/state_fill_plot.py +0 -0
  34. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/__init__.py +0 -0
  35. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/base/__init__.py +0 -0
  36. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/base/constructs.py +0 -0
  37. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/base/labeling.py +0 -0
  38. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/base/numpy_encoder.py +0 -0
  39. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/coefficients.py +0 -0
  40. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/environment/__init__.py +0 -0
  41. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/environment/environment.py +0 -0
  42. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/environment/wind.py +0 -0
  43. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/flight/__init__.py +0 -0
  44. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/flight/ardupilot.py +0 -0
  45. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/flight/fields.py +0 -0
  46. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/flight/parameters.py +0 -0
  47. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/flow.py +0 -0
  48. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/model/aerodynamic.py +0 -0
  49. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/model/thrust.py +0 -0
  50. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/origin.py +0 -0
  51. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata/scripts/collect_logs.py +0 -0
  52. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata.egg-info/dependency_links.txt +0 -0
  53. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata.egg-info/entry_points.txt +0 -0
  54. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata.egg-info/requires.txt +0 -0
  55. {flightdata-0.2.16 → flightdata-0.2.17}/flightdata.egg-info/top_level.txt +0 -0
  56. {flightdata-0.2.16 → flightdata-0.2.17}/pyproject.toml +0 -0
  57. {flightdata-0.2.16 → flightdata-0.2.17}/requirements-dev.txt +0 -0
  58. {flightdata-0.2.16 → flightdata-0.2.17}/requirements.txt +0 -0
  59. {flightdata-0.2.16 → flightdata-0.2.17}/setup.cfg +0 -0
  60. {flightdata-0.2.16 → flightdata-0.2.17}/test/EmailedBox.f3a +0 -0
  61. {flightdata-0.2.16 → flightdata-0.2.17}/test/__init__.py +0 -0
  62. {flightdata-0.2.16 → flightdata-0.2.17}/test/base/__init__.py +0 -0
  63. {flightdata-0.2.16 → flightdata-0.2.17}/test/base/test_base_constructs.py +0 -0
  64. {flightdata-0.2.16 → flightdata-0.2.17}/test/conftest.py +0 -0
  65. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/make_inputs.py +0 -0
  66. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/manual_F3A_P23.json +0 -0
  67. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/p23.BIN +0 -0
  68. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/p23.json +0 -0
  69. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/p23_box.f3a +0 -0
  70. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/p23_fc.json +0 -0
  71. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/p23_flight.json +0 -0
  72. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/vtol_hover.bin +0 -0
  73. {flightdata-0.2.16 → flightdata-0.2.17}/test/data/vtol_hover.json +0 -0
  74. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_environment/__init__.py +0 -0
  75. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_environment/test_environment.py +0 -0
  76. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_environment/test_environment_wind.py +0 -0
  77. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_fields.py +0 -0
  78. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_flight.py +0 -0
  79. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_model/__init__.py +0 -0
  80. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_model/test_model_coefficients.py +0 -0
  81. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_model/test_model_flow.py +0 -0
  82. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_origin.py +0 -0
  83. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_state/__init__.py +0 -0
  84. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_state/test_state.py +0 -0
  85. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_state/test_state_builders.py +0 -0
  86. {flightdata-0.2.16 → flightdata-0.2.17}/test/test_state/test_state_conversions.py +0 -0
  87. {flightdata-0.2.16 → flightdata-0.2.17}/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.16
3
+ Version: 0.2.17
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
@@ -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, Environment, Flow
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
- self.data[getattr(v, self.uid)] = v
72
+ odata[getattr(v, self.uid)] = v
67
73
  elif isinstance(v, self.__class__):
68
- self.data = dict(**self.data, **v.data)
69
- return v
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