pyFlightPlotter 0.3.1__py3-none-any.whl

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.
@@ -0,0 +1,55 @@
1
+ import numpy as np
2
+
3
+ from .core import Craft3D
4
+
5
+ class Quadrotor(Craft3D):
6
+ def __init__(self, l=0.2, R=0.1):
7
+ body = np.array([
8
+ [ l, -l/4, 0.],
9
+ [ l, +l/4, 0.],
10
+ [ l+l/4, 0, 0.],
11
+ [ l, -l/4, 0.],
12
+ ])
13
+ geometry = [body]
14
+ super().__init__(body_geometry=geometry)
15
+
16
+ arms = np.array([
17
+ [ -l, +l, 0.],
18
+ [ +l, +l, 0.],
19
+ [ -l, -l, 0.],
20
+ [ +l, -l, 0.],
21
+ ])
22
+
23
+ for arm in arms:
24
+ self.addRotor(xyz=arm, axis=[0, 0, -1], R=R)
25
+
26
+ class Tailsitter(Craft3D):
27
+ def __init__(self, l=0.3, R=0.1):
28
+ body = np.array([
29
+ [ 0, 2*l, l],
30
+ [ 0, 2*l, 0.],
31
+ [ 0, 0., -1*l],
32
+ [ l, 0., -1*l],
33
+ [ 0, 0., -1*l],
34
+ [ 0, -2*l, 0.],
35
+ [ 0, -2*l, l],
36
+ [ 0, 2*l, l],
37
+ ])
38
+ geometry = [body]
39
+ super().__init__(body_geometry=geometry)
40
+
41
+ self.addRotor(xyz=[0, -l, -l], axis=[0, 0, -1], R=R)
42
+ self.addRotor(xyz=[0, +l, -l], axis=[0, 0, -1], R=R)
43
+
44
+ surface = np.array([
45
+ [0, -2.*l, 1.0*l],
46
+ [0, -2.*l, 1.5*l],
47
+ [0, -0.5*l, 1.5*l],
48
+ [0, -0.5*l, 1.0*l],
49
+ ])
50
+ self.addSurface(
51
+ tilt_xyz=[0., 0., 1.*l], tilt_axis=[0., +1., 0.], geometry=[surface]
52
+ )
53
+ self.addSurface(
54
+ tilt_xyz=[0., 0., 1.*l], tilt_axis=[0., +1., 0.], geometry=[surface*np.array([1., -1., 1.])]
55
+ )
@@ -0,0 +1,107 @@
1
+
2
+ from matplotlib.backend_bases import MouseEvent
3
+ import matplotlib.pyplot as plt
4
+ from time import sleep
5
+
6
+ class BlittedCursor(object):
7
+ def __init__(self, axes, sharex=True):
8
+ self.axes = axes
9
+ self.backgrounds = []
10
+ self.cursors = []
11
+
12
+ ax0 = self.axes[0]
13
+ self.playing = False
14
+ self.x = 0
15
+
16
+ for i, ax in enumerate(self.axes):
17
+ if i > 0 and sharex:
18
+ ax.sharex(ax0)
19
+
20
+ lines = ax.get_lines()
21
+ if lines:
22
+ min_x = min(min(line.get_xdata()) for line in lines)
23
+ self.cursors.append(ax.axvline(x=min_x,
24
+ color='black',
25
+ linestyle='--',
26
+ lw=0.8,
27
+ visible=False))
28
+ else:
29
+ self.cursors.append(None)
30
+
31
+ # get unique canvasses by iterating over axes
32
+ # this is necessary to avoid multiple connections to the same canvas
33
+ self.canvasses = [self.axes[0].figure.canvas]
34
+ for ax in self.axes[1:]:
35
+ if ax.figure.canvas is not self.canvasses:
36
+ self.canvasses.append(ax.figure.canvas)
37
+
38
+ for canvas in self.canvasses:
39
+ # connect the canvas to the draw and motion events
40
+ canvas.mpl_connect('draw_event', self._on_draw)
41
+ canvas.mpl_connect('motion_notify_event', self._on_mouse_move)
42
+ canvas.mpl_connect('key_press_event', self._on_key_press)
43
+
44
+ self.timer = self.axes[0].figure.canvas.new_timer(interval=50)
45
+ self.timer.add_callback(self._on_timer)
46
+
47
+ def _on_draw(self, event):
48
+ self.backgrounds.clear()
49
+ for ax in self.axes:
50
+ canvas = ax.figure.canvas
51
+ self.backgrounds.append(canvas.copy_from_bbox(ax.bbox))
52
+
53
+ def _on_mouse_move(self, event):
54
+ if event.xdata is None or not self.backgrounds:
55
+ return
56
+
57
+ self.x = event.xdata
58
+
59
+ for ax, line, bg in zip(self.axes, self.cursors, self.backgrounds):
60
+ canvas = ax.figure.canvas
61
+ canvas.restore_region(bg)
62
+ if line is not None:
63
+ line.set_xdata([event.xdata])
64
+ line.set_visible(True)
65
+ ax.draw_artist(line)
66
+ canvas.blit(ax.bbox)
67
+
68
+ def _on_timer(self):
69
+
70
+ # generate mouse event to canvas, so that all children get updated
71
+ from matplotlib.backend_bases import MouseEvent
72
+
73
+ self.x += 0.02
74
+ canvas = self.axes[0].figure.canvas
75
+ event = MouseEvent(
76
+ name="motion_notify_event",
77
+ canvas=canvas,
78
+ x=self.x, # pixel coordinates
79
+ y=0,
80
+ button=None,
81
+ key=None,
82
+ step=0,
83
+ dblclick=False,
84
+ guiEvent=None
85
+ )
86
+ event.xdata = self.x # data coordinates
87
+ canvas.callbacks.process(
88
+ "motion_notify_event", event
89
+ )
90
+
91
+
92
+ def _on_key_press(self, event):
93
+ """Callback function to update the viewport on key press
94
+
95
+ Args:
96
+ event (matplotlib.backend_bases.KeyEvent): key event
97
+
98
+ Returns:
99
+ None
100
+ """
101
+ if event.key == ' ':
102
+ if self.playing:
103
+ self.playing = False
104
+ self.timer.stop()
105
+ else:
106
+ self.playing = True
107
+ self.timer.start()
@@ -0,0 +1,25 @@
1
+ from matplotlib import pyplot as plt
2
+
3
+ local_rc = plt.rcParams.copy()
4
+ local_rc.update({
5
+ # "text.usetex": True,
6
+ # "font.family": "Helvetica",
7
+ "font.family": "sans-serif",
8
+ "font.size": 10,
9
+ "axes.grid": True,
10
+ "axes.grid.which": 'both',
11
+ "grid.linestyle": '--',
12
+ "grid.alpha": 0.7,
13
+ "axes.labelsize": 10,
14
+ "axes.titlesize": 14,
15
+ "xtick.labelsize": 10,
16
+ "ytick.labelsize": 10,
17
+ "legend.loc": 'upper left',
18
+ "legend.fontsize": 8,
19
+ 'figure.subplot.bottom': 0.06,
20
+ 'figure.subplot.left': 0.05,
21
+ 'figure.subplot.right': 0.95,
22
+ 'figure.subplot.top': 0.925,
23
+ 'figure.subplot.wspace': 0.3,
24
+ 'figure.subplot.hspace': 0.433,
25
+ })
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyFlightPlotter
3
+ Version: 0.3.1
4
+ Summary: Interactive, synchronized 2D time-series and 3D attitude/position plotting for flight logs
5
+ Author-email: Till Blaha <t.m.blaha@tudelft.nl>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/tudelft/pyFlightPlotter
8
+ Project-URL: Repository, https://github.com/tudelft/pyFlightPlotter
9
+ Project-URL: Issues, https://github.com/tudelft/pyFlightPlotter/issues
10
+ Keywords: flight data,log analysis,drone,UAV,quaternion,matplotlib,visualization
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Scientific/Engineering :: Visualization
18
+ Classifier: Framework :: Matplotlib
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=1.24
23
+ Requires-Dist: scipy>=1.10
24
+ Requires-Dist: matplotlib>=3.7
25
+ Provides-Extra: video
26
+ Requires-Dist: opencv-python; extra == "video"
27
+ Dynamic: license-file
28
+
29
+ # pyFlightPlotter
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/pyFlightPlotter)](https://pypi.org/project/pyFlightPlotter/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/pyFlightPlotter)](https://pypi.org/project/pyFlightPlotter/)
33
+ [![License: GPL v3+](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)](https://github.com/tudelft/pyFlightPlotter/blob/main/LICENSE)
34
+
35
+ Interactive, configurable and three-dimensional flight data plotting, with little boilerplate.
36
+
37
+ Hover over a time-series plot and every connected window follows along: the cursor moves in sync in all
38
+ time-series axes, and a 3D viewport shows the vehicle's attitude, position, velocity, acceleration and
39
+ actuator state at that moment. You can also sync an onboard or external video to the log.
40
+
41
+ ![Demo of pyFlightPlotter](https://raw.githubusercontent.com/tudelft/pyFlightPlotter/main/docs/images/demo.gif)
42
+
43
+ ## Features
44
+
45
+ - **3D viewport** showing estimates, setpoints and measurements of
46
+ - attitude (quaternions)
47
+ - position, velocity and acceleration
48
+ - rotor force commands
49
+ - control surface deflections
50
+ - **Time-series plotting** in a configurable grid layout. `_plot_timeseries()` takes several line styles
51
+ (light / solid / dashed / true value) and keeps colours consistent across series.
52
+ - **A synchronized vertical cursor** across all time-series axes, even when they're spread over several
53
+ figures. The 3D viewport updates on mouse hover.
54
+ - **Playback**: press <kbd>Space</kbd> to play or pause the log in real time.
55
+ - **Video sync** (`VideoViewport`): show an MP4 next to the log, with an adjustable time offset.
56
+ - **Craft builder**: the `Craft3D` class has `addRotor` and `addSurface` methods to define a vehicle quickly.
57
+ - single-axis surface tilt
58
+ - dual-axis rotor tilt
59
+ - presets included: `Quadrotor`, `Tailsitter`
60
+
61
+ ## Installation
62
+
63
+ ```sh
64
+ pip install pyFlightPlotter
65
+ ```
66
+
67
+ With video support (`VideoViewport`, which needs OpenCV):
68
+
69
+ ```sh
70
+ pip install "pyFlightPlotter[video]"
71
+ ```
72
+
73
+ The latest development version from GitHub:
74
+
75
+ ```sh
76
+ pip install "pyFlightPlotter @ git+https://github.com/tudelft/pyFlightPlotter"
77
+ ```
78
+
79
+ Requires Python 3.10 or newer. The dependencies are `numpy`, `scipy` and `matplotlib`. You also need an
80
+ interactive matplotlib backend (e.g. QtAgg or TkAgg).
81
+
82
+ ## Usage
83
+
84
+ You build a plotter by subclassing `FlightPlotterBase` and implementing `_populate()`, where you add the
85
+ subplots you want. `BlittedCursor` adds the synchronized cursor, and `connect_viewport()` links a 3D
86
+ `Viewport` (or a `VideoViewport`) to the mouse.
87
+
88
+ A minimal example ([`docs/examples/minimal_example.py`](https://github.com/tudelft/pyFlightPlotter/blob/main/docs/examples/minimal_example.py)):
89
+
90
+ ```python
91
+ import numpy as np
92
+
93
+ from pyFlightPlotter import FlightPlotterBase, BlittedCursor, Viewport, Quadrotor
94
+
95
+ class FlightPlotterMinimalExample(FlightPlotterBase):
96
+ def __init__(self, time, data):
97
+ self.data = data
98
+ super().__init__(time, name="MWE")
99
+ self.define_layout(nrows=1, ncols=1)
100
+ self.plot()
101
+
102
+ def _populate(self):
103
+ self._plot_timeseries(
104
+ self.fig.add_subplot(self.gs[0, 0]),
105
+ light=None,
106
+ solid=[self.data[2], self.data[3]],
107
+ dashed=[self.data[0], self.data[1]],
108
+ series_labels=["Signal 1", "Signal 2"],
109
+ style_labels=[None, "Estimated", "Setpoint"],
110
+ title="Example Time Series",
111
+ ylabel="Amplitude",
112
+ )
113
+
114
+ if __name__ == "__main__":
115
+ t = np.linspace(0, 2, 101)
116
+ data = [
117
+ np.sin(2*np.pi*0.5*t), np.sin(2*np.pi*0.5*t + 0.2),
118
+ np.cos(2*np.pi*0.5*t + 0.4), np.cos(2*np.pi*0.5*t + 0.6),
119
+ ]
120
+
121
+ fp = FlightPlotterMinimalExample(t, data)
122
+ bc = BlittedCursor(fp.all_axes, sharex=True)
123
+
124
+ vp = Viewport(craft=Quadrotor(),
125
+ time=np.array([0., 1., 2.]),
126
+ att=np.array([[1., 0., 0., 0.], [0.7, 0., 0., 0.7], [0., 0., 0., 1.]]),
127
+ # ... many more data can be passed here, but attitude is required
128
+ interpolation="linear",
129
+ follow=False,
130
+ title="Viewport Example",
131
+ )
132
+
133
+ # connect mouse hover
134
+ fp.connect_viewport(vp)
135
+ ```
136
+
137
+ Run it with `python -i` so the windows stay open.
138
+
139
+ For a full example with several figures, a tailsitter, and position/velocity/actuator data, see
140
+ [`docs/examples/complete_example.py`](https://github.com/tudelft/pyFlightPlotter/blob/main/docs/examples/complete_example.py).
141
+ For an example of hooking pyFlightPlotter into your own log-data structure, see
142
+ [indiflight](https://github.com/tudelft/indiflight/blob/global_rls/support/LogAnalysis/indiflightPlotter.py).
143
+
144
+ ### Video synchronization
145
+
146
+ ```python
147
+ from pyFlightPlotter import VideoViewport
148
+
149
+ vv = VideoViewport(time=t, mp4_path="flight.mp4", offset_s=0.0)
150
+ fp.connect_viewport(vv)
151
+ ```
152
+
153
+ The video time is `data_time + offset`. You can set the offset with the slider or text box in the video
154
+ window, or step one frame at a time with the arrow keys. You can also load a file from inside the window.
155
+
156
+ ### Controls
157
+
158
+ | Input | Action |
159
+ |---|---|
160
+ | Mouse hover over a time-series axis | Move the cursor; all connected viewports update |
161
+ | <kbd>Space</kbd> | Play / pause real-time playback |
162
+ | <kbd>←</kbd> / <kbd>→</kbd> (video window) | Shift the video offset by one frame |
163
+
164
+ ### Conventions
165
+
166
+ Quaternions are scalar-first: `[w, x, y, z]`. Arrays are expected with time along the first axis, e.g.
167
+ `Nx4` for attitude and `Nx3` for position, velocity and acceleration. See the `Viewport` docstring for all
168
+ supported inputs.
169
+
170
+ ## Development
171
+
172
+ ```sh
173
+ git clone https://github.com/tudelft/pyFlightPlotter
174
+ cd pyFlightPlotter
175
+ pip install -e ".[video]"
176
+ python -i docs/examples/complete_example.py
177
+ ```
178
+
179
+ ## License
180
+
181
+ GPL-3.0-or-later. See [LICENSE](https://github.com/tudelft/pyFlightPlotter/blob/main/LICENSE).
@@ -0,0 +1,10 @@
1
+ pyFlightPlotter/__init__.py,sha256=Gq4Qu7E6F25fmDFhoLKBvGjDr8RtFvFUOTg38TT69IM,574
2
+ pyFlightPlotter/core.py,sha256=N-yhz-U-KAQrVtwIv4qh-0rGTPW4Psrrzpp9wW5fO5c,40218
3
+ pyFlightPlotter/crafts.py,sha256=xL2k5ohu4CPbf3ZsWAwdPqfZNh4IbQkhxATpgoyHMIU,1604
4
+ pyFlightPlotter/cursor.py,sha256=TvenyFDVUvNMFlFz-NQ9Bng8f90nCibRSDRj6YZIcRI,3467
5
+ pyFlightPlotter/style.py,sha256=SjsRglz_U91Oq0QnJccr3dfDvjD19KzrcHMqvissivM,677
6
+ pyflightplotter-0.3.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
+ pyflightplotter-0.3.1.dist-info/METADATA,sha256=ff2Zaeom9ez6tkam_Ht3leS6eTw0Bp5xbPj_Y4RrrLw,7029
8
+ pyflightplotter-0.3.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ pyflightplotter-0.3.1.dist-info/top_level.txt,sha256=mIR6Po0K39Gzf-hMDp-VJVznog_ZmNxbcEa1WeYWDec,16
10
+ pyflightplotter-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+