-
Notifications
You must be signed in to change notification settings - Fork 0
/
plot_widget.py
225 lines (182 loc) · 7.59 KB
/
plot_widget.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# Author: William Liu <[email protected]>
from PySide6.QtWidgets import (QWidget, QVBoxLayout)
from PySide6.QtCore import Slot, QTimer
from pyqtgraph import GraphicsLayoutWidget
import numpy as np
import json
import consts
def calculate_center_of_pressure(fx, fy, fz, mx, my):
"""Calculate the center of pressure (CoP).
Parameters
----------
fx : float
the force along the x axis
fy : float
the force along the y axis
fz : float
the force along the z axis
mx : float
the moment about the x axis
my : float
the moment about the y axis
Returns
-------
tuple
(x coordinate of the CoP, y coordinate of the CoP)
"""
cop_x = (-1) * ((my + (consts.ZOFF * fx)) / fz)
cop_y = ((mx - (consts.ZOFF * fy)) / fz)
return cop_x, cop_y
class PlotWidget(QWidget):
"""Custom widget that receives data and plots it using pyqtgraph."""
def __init__(self, parent=None) -> None:
"""
Parameters
----------
parent : PySide6.QtWidgets.QWidget, optional
the parent widget, if None the widget becomes a window
"""
super().__init__(parent=parent)
# Initiate the timer that updates the graphs
self.timer = QTimer(parent=self)
self.timer.setInterval(33.33) # ~30Hz, the faster the more resource intensive the app
self.timer.timeout.connect(self.update_plots)
# Initiate variables to store incoming data from DataWorker
self._sample_rate = self._read_settings_file()
samples_to_show = consts.SECONDS_TO_SHOW * self._sample_rate
self.cop_xdirection = [0 for i in range(samples_to_show)]
self.cop_ydirection = [0 for i in range(samples_to_show)]
self.force_zdirection = [0 for i in range(samples_to_show)]
self.emg_tibialis = [0 for i in range(samples_to_show)]
self.emg_soleus = [0 for i in range(samples_to_show)]
# Initiate the pyqygraph widget
self.plots = Plots(self, self._sample_rate)
layout = QVBoxLayout()
layout.addWidget(self.plots)
self.setLayout(layout)
def _read_settings_file(self):
"""Read the settings file to get the sample rate."""
with open("amti_settings.json", 'r') as file:
settings = json.load(file)
sample_rate = settings["sample_rate"]
return sample_rate
@Slot(np.ndarray)
def process_data_from_worker(self, data: np.ndarray) -> None:
"""Slot to receive data and store it in appropriate lists.
Incoming data is array-like with values
[Fx, Fy, Fz, Mx, My, Mz, EMG Tibialis, EMG Soleus]. Extract individual
components of incoming data and add them to their list.
Parameters
----------
data : np.ndarray
incoming data
"""
if data[consts.FZ] > consts.MINIMUM_VERTICAL_FORCE:
cop_x, cop_y = calculate_center_of_pressure(
data[consts.FX],
data[consts.FY],
data[consts.FZ],
data[consts.MX],
data[consts.MY]
)
else:
# This is super kludge, but basically want a threshold below which
# CoP data won't be displayed. Setting to np.NAN works, but raises
# an unavoidable warning that has to do with how pyqtgraph uses np,
# so for now I'll stick with this.
cop_x = 100
cop_y = 100
self.cop_xdirection = self.cop_xdirection[1:]
self.cop_xdirection.append(cop_x)
self.cop_ydirection = self.cop_ydirection[1:]
self.cop_ydirection.append(cop_y)
self.force_zdirection = self.force_zdirection[1:]
self.force_zdirection.append(data[consts.FZ])
self.emg_tibialis = self.emg_tibialis[1:]
self.emg_tibialis.append(data[consts.EMG_1])
self.emg_soleus = self.emg_soleus[1:]
self.emg_soleus.append(data[consts.EMG_2])
@Slot()
def update_plots(self) -> None:
"""Update the graphs with new data."""
self.plots.update(
self.cop_xdirection,
self.cop_ydirection,
self.force_zdirection,
self.emg_tibialis,
self.emg_soleus
)
@Slot()
def start_timer(self) -> None:
"""Start the QTimer."""
self.timer.start()
@Slot()
def stop_timer(self) -> None:
"""Stop the QTimer."""
self.timer.stop()
class Plots(GraphicsLayoutWidget):
"""A class to display a multi-panel pyqtgraph figure."""
def __init__(self, parent, sample_rate) -> None:
"""
Parameters
----------
parent : PySide6.QtWidgets.QWidget
the parent widget
sample_rate : int
sample rate of the DAQ
"""
super().__init__(parent=parent)
# Set cutoff for displaying 1 second of data on the CoP graph
self._cop_cutoff = (-1) * sample_rate
# Create the center of pressure graph
self.cop_plot_item = self.addPlot(row=0, col=0, title="Center of Pressure (m)")
self.cop_plot_item.setRange(xRange=(-0.254, 0.254), yRange=(-0.254, 0.254))
self.cop_plot_item.disableAutoRange(axis='xy')
self.cop_plot_item.invertX(b=True) # AMTI axis definitions have +X on the left side of the platform
self.cop_plot_item.hideButtons() # Hide the auto-scale button
self.cop_plot_line = self.cop_plot_item.plot(
x=[0],
y=[0],
pen=None,
symbol='o',
symbolSize=2,
symbolPen=(167, 204, 237),
symbolBrush=(167, 204, 237)
)
# Create the vertical force graph
self.fz_plot_item = self.addPlot(row=1, col=0, title="Vertical Force (N)")
self.fz_plot_item.setRange(yRange=(consts.FZ_MIN, consts.FZ_MAX))
self.fz_plot_item.disableAutoRange(axis='y')
self.fz_plot_item.hideAxis('bottom')
self.fz_plot_line = self.fz_plot_item.plot(x=[0], y=[0])
# Create the EMG plots
self.emg_tibialis_plot_item = self.addPlot(row=0, col=1, title="EMG: Tibialis")
self.emg_tibialis_plot_item.setRange(yRange=(-2.5, 2.5))
self.emg_tibialis_plot_item.disableAutoRange(axis='y')
self.emg_tibialis_plot_item.hideAxis('bottom')
self.emg_tibialis_plot_line = self.emg_tibialis_plot_item.plot(x=[0], y=[0])
self.emg_soleus_plot_item = self.addPlot(row=1, col=1, title="EMG: Soleus")
self.emg_soleus_plot_item.setRange(yRange=(-2.5, 2.5))
self.emg_soleus_plot_item.disableAutoRange(axis='y')
self.emg_soleus_plot_item.hideAxis('bottom')
self.emg_soleus_plot_line = self.emg_soleus_plot_item.plot(x=[0], y=[0])
def update(self, cop_xdirection, cop_ydirection, force_zdirection, emg_tibialis, emg_soleus) -> None:
"""
Update the graphs with new data.
Parameters
----------
cop_xdirection : list
center of pressure data in x-direction (platform coordinates)
cop_ydirection : list
center of pressure data in y-direction (platform coordinates)
force_zdirection : list
force data in the z-direction (platform coordinates)
emg_tibialis : list
emg data from tibialis sensor
emg_soleus : list
emg data from soleus sensor
"""
self.cop_plot_line.setData(x=cop_xdirection[self._cop_cutoff:], y=cop_ydirection[self._cop_cutoff:])
self.fz_plot_line.setData(y=force_zdirection)
self.emg_tibialis_plot_line.setData(y=emg_tibialis)
self.emg_soleus_plot_line.setData(y=emg_soleus)