1 | import logging |
---|
2 | import copy |
---|
3 | import numpy |
---|
4 | import pylab |
---|
5 | |
---|
6 | from PyQt4 import QtGui |
---|
7 | |
---|
8 | # TODO: Replace the qt4agg calls below with qt5 equivalent. |
---|
9 | # Requires some code modifications. |
---|
10 | # https://www.boxcontrol.net/embedding-matplotlib-plot-on-pyqt5-gui.html |
---|
11 | # |
---|
12 | from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas |
---|
13 | from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar |
---|
14 | import matplotlib.pyplot as plt |
---|
15 | |
---|
16 | DEFAULT_CMAP = pylab.cm.jet |
---|
17 | |
---|
18 | import PlotHelper |
---|
19 | |
---|
20 | class Plotter2D(QtGui.QDialog): |
---|
21 | def __init__(self, parent=None): |
---|
22 | super(Plotter2D, self).__init__(parent) |
---|
23 | |
---|
24 | # Required for the communicator |
---|
25 | self.parent = parent |
---|
26 | |
---|
27 | # a figure instance to plot on |
---|
28 | self.figure = plt.figure() |
---|
29 | |
---|
30 | # this is the Canvas Widget that displays the `figure` |
---|
31 | # it takes the `figure` instance as a parameter to __init__ |
---|
32 | self.canvas = FigureCanvas(self.figure) |
---|
33 | |
---|
34 | # this is the Navigation widget |
---|
35 | # it takes the Canvas widget and a parent |
---|
36 | self.toolbar = NavigationToolbar(self.canvas, self) |
---|
37 | |
---|
38 | # set the layout |
---|
39 | layout = QtGui.QVBoxLayout() |
---|
40 | layout.addWidget(self.canvas) |
---|
41 | layout.addWidget(self.toolbar) |
---|
42 | self.setLayout(layout) |
---|
43 | |
---|
44 | # defaults |
---|
45 | self._current_plot = 111 |
---|
46 | self._data = [] |
---|
47 | self._qx_data = [] |
---|
48 | self._qy_data = [] |
---|
49 | |
---|
50 | # default color map |
---|
51 | self._cmap = DEFAULT_CMAP |
---|
52 | |
---|
53 | self._ax = self.figure.add_subplot(self._current_plot) |
---|
54 | |
---|
55 | # Notify the helper |
---|
56 | PlotHelper.addPlot(self) |
---|
57 | # Notify the listeners |
---|
58 | self.parent.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots()) |
---|
59 | |
---|
60 | def data(self, data=None): |
---|
61 | """ data setter """ |
---|
62 | self._data = data.data |
---|
63 | self._qx_data=data.qx_data |
---|
64 | self._qy_data=data.qy_data |
---|
65 | self._xmin=data.xmin |
---|
66 | self._xmax=data.xmax |
---|
67 | self._ymin=data.ymin |
---|
68 | self._ymax=data.ymax |
---|
69 | self._zmin=data.zmin |
---|
70 | self._zmax=data.zmax |
---|
71 | self._color=0 |
---|
72 | self._symbol=0 |
---|
73 | self._label=data.name |
---|
74 | self._scale = 'linear' |
---|
75 | |
---|
76 | self.x_label(xlabel=data._xaxis + data._xunit) |
---|
77 | self.y_label(ylabel=data._yaxis + data._yunit) |
---|
78 | self.title(title=data.title) |
---|
79 | |
---|
80 | def title(self, title=""): |
---|
81 | """ title setter """ |
---|
82 | self._title = title |
---|
83 | |
---|
84 | def id(self, id=""): |
---|
85 | """ id setter """ |
---|
86 | self._id = id |
---|
87 | |
---|
88 | def x_label(self, xlabel=""): |
---|
89 | """ x-label setter """ |
---|
90 | self._xlabel = r'$%s$'% xlabel |
---|
91 | |
---|
92 | def y_label(self, ylabel=""): |
---|
93 | """ y-label setter """ |
---|
94 | self._ylabel = r'$%s$'% ylabel |
---|
95 | |
---|
96 | def clean(self): |
---|
97 | """ |
---|
98 | Redraw the graph |
---|
99 | """ |
---|
100 | self.figure.delaxes(self._ax) |
---|
101 | self._ax = self.figure.add_subplot(self._current_plot) |
---|
102 | |
---|
103 | def plot(self, marker=None, linestyle=None): |
---|
104 | """ |
---|
105 | Plot 2D self._data |
---|
106 | """ |
---|
107 | # create an axis |
---|
108 | ax = self._ax |
---|
109 | |
---|
110 | # graph properties |
---|
111 | ax.set_xlabel(self._xlabel) |
---|
112 | ax.set_ylabel(self._ylabel) |
---|
113 | ax.set_title(label=self._title) |
---|
114 | |
---|
115 | # Re-adjust colorbar |
---|
116 | # self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2) |
---|
117 | |
---|
118 | output = self._build_matrix() |
---|
119 | |
---|
120 | im = ax.imshow(output, |
---|
121 | interpolation='nearest', |
---|
122 | origin='lower', |
---|
123 | vmin=self._zmin, vmax=self._zmax, |
---|
124 | cmap=self._cmap, |
---|
125 | extent=(self._xmin, self._xmax, |
---|
126 | self._ymin, self._ymax)) |
---|
127 | |
---|
128 | cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7]) |
---|
129 | cb = self.figure.colorbar(im, cax=cbax) |
---|
130 | cb.update_bruteforce(im) |
---|
131 | cb.set_label('$' + self._scale + '$') |
---|
132 | |
---|
133 | # Schedule the draw for the next time the event loop is idle. |
---|
134 | self.canvas.draw_idle() |
---|
135 | |
---|
136 | def closeEvent(self, event): |
---|
137 | """ |
---|
138 | Overwrite the close event adding helper notification |
---|
139 | """ |
---|
140 | # Please remove me from your database. |
---|
141 | PlotHelper.deletePlot(PlotHelper.idOfPlot(self)) |
---|
142 | # Notify the listeners |
---|
143 | self.parent.communicator.activeGraphsSignal.emit(PlotHelper.currentPlots()) |
---|
144 | event.accept() |
---|
145 | |
---|
146 | def _build_matrix(self): |
---|
147 | """ |
---|
148 | Build a matrix for 2d plot from a vector |
---|
149 | Returns a matrix (image) with ~ square binning |
---|
150 | Requirement: need 1d array formats of |
---|
151 | self.data, self._qx_data, and self._qy_data |
---|
152 | where each one corresponds to z, x, or y axis values |
---|
153 | |
---|
154 | """ |
---|
155 | # No qx or qy given in a vector format |
---|
156 | if self._qx_data == None or self._qy_data == None \ |
---|
157 | or self._qx_data.ndim != 1 or self._qy_data.ndim != 1: |
---|
158 | # do we need deepcopy here? |
---|
159 | return self._data |
---|
160 | |
---|
161 | # maximum # of loops to fillup_pixels |
---|
162 | # otherwise, loop could never stop depending on data |
---|
163 | max_loop = 1 |
---|
164 | # get the x and y_bin arrays. |
---|
165 | self._get_bins() |
---|
166 | # set zero to None |
---|
167 | |
---|
168 | #Note: Can not use scipy.interpolate.Rbf: |
---|
169 | # 'cause too many data points (>10000)<=JHC. |
---|
170 | # 1d array to use for weighting the data point averaging |
---|
171 | #when they fall into a same bin. |
---|
172 | weights_data = numpy.ones([self._data.size]) |
---|
173 | # get histogram of ones w/len(data); this will provide |
---|
174 | #the weights of data on each bins |
---|
175 | weights, xedges, yedges = numpy.histogram2d(x=self._qy_data, |
---|
176 | y=self._qx_data, |
---|
177 | bins=[self.y_bins, self.x_bins], |
---|
178 | weights=weights_data) |
---|
179 | # get histogram of data, all points into a bin in a way of summing |
---|
180 | image, xedges, yedges = numpy.histogram2d(x=self._qy_data, |
---|
181 | y=self._qx_data, |
---|
182 | bins=[self.y_bins, self.x_bins], |
---|
183 | weights=self._data) |
---|
184 | # Now, normalize the image by weights only for weights>1: |
---|
185 | # If weight == 1, there is only one data point in the bin so |
---|
186 | # that no normalization is required. |
---|
187 | image[weights > 1] = image[weights > 1] / weights[weights > 1] |
---|
188 | # Set image bins w/o a data point (weight==0) as None (was set to zero |
---|
189 | # by histogram2d.) |
---|
190 | image[weights == 0] = None |
---|
191 | |
---|
192 | # Fill empty bins with 8 nearest neighbors only when at least |
---|
193 | #one None point exists |
---|
194 | loop = 0 |
---|
195 | |
---|
196 | # do while loop until all vacant bins are filled up up |
---|
197 | #to loop = max_loop |
---|
198 | while not(numpy.isfinite(image[weights == 0])).all(): |
---|
199 | if loop >= max_loop: # this protects never-ending loop |
---|
200 | break |
---|
201 | image = self._fillup_pixels(image=image, weights=weights) |
---|
202 | loop += 1 |
---|
203 | |
---|
204 | return image |
---|
205 | |
---|
206 | def _get_bins(self): |
---|
207 | """ |
---|
208 | get bins |
---|
209 | set x_bins and y_bins into self, 1d arrays of the index with |
---|
210 | ~ square binning |
---|
211 | Requirement: need 1d array formats of |
---|
212 | self._qx_data, and self._qy_data |
---|
213 | where each one corresponds to x, or y axis values |
---|
214 | """ |
---|
215 | # No qx or qy given in a vector format |
---|
216 | if self._qx_data == None or self._qy_data == None \ |
---|
217 | or self._qx_data.ndim != 1 or self._qy_data.ndim != 1: |
---|
218 | return self._data |
---|
219 | |
---|
220 | # find max and min values of qx and qy |
---|
221 | xmax = self._qx_data.max() |
---|
222 | xmin = self._qx_data.min() |
---|
223 | ymax = self._qy_data.max() |
---|
224 | ymin = self._qy_data.min() |
---|
225 | |
---|
226 | # calculate the range of qx and qy: this way, it is a little |
---|
227 | # more independent |
---|
228 | x_size = xmax - xmin |
---|
229 | y_size = ymax - ymin |
---|
230 | |
---|
231 | # estimate the # of pixels on each axes |
---|
232 | npix_y = int(numpy.floor(numpy.sqrt(len(self._qy_data)))) |
---|
233 | npix_x = int(numpy.floor(len(self._qy_data) / npix_y)) |
---|
234 | |
---|
235 | # bin size: x- & y-directions |
---|
236 | xstep = x_size / (npix_x - 1) |
---|
237 | ystep = y_size / (npix_y - 1) |
---|
238 | |
---|
239 | # max and min taking account of the bin sizes |
---|
240 | xmax = xmax + xstep / 2.0 |
---|
241 | xmin = xmin - xstep / 2.0 |
---|
242 | ymax = ymax + ystep / 2.0 |
---|
243 | ymin = ymin - ystep / 2.0 |
---|
244 | |
---|
245 | # store x and y bin centers in q space |
---|
246 | x_bins = numpy.linspace(xmin, xmax, npix_x) |
---|
247 | y_bins = numpy.linspace(ymin, ymax, npix_y) |
---|
248 | |
---|
249 | #set x_bins and y_bins |
---|
250 | self.x_bins = x_bins |
---|
251 | self.y_bins = y_bins |
---|
252 | |
---|
253 | def _fillup_pixels(self, image=None, weights=None): |
---|
254 | """ |
---|
255 | Fill z values of the empty cells of 2d image matrix |
---|
256 | with the average over up-to next nearest neighbor points |
---|
257 | |
---|
258 | :param image: (2d matrix with some zi = None) |
---|
259 | |
---|
260 | :return: image (2d array ) |
---|
261 | |
---|
262 | :TODO: Find better way to do for-loop below |
---|
263 | |
---|
264 | """ |
---|
265 | # No image matrix given |
---|
266 | if image == None or numpy.ndim(image) != 2 \ |
---|
267 | or numpy.isfinite(image).all() \ |
---|
268 | or weights == None: |
---|
269 | return image |
---|
270 | # Get bin size in y and x directions |
---|
271 | len_y = len(image) |
---|
272 | len_x = len(image[1]) |
---|
273 | temp_image = numpy.zeros([len_y, len_x]) |
---|
274 | weit = numpy.zeros([len_y, len_x]) |
---|
275 | # do for-loop for all pixels |
---|
276 | for n_y in range(len(image)): |
---|
277 | for n_x in range(len(image[1])): |
---|
278 | # find only null pixels |
---|
279 | if weights[n_y][n_x] > 0 or numpy.isfinite(image[n_y][n_x]): |
---|
280 | continue |
---|
281 | else: |
---|
282 | # find 4 nearest neighbors |
---|
283 | # check where or not it is at the corner |
---|
284 | if n_y != 0 and numpy.isfinite(image[n_y - 1][n_x]): |
---|
285 | temp_image[n_y][n_x] += image[n_y - 1][n_x] |
---|
286 | weit[n_y][n_x] += 1 |
---|
287 | if n_x != 0 and numpy.isfinite(image[n_y][n_x - 1]): |
---|
288 | temp_image[n_y][n_x] += image[n_y][n_x - 1] |
---|
289 | weit[n_y][n_x] += 1 |
---|
290 | if n_y != len_y - 1 and numpy.isfinite(image[n_y + 1][n_x]): |
---|
291 | temp_image[n_y][n_x] += image[n_y + 1][n_x] |
---|
292 | weit[n_y][n_x] += 1 |
---|
293 | if n_x != len_x - 1 and numpy.isfinite(image[n_y][n_x + 1]): |
---|
294 | temp_image[n_y][n_x] += image[n_y][n_x + 1] |
---|
295 | weit[n_y][n_x] += 1 |
---|
296 | # go 4 next nearest neighbors when no non-zero |
---|
297 | # neighbor exists |
---|
298 | if n_y != 0 and n_x != 0 and\ |
---|
299 | numpy.isfinite(image[n_y - 1][n_x - 1]): |
---|
300 | temp_image[n_y][n_x] += image[n_y - 1][n_x - 1] |
---|
301 | weit[n_y][n_x] += 1 |
---|
302 | if n_y != len_y - 1 and n_x != 0 and \ |
---|
303 | numpy.isfinite(image[n_y + 1][n_x - 1]): |
---|
304 | temp_image[n_y][n_x] += image[n_y + 1][n_x - 1] |
---|
305 | weit[n_y][n_x] += 1 |
---|
306 | if n_y != len_y and n_x != len_x - 1 and \ |
---|
307 | numpy.isfinite(image[n_y - 1][n_x + 1]): |
---|
308 | temp_image[n_y][n_x] += image[n_y - 1][n_x + 1] |
---|
309 | weit[n_y][n_x] += 1 |
---|
310 | if n_y != len_y - 1 and n_x != len_x - 1 and \ |
---|
311 | numpy.isfinite(image[n_y + 1][n_x + 1]): |
---|
312 | temp_image[n_y][n_x] += image[n_y + 1][n_x + 1] |
---|
313 | weit[n_y][n_x] += 1 |
---|
314 | |
---|
315 | # get it normalized |
---|
316 | ind = (weit > 0) |
---|
317 | image[ind] = temp_image[ind] / weit[ind] |
---|
318 | |
---|
319 | return image |
---|