LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
densityPlot.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2008-2013 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22
23"""A set of matplotlib-based classes that displays a grid of 1-d and 2-d slices through an
24N-d density.
25
26The main class, DensityPlot, manages the grid of matplotlib.axes.Axes objects, and holds
27a sequence of Layer objects that each know how to draw individual 1-d or 2-d plots and a
28data object that abstracts away how the N-d density data is actually represented.
29
30For simple cases, users can just create a custom data class with an interface like that of
31the ExampleData class provided here, and use the provided HistogramLayer and SurfaceLayer
32classes directly. In more complicated cases, users may want to create their own Layer classes,
33which may define their own relationship with the data object.
34"""
35
36import collections.abc
37import numpy
38import matplotlib.cm
39import matplotlib.pyplot
40import matplotlib.ticker
41
42__all__ = ("HistogramLayer", "SurfaceLayer", "ScatterLayer", "CrossPointsLayer",
43 "DensityPlot", "ExampleData", "demo")
44
45
47 for label in axes.get_xticklabels():
48 label.set_visible(False)
49
50
52 for label in axes.get_yticklabels():
53 label.set_visible(False)
54
55
56def mergeDefaults(kwds, defaults):
57 copy = defaults.copy()
58 if kwds is not None:
59 copy.update(**kwds)
60 return copy
61
62
64 """A Layer class for DensityPlot for gridded histograms, drawing bar plots in 1-d and
65 colormapped large-pixel images in 2-d.
66
67 Relies on two data object attributes:
68
69 values ----- a (M,N) array of data points, where N is the dimension of the dataset and M is the
70 number of data points
71
72 weights ---- (optional) an array of weights with shape (M,); if not present, all weights will
73 be set to unity
74
75 The need for these data object attributes can be removed by subclassing HistogramLayer and overriding
76 the hist1d and hist2d methods.
77 """
78
79 defaults1d = dict(facecolor='b', alpha=0.5)
80 defaults2d = dict(cmap=matplotlib.cm.Blues, vmin=0.0, interpolation='nearest')
81
82 def __init__(self, tag, bins1d=20, bins2d=(20, 20), kwds1d=None, kwds2d=None):
83 self.tagtag = tag
84 self.bins1dbins1d = bins1d
85 self.bins2dbins2d = bins2d
86 self.kwds1dkwds1d = mergeDefaults(kwds1d, self.defaults1ddefaults1d)
87 self.kwds2dkwds2d = mergeDefaults(kwds2d, self.defaults2ddefaults2d)
88
89 def hist1d(self, data, dim, limits):
90 """Extract points from the data object and compute a 1-d histogram.
91
92 Return value should match that of numpy.histogram: a tuple of (hist, edges),
93 where hist is a 1-d array with size=bins1d, and edges is a 1-d array with
94 size=self.bins1dbins1d+1 giving the upper and lower edges of the bins.
95 """
96 i = data.dimensions.index(dim)
97 if hasattr(data, "weights") and data.weights is not None:
98 weights = data.weights
99 else:
100 weights = None
101 return numpy.histogram(data.values[:, i], bins=self.bins1dbins1d, weights=weights,
102 range=limits, normed=True)
103
104 def hist2d(self, data, xDim, yDim, xLimits, yLimits):
105 """Extract points from the data object and compute a 1-d histogram.
106
107 Return value should match that of numpy.histogram2d: a tuple of (hist, xEdges, yEdges),
108 where hist is a 2-d array with shape=bins2d, xEdges is a 1-d array with size=bins2d[0]+1,
109 and yEdges is a 1-d array with size=bins2d[1]+1.
110 """
111 i = data.dimensions.index(yDim)
112 j = data.dimensions.index(xDim)
113 if hasattr(data, "weights") and data.weights is not None:
114 weights = data.weights
115 else:
116 weights = None
117 return numpy.histogram2d(data.values[:, j], data.values[:, i], bins=self.bins2dbins2d, weights=weights,
118 range=(xLimits, yLimits), normed=True)
119
120 def plotX(self, axes, data, dim):
121 y, xEdge = self.hist1dhist1d(data, dim, axes.get_xlim())
122 xCenter = 0.5*(xEdge[:-1] + xEdge[1:])
123 width = xEdge[1:] - xEdge[:-1]
124 return axes.bar(xCenter, y, width=width, align='center', **self.kwds1dkwds1d)
125
126 def plotY(self, axes, data, dim):
127 x, yEdge = self.hist1dhist1d(data, dim, axes.get_ylim())
128 yCenter = 0.5*(yEdge[:-1] + yEdge[1:])
129 height = yEdge[1:] - yEdge[:-1]
130 return axes.barh(yCenter, x, height=height, align='center', **self.kwds1dkwds1d)
131
132 def plotXY(self, axes, data, xDim, yDim):
133 z, xEdge, yEdge = self.hist2dhist2d(data, xDim, yDim, axes.get_xlim(), axes.get_ylim())
134 return axes.imshow(z.transpose(), aspect='auto', extent=(xEdge[0], xEdge[-1], yEdge[0], yEdge[-1]),
135 origin='lower', **self.kwds2dkwds2d)
136
137
139 """A Layer class that plots individual points in 2-d, and does nothing in 1-d.
140
141 Relies on two data object attributes:
142
143 values ----- a (M,N) array of data points, where N is the dimension of the dataset and M is the
144 number of data points
145
146 weights ---- (optional) an array of weights with shape (M,); will be used to set the color of points
147
148 """
149
150 defaults = dict(linewidth=0, alpha=0.2)
151
152 def __init__(self, tag, **kwds):
153 self.tagtag = tag
154 self.kwdskwds = mergeDefaults(kwds, self.defaultsdefaults)
155
156 def plotX(self, axes, data, dim):
157 pass
158
159 def plotY(self, axes, data, dim):
160 pass
161
162 def plotXY(self, axes, data, xDim, yDim):
163 i = data.dimensions.index(yDim)
164 j = data.dimensions.index(xDim)
165 if hasattr(data, "weights") and data.weights is not None:
166 args = data.values[:, j], data.values[:, i], data.weights
167 else:
168 args = data.values[:, j], data.values[:, i]
169 return axes.scatter(*args, **self.kwds)
170
171
173 """A Layer class for analytic N-d distributions that can be evaluated in 1-d or 2-d slices.
174
175 The 2-d slices are drawn as contours, and the 1-d slices are drawn as simple curves.
176
177 Relies on eval1d and eval2d methods in the data object; this can be avoided by subclassing
178 SurfaceLayer and reimplementing its own eval1d and eval2d methods.
179 """
180
181 defaults1d = dict(linewidth=2, color='r')
182 defaults2d = dict(linewidths=2, cmap=matplotlib.cm.Reds)
183
184 def __init__(self, tag, steps1d=200, steps2d=200, filled=False, kwds1d=None, kwds2d=None):
185 self.tagtag = tag
186 self.steps1dsteps1d = int(steps1d)
187 self.steps2dsteps2d = int(steps2d)
188 self.filledfilled = bool(filled)
189 self.kwds1dkwds1d = mergeDefaults(kwds1d, self.defaults1ddefaults1d)
190 self.kwds2dkwds2d = mergeDefaults(kwds2d, self.defaults2ddefaults2d)
191
192 def eval1d(self, data, dim, x):
193 """Return analytic function values for the given values."""
194 return data.eval1d(dim, x)
195
196 def eval2d(self, data, xDim, yDim, x, y):
197 """Return analytic function values for the given values."""
198 return data.eval2d(xDim, yDim, x, y)
199
200 def plotX(self, axes, data, dim):
201 xMin, xMax = axes.get_xlim()
202 x = numpy.linspace(xMin, xMax, self.steps1dsteps1d)
203 z = self.eval1deval1d(data, dim, x)
204 if z is None:
205 return
206 return axes.plot(x, z, **self.kwds1dkwds1d)
207
208 def plotY(self, axes, data, dim):
209 yMin, yMax = axes.get_ylim()
210 y = numpy.linspace(yMin, yMax, self.steps1dsteps1d)
211 z = self.eval1deval1d(data, dim, y)
212 if z is None:
213 return
214 return axes.plot(z, y, **self.kwds1dkwds1d)
215
216 def plotXY(self, axes, data, xDim, yDim):
217 xMin, xMax = axes.get_xlim()
218 yMin, yMax = axes.get_ylim()
219 xc = numpy.linspace(xMin, xMax, self.steps2dsteps2d)
220 yc = numpy.linspace(yMin, yMax, self.steps2dsteps2d)
221 xg, yg = numpy.meshgrid(xc, yc)
222 z = self.eval2deval2d(data, xDim, yDim, xg, yg)
223 if z is None:
224 return
225 if self.filledfilled:
226 return axes.contourf(xg, yg, z, 6, **self.kwds2dkwds2d)
227 else:
228 return axes.contour(xg, yg, z, 6, **self.kwds2dkwds2d)
229
230
232 """A layer that marks a few points with axis-length vertical and horizontal lines.
233
234 This relies on a "points" data object attribute.
235 """
236
237 defaults = dict(alpha=0.8)
238
239 def __init__(self, tag, colors=("y", "m", "c", "r", "g", "b"), **kwds):
240 self.tagtag = tag
241 self.colorscolors = colors
242 self.kwdskwds = mergeDefaults(kwds, self.defaultsdefaults)
243
244 def plotX(self, axes, data, dim):
245 i = data.dimensions.index(dim)
246 artists = []
247 for n, point in enumerate(data.points):
248 artists.append(axes.axvline(point[i], color=self.colorscolors[n % len(self.colorscolors)], **self.kwdskwds))
249 return artists
250
251 def plotY(self, axes, data, dim):
252 i = data.dimensions.index(dim)
253 artists = []
254 for n, point in enumerate(data.points):
255 artists.append(axes.axhline(point[i], color=self.colorscolors[n % len(self.colorscolors)], **self.kwdskwds))
256 return artists
257
258 def plotXY(self, axes, data, xDim, yDim):
259 i = data.dimensions.index(yDim)
260 j = data.dimensions.index(xDim)
261 artists = []
262 for n, point in enumerate(data.points):
263 artists.append(axes.axvline(point[j], color=self.colorscolors[n % len(self.colorscolors)], **self.kwdskwds))
264 artists.append(axes.axhline(point[i], color=self.colorscolors[n % len(self.colorscolors)], **self.kwdskwds))
265 return artists
266
267
269 """An object that manages a matrix of matplotlib.axes.Axes objects that represent a set of 1-d and 2-d
270 slices through an N-d density.
271 """
272
273 class LayerDict(collections.abc.MutableMapping):
274
275 def __init__(self, parent):
276 self._dict_dict = dict()
277 self._parent_parent = parent
278
279 def __delitem__(self, name):
280 layer = self._dict_dict.pop(name)
281 self._parent_parent._dropLayer(name, layer)
282
283 def __setitem__(self, name, layer):
284 self.pop(name, None)
285 self._dict_dict[name] = layer
286 self._parent_parent._plotLayer(name, layer)
287
288 def __getitem__(self, name):
289 return self._dict_dict[name]
290
291 def __iter__(self):
292 return iter(self._dict_dict)
293
294 def __len__(self):
295 return len(self._dict_dict)
296
297 def __str__(self):
298 return str(self._dict_dict)
299
300 def __repr__(self):
301 return repr(self._dict_dict)
302
303 def replot(self, name):
304 layer = self._dict_dict[name]
305 self._parent_parent._dropLayer(name, layer)
306 self._parent_parent._plotLayer(name, layer)
307
308 def __init__(self, figure, **kwds):
309 self.figurefigure = figure
310 self.datadata = dict(kwds)
311 active = []
312 self._lower_lower = dict()
313 self._upper_upper = dict()
314 # We merge the dimension name lists manually rather than using sets to preserve the order.
315 # Most of the time we expect all data objects to have the same dimensions anyway.
316 for v in self.datadata.values():
317 for dim in v.dimensions:
318 if dim not in active:
319 active.append(dim)
320 self._lower_lower[dim] = v.lower[dim]
321 self._upper_upper[dim] = v.upper[dim]
322 else:
323 self._lower_lower[dim] = min(v.lower[dim], self._lower_lower[dim])
324 self._upper_upper[dim] = max(v.upper[dim], self._upper_upper[dim])
325 self._active_active = tuple(active)
326 self._all_dims_all_dims = frozenset(self._active_active)
327 self.figurefigure.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95, hspace=0.01, wspace=0.01)
328 self._build_axes_build_axes()
329 self.layerslayers = self.LayerDictLayerDict(self)
330
331 def _dropLayer(self, name, layer):
332 def removeArtist(*key):
333 try:
334 self._objs_objs.pop(key).remove()
335 except AttributeError:
336 # sometimes the value might be None, which doesn't have a remove
337 pass
338 except TypeError:
339 # probably a matplotlib bug: remove sometimes raises an exception,
340 # but it still works
341 pass
342 for i, yDim in enumerate(self._active_active):
343 removeArtist(None, i, name)
344 removeArtist(i, None, name)
345 for j, xDim in enumerate(self._active_active):
346 if i == j:
347 continue
348 removeArtist(i, j, name)
349
350 def _plotLayer(self, name, layer):
351 for i, yDim in enumerate(self._active_active):
352 if yDim not in self.datadata[layer.tag].dimensions:
353 continue
354 self._objs_objs[None, i, name] = layer.plotX(self._axes_axes[None, i], self.datadata[layer.tag], yDim)
355 self._objs_objs[i, None, name] = layer.plotY(self._axes_axes[i, None], self.datadata[layer.tag], yDim)
356 for j, xDim in enumerate(self._active_active):
357 if xDim not in self.datadata[layer.tag].dimensions:
358 continue
359 if i == j:
360 continue
361 self._objs_objs[i, j, name] = layer.plotXY(self._axes_axes[i, j], self.datadata[layer.tag], xDim, yDim)
362 self._axes_axes[None, i].xaxis.set_major_locator(matplotlib.ticker.MaxNLocator(nbins=5, prune='both'))
363 self._axes_axes[i, None].yaxis.set_major_locator(matplotlib.ticker.MaxNLocator(nbins=5, prune='both'))
364 self._axes_axes[None, i].xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
365 self._axes_axes[i, None].yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
366
367 def _get_active(self):
368 return self._active_active
369
370 def _set_active(self, active):
371 s = set(active)
372 if len(s) != len(active):
373 raise ValueError("Active set contains duplicates")
374 if not self._all_dims_all_dims.issuperset(s):
375 raise ValueError("Invalid values in active set")
376 self._active_active = tuple(active)
377 self.replotreplot()
378 active = property(_get_active, _set_active, doc="sequence of active dimensions to plot (sequence of str)")
379
380 def replot(self):
381 self._lower_lower = {dim: min(self.datadata[k].lower[dim] for k in self.datadata) for dim in self._active_active}
382 self._upper_upper = {dim: max(self.datadata[k].upper[dim] for k in self.datadata) for dim in self._active_active}
383 self._build_axes_build_axes()
384 for name, layer in self.layerslayers.items():
385 self._plotLayer_plotLayer(name, layer)
386
387 def _build_axes(self):
388 self.figurefigure.clear()
389 self._axes_axes = dict()
390 self._objs_objs = dict()
391 n = len(self._active_active)
392 iStride = n + 1
393 jStride = -1
394 iStart = n + 1
395 jStart = n
396 for i in range(n):
397 j = i
398 axesX = self._axes_axes[None, j] = self.figurefigure.add_subplot(n+1, n+1, jStart+j*jStride)
399 axesX.autoscale(False, axis='x')
400 axesX.xaxis.tick_top()
401 axesX.set_xlim(self._lower_lower[self._active_active[j]], self._upper_upper[self._active_active[j]])
402 hide_yticklabels(axesX)
403 bbox = axesX.get_position()
404 bbox.y1 -= 0.035
405 axesX.set_position(bbox)
406 axesY = self._axes_axes[i, None] = self.figurefigure.add_subplot(n+1, n+1, iStart + iStart+i*iStride)
407 axesY.autoscale(False, axis='y')
408 axesY.yaxis.tick_right()
409 axesY.set_ylim(self._lower_lower[self._active_active[i]], self._upper_upper[self._active_active[i]])
410 hide_xticklabels(axesY)
411 bbox = axesY.get_position()
412 bbox.x1 -= 0.035
413 axesY.set_position(bbox)
414 for i in range(n):
415 for j in range(n):
416 axesXY = self._axes_axes[i, j] = self.figurefigure.add_subplot(
417 n+1, n+1, iStart+i*iStride + jStart+j*jStride,
418 sharex=self._axes_axes[None, j],
419 sharey=self._axes_axes[i, None]
420 )
421 axesXY.autoscale(False)
422 if j < n - 1:
423 hide_yticklabels(axesXY)
424 if i < n - 1:
425 hide_xticklabels(axesXY)
426 for i in range(n):
427 j = i
428 xbox = self._axes_axes[None, j].get_position()
429 ybox = self._axes_axes[i, None].get_position()
430 self.figurefigure.text(0.5*(xbox.x0 + xbox.x1), 0.5*(ybox.y0 + ybox.y1), self.activeactive[i],
431 ha='center', va='center', weight='bold')
432 self._axes_axes[i, j].get_frame().set_facecolor('none')
433
434 def draw(self):
435 self.figurefigure.canvas.draw()
436
437
439 """An example data object for DensityPlot, demonstrating the necessarity interface.
440
441 There are two levels of requirements for a data object. First are the attributes
442 required by the DensityPlot object itself; these must be present on every data object:
443
444 dimensions ------ a sequence of strings that provide names for the dimensions
445
446 lower ----------- a dictionary of {dimension-name: lower-bound}
447
448 upper ----------- a dictionary of {dimension-name: upper-bound}
449
450 The second level of requirements are those of the Layer objects provided here. These
451 may be absent if the associated Layer is not used or is subclassed to reimplement the
452 Layer method that calls the data object method. Currently, these include:
453
454 eval1d, eval2d -- methods used by the SurfaceLayer class; see their docs for more info
455
456 values ---------- attribute used by the HistogramLayer and ScatterLayer classes, an array
457 with shape (M,N), where N is the number of dimension and M is the number
458 of data points
459
460 weights --------- optional attribute used by the HistogramLayer and ScatterLayer classes,
461 a 1-d array with size=M that provides weights for each data point
462 """
463
464 def __init__(self):
465 self.dimensionsdimensions = ["a", "b", "c"]
466 self.mumu = numpy.array([-10.0, 0.0, 10.0])
467 self.sigmasigma = numpy.array([3.0, 2.0, 1.0])
468 self.lowerlower = {dim: -3*self.sigmasigma[i] + self.mumu[i] for i, dim in enumerate(self.dimensionsdimensions)}
469 self.upperupper = {dim: 3*self.sigmasigma[i] + self.mumu[i] for i, dim in enumerate(self.dimensionsdimensions)}
470 self.valuesvalues = numpy.random.randn(2000, 3) * self.sigmasigma[numpy.newaxis, :] + self.mumu[numpy.newaxis, :]
471
472 def eval1d(self, dim, x):
473 """Evaluate the 1-d analytic function for the given dim at points x (a 1-d numpy array;
474 this method must be numpy-vectorized).
475 """
476 i = self.dimensionsdimensions.index(dim)
477 return numpy.exp(-0.5*((x-self.mumu[i])/self.sigmasigma[i])**2) / ((2.0*numpy.pi)**0.5 * self.sigmasigma[i])
478
479 def eval2d(self, xDim, yDim, x, y):
480 """Evaluate the 2-d analytic function for the given xDim and yDim at points x,y
481 (2-d numpy arrays with the same shape; this method must be numpy-vectorized).
482 """
483 i = self.dimensionsdimensions.index(yDim)
484 j = self.dimensionsdimensions.index(xDim)
485 return (numpy.exp(-0.5*(((x-self.mumu[j])/self.sigmasigma[j])**2 + ((y-self.mumu[i])/self.sigmasigma[i])**2))
486 / (2.0*numpy.pi * self.sigmasigma[j]*self.sigmasigma[i]))
487
488
489def demo():
490 """Create and return a DensityPlot with example data."""
491 fig = matplotlib.pyplot.figure()
492 p = DensityPlot(fig, primary=ExampleData())
493 p.layers['histogram'] = HistogramLayer('primary')
494 p.layers['surface'] = SurfaceLayer('primary')
495 p.draw()
496 return p
std::vector< SchemaItem< Flag > > * items
int min
int max
def __init__(self, tag, colors=("y", "m", "c", "r", "g", "b"), **kwds)
Definition: densityPlot.py:239
def hist2d(self, data, xDim, yDim, xLimits, yLimits)
Definition: densityPlot.py:104
def __init__(self, tag, bins1d=20, bins2d=(20, 20), kwds1d=None, kwds2d=None)
Definition: densityPlot.py:82
def plotXY(self, axes, data, xDim, yDim)
Definition: densityPlot.py:162
def eval2d(self, data, xDim, yDim, x, y)
Definition: densityPlot.py:196
def plotXY(self, axes, data, xDim, yDim)
Definition: densityPlot.py:216
def __init__(self, tag, steps1d=200, steps2d=200, filled=False, kwds1d=None, kwds2d=None)
Definition: densityPlot.py:184
daf::base::PropertySet * set
Definition: fits.cc:912