LSSTApplications  18.1.0
LSSTDataManagementBasePackage
firefly.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010, 2015 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 from __future__ import absolute_import, division, print_function
24 from past.builtins import long
25 
26 from io import BytesIO
27 from socket import gaierror
28 import tempfile
29 
30 import lsst.afw.display.interface as interface
31 import lsst.afw.display.virtualDevice as virtualDevice
32 import lsst.afw.display.ds9Regions as ds9Regions
33 import lsst.afw.display.displayLib as displayLib
34 import lsst.afw.math as afwMath
35 import lsst.log
36 
37 from .footprints import createFootprintsTable
38 
39 try:
40  import firefly_client
41  _fireflyClient = None
42 except ImportError as e:
43  raise RuntimeError("Cannot import firefly_client: %s" % (e))
44 from ws4py.client import HandshakeError
45 
46 
47 class FireflyError(Exception):
48 
49  def __init__(self, str):
50  Exception.__init__(self, str)
51 
52 
54  """Return the version of firefly_client in use, as a string"""
55  return(firefly_client.__version__)
56 
57 
58 class DisplayImpl(virtualDevice.DisplayImpl):
59  """Device to talk to a firefly display"""
60 
61  @staticmethod
62  def __handleCallbacks(event):
63  if 'type' in event['data']:
64  if event['data']['type'] == 'AREA_SELECT':
65  lsst.log.debug('*************area select')
66  pParams = {'URL': 'http://web.ipac.caltech.edu/staff/roby/demo/wise-m51-band2.fits',
67  'ColorTable': '9'}
68  plot_id = 3
69  global _fireflyClient
70  _fireflyClient.show_fits(fileOnServer=None, plot_id=plot_id, additionalParams=pParams)
71 
72  lsst.log.debug("Callback event info: {}".format(event))
73  return
74  data = dict((_.split('=') for _ in event.get('data', {}).split('&')))
75  if data.get('type') == "POINT":
76  lsst.log.debug("Event Received: %s" % data.get('id'))
77 
78  def __init__(self, display, verbose=False, url=None,
79  name=None, *args, **kwargs):
80  virtualDevice.DisplayImpl.__init__(self, display, verbose)
81 
82  if self.verbose:
83  print("Opening firefly device %s" % (self.display.frame if self.display else "[None]"))
84 
85  global _fireflyClient
86  if not _fireflyClient:
87  import os
88  start_tab = None
89  html_file = kwargs.get('html_file',
90  os.environ.get('FIREFLY_HTML', 'slate.html'))
91  if url is None:
92  if (('fireflyLabExtension' in os.environ) and
93  ('fireflyURLLab' in os.environ)):
94  url = os.environ['fireflyURLLab']
95  start_tab = kwargs.get('start_tab', True)
96  start_browser_tab = kwargs.get('start_browser_tab', False)
97  if (name is None) and ('fireflyChannelLab' in os.environ):
98  name = os.environ['fireflyChannelLab']
99  elif 'FIREFLY_URL' in os.environ:
100  url = os.environ['FIREFLY_URL']
101  else:
102  raise RuntimeError('Cannot determine url from environment; you must pass url')
103  try:
104  if start_tab:
105  if verbose:
106  print('Starting Jupyterlab client')
107  _fireflyClient = firefly_client.FireflyClient.make_lab_client(
108  start_tab=True, start_browser_tab=start_browser_tab,
109  html_file=kwargs.get('html_file'), verbose=verbose)
110  else:
111  if verbose:
112  print('Starting vanilla client')
113  _fireflyClient = firefly_client.FireflyClient.make_client(
114  url=url, html_file=html_file, launch_browser=True,
115  channel_override=name, verbose=verbose)
116  except (HandshakeError, gaierror) as e:
117  raise RuntimeError("Unable to connect to %s: %s" % (url or '', e))
118 
119  try:
120  _fireflyClient.add_listener(self.__handleCallbacks)
121  except Exception as e:
122  raise RuntimeError("Cannot add listener. Browser must be connected" +
123  "to %s: %s" %
124  (_fireflyClient.get_firefly_url(), e))
125 
126  self._isBuffered = False
127  self._regions = []
128  self._regionLayerId = self._getRegionLayerId()
129  self._fireflyFitsID = None
130  self._fireflyMaskOnServer = None
131  self._client = _fireflyClient
132  self._channel = _fireflyClient.channel
133  self._url = _fireflyClient.get_firefly_url()
134  self._maskIds = []
135  self._maskDict = {}
136  self._maskPlaneColors = {}
137  self._maskTransparencies = {}
138  self._lastZoom = None
139  self._lastPan = None
140  self._lastStretch = None
141 
142  def _getRegionLayerId(self):
143  return "lsstRegions%s" % self.display.frame if self.display else "None"
144 
145  def _clearImage(self):
146  """Delete the current image in the Firefly viewer
147  """
148  self._client.dispatch(action_type='ImagePlotCntlr.deletePlotView',
149  payload=dict(plotId=str(self.display.frame)))
150 
151  def _mtv(self, image, mask=None, wcs=None, title=""):
152  """Display an Image and/or Mask on a Firefly display
153  """
154  if title == "":
155  title = str(self.display.frame)
156  if image:
157  if self.verbose:
158  print('displaying image')
159  self._erase()
160 
161  with tempfile.NamedTemporaryFile() as fd:
162  displayLib.writeFitsImage(fd.name, image, wcs, title)
163  fd.flush()
164  fd.seek(0, 0)
165  self._fireflyFitsID = _fireflyClient.upload_data(fd, 'FITS')
166 
167  try:
168  viewer_id = ('image-' + str(_fireflyClient.render_tree_id) + '-' +
169  str(self.frame))
170  except AttributeError:
171  viewer_id = 'image-' + str(self.frame)
172  extraParams = dict(Title=title,
173  MultiImageIdx=0,
174  PredefinedOverlayIds=' ',
175  viewer_id=viewer_id)
176  # Firefly's Javascript API requires a space for parameters;
177  # otherwise the parameter will be ignored
178 
179  if self._lastZoom:
180  extraParams['InitZoomLevel'] = self._lastZoom
181  extraParams['ZoomType'] = 'LEVEL'
182  if self._lastPan:
183  extraParams['InitialCenterPosition'] = '{0:.3f};{1:.3f};PIXEL'.format(
184  self._lastPan[0], self._lastPan[1])
185  if self._lastStretch:
186  extraParams['RangeValues'] = self._lastStretch
187 
188  ret = _fireflyClient.show_fits(self._fireflyFitsID, plot_id=str(self.display.frame),
189  **extraParams)
190 
191  if not ret["success"]:
192  raise RuntimeError("Display of image failed")
193 
194  if mask:
195  if self.verbose:
196  print('displaying mask')
197  with tempfile.NamedTemporaryFile() as fdm:
198  displayLib.writeFitsImage(fdm.name, mask, wcs, title)
199  fdm.flush()
200  fdm.seek(0, 0)
201  self._fireflyMaskOnServer = _fireflyClient.upload_data(fdm, 'FITS')
202 
203  maskPlaneDict = mask.getMaskPlaneDict()
204  for k, v in maskPlaneDict.items():
205  self._maskDict[k] = v
206  self._maskPlaneColors[k] = self.display.getMaskPlaneColor(k)
207  usedPlanes = long(afwMath.makeStatistics(mask, afwMath.SUM).getValue())
208  for k in self._maskDict:
209  if (((1 << self._maskDict[k]) & usedPlanes) and
210  (k in self._maskPlaneColors) and
211  (self._maskPlaneColors[k] is not None) and
212  (self._maskPlaneColors[k].lower() != 'ignore')):
213  _fireflyClient.add_mask(bit_number=self._maskDict[k],
214  image_number=0,
215  plot_id=str(self.display.frame),
216  mask_id=k,
217  title=k + ' - bit %d'%self._maskDict[k],
218  color=self._maskPlaneColors[k],
219  file_on_server=self._fireflyMaskOnServer)
220  if k in self._maskTransparencies:
222  self._maskIds.append(k)
223 
224  def _remove_masks(self):
225  """Remove mask layers"""
226  for k in self._maskIds:
227  _fireflyClient.remove_mask(plot_id=str(self.display.frame), mask_id=k)
228  self._maskIds = []
229 
230  def _buffer(self, enable=True):
231  """!Enable or disable buffering of writes to the display
232  param enable True or False, as appropriate
233  """
234  self._isBuffered = enable
235 
236  def _flush(self):
237  """!Flush any I/O buffers
238  """
239  if not self._regions:
240  return
241 
242  if self.verbose:
243  print("Flushing %d regions" % len(self._regions))
244  print(self._regions)
245 
246  self._regionLayerId = self._getRegionLayerId()
247  _fireflyClient.add_region_data(region_data=self._regions, plot_id=str(self.display.frame),
248  region_layer_id=self._regionLayerId)
249  self._regions = []
250 
251  def _uploadTextData(self, regions):
252  self._regions += regions
253 
254  if not self._isBuffered:
255  self._flush()
256 
257  def _close(self):
258  """Called when the device is closed"""
259  if self.verbose:
260  print("Closing firefly device %s" % (self.display.frame if self.display else "[None]"))
261  if _fireflyClient is not None:
262  _fireflyClient.disconnect()
263  _fireflyClient.session.close()
264 
265  def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None):
266  """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates]
267  Possible values are:
268  + Draw a +
269  x Draw an x
270  * Draw a *
271  o Draw a circle
272  @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
273  An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored)
274  Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended
275  with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle
276  is ignored otherwise).
277 
278  N.b. objects derived from BaseCore include Axes and Quadrupole.
279  """
280  self._uploadTextData(ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle))
281 
282  def _drawLines(self, points, ctype):
283  """Connect the points, a list of (col,row)
284  Ctype is the name of a colour (e.g. 'red')"""
285 
286  self._uploadTextData(ds9Regions.drawLines(points, ctype))
287 
288  def _erase(self):
289  """Erase all overlays on the image"""
290  if self.verbose:
291  print('region layer id is {}'.format(self._regionLayerId))
292  if self._regionLayerId:
293  _fireflyClient.delete_region_layer(self._regionLayerId, plot_id=str(self.display.frame))
294 
295  def _setCallback(self, what, func):
296  if func != interface.noop_callback:
297  try:
298  status = _fireflyClient.add_extension('POINT' if False else 'AREA_SELECT', title=what,
299  plot_id=str(self.display.frame),
300  extension_id=what)
301  if not status['success']:
302  pass
303  except Exception as e:
304  raise RuntimeError("Cannot set callback. Browser must be (re)opened " +
305  "to %s%s : %s" %
306  (_fireflyClient.url_bw,
307  _fireflyClient.channel, e))
308 
309  def _getEvent(self):
310  """Return an event generated by a keypress or mouse click
311  """
312  ev = interface.Event("q")
313 
314  if self.verbose:
315  print("virtual[%s]._getEvent() -> %s" % (self.display.frame, ev))
316 
317  return ev
318  #
319  # Set gray scale
320  #
321 
322  def _scale(self, algorithm, min, max, unit=None, *args, **kwargs):
323  """Scale the image stretch and limits
324 
325  Parameters:
326  -----------
327  algorithm : `str`
328  stretch algorithm, e.g. 'linear', 'log', 'loglog', 'equal', 'squared',
329  'sqrt', 'asinh', powerlaw_gamma'
330  min : `float` or `str`
331  lower limit, or 'minmax' for full range, or 'zscale'
332  max : `float` or `str`
333  upper limit; overrriden if min is 'minmax' or 'zscale'
334  unit : `str`
335  unit for min and max. 'percent', 'absolute', 'sigma'.
336  if not specified, min and max are presumed to be in 'absolute' units.
337 
338  *args, **kwargs : additional position and keyword arguments.
339  The options are shown below:
340 
341  **Q** : `float`, optional
342  The asinh softening parameter for asinh stretch.
343  Use Q=0 for linear stretch, increase Q to make brighter features visible.
344  When not specified or None, Q is calculated by Firefly to use full color range.
345  **gamma**
346  The gamma value for power law gamma stretch (default 2.0)
347  **zscale_contrast** : `int`, optional
348  Contrast parameter in percent for zscale algorithm (default 25)
349  **zscale_samples** : `int`, optional
350  Number of samples for zscale algorithm (default 600)
351  **zscale_samples_perline** : `int`, optional
352  Number of samples per line for zscale algorithm (default 120)
353  """
354  stretch_algorithms = ('linear', 'log', 'loglog', 'equal', 'squared', 'sqrt',
355  'asinh', 'powerlaw_gamma')
356  interval_methods = ('percent', 'maxmin', 'absolute', 'zscale', 'sigma')
357  #
358  #
359  # Normalise algorithm's case
360  #
361  if algorithm:
362  algorithm = dict((a.lower(), a) for a in stretch_algorithms).get(algorithm.lower(), algorithm)
363 
364  if algorithm not in stretch_algorithms:
365  raise FireflyError('Algorithm %s is invalid; please choose one of "%s"' %
366  (algorithm, '", "'.join(stretch_algorithms)))
367  self._stretchAlgorithm = algorithm
368  else:
369  algorithm = 'linear'
370 
371  # Translate parameters for asinh and powerlaw_gamma stretches
372  if 'Q' in kwargs:
373  kwargs['asinh_q_value'] = kwargs['Q']
374  del kwargs['Q']
375 
376  if 'gamma' in kwargs:
377  kwargs['gamma_value'] = kwargs['gamma']
378  del kwargs['gamma']
379 
380  if min == 'minmax':
381  interval_type = 'percent'
382  unit = 'percent'
383  min, max = 0, 100
384  elif min == 'zscale':
385  interval_type = 'zscale'
386  else:
387  interval_type = None
388 
389  if not unit:
390  unit = 'absolute'
391 
392  units = ('percent', 'absolute', 'sigma')
393  if unit not in units:
394  raise FireflyError('Unit %s is invalid; please choose one of "%s"' % (unit, '", "'.join(units)))
395 
396  if unit == 'sigma':
397  interval_type = 'sigma'
398  elif unit == 'absolute' and interval_type is None:
399  interval_type = 'absolute'
400  elif unit == 'percent':
401  interval_type = 'percent'
402 
403  self._stretchMin = min
404  self._stretchMax = max
405  self._stretchUnit = unit
406 
407  if interval_type not in interval_methods:
408  raise FireflyError('Interval method %s is invalid' % interval_type)
409 
410  rval = {}
411  if interval_type != 'zscale':
412  rval = _fireflyClient.set_stretch(str(self.display.frame), stype=interval_type,
413  algorithm=algorithm, lower_value=min,
414  upper_value=max, **kwargs)
415  else:
416  if 'zscale_contrast' not in kwargs:
417  kwargs['zscale_contrast'] = 25
418  if 'zscale_samples' not in kwargs:
419  kwargs['zscale_samples'] = 600
420  if 'zscale_samples_perline' not in kwargs:
421  kwargs['zscale_samples_perline'] = 120
422  rval = _fireflyClient.set_stretch(str(self.display.frame), stype='zscale',
423  algorithm=algorithm, **kwargs)
424 
425  if 'rv_string' in rval:
426  self._lastStretch = rval['rv_string']
427 
428  def _setMaskTransparency(self, transparency, maskName):
429  """Specify mask transparency (percent); or None to not set it when loading masks"""
430  if maskName is not None:
431  masklist = [maskName]
432  else:
433  masklist = set(self._maskIds + list(self.display._defaultMaskPlaneColor.keys()))
434  for k in masklist:
435  self._maskTransparencies[k] = transparency
436  _fireflyClient.dispatch(action_type='ImagePlotCntlr.overlayPlotChangeAttributes',
437  payload={'plotId': str(self.display.frame),
438  'imageOverlayId': k,
439  'attributes': {'opacity': 1.0 - transparency/100.},
440  'doReplot': False})
441 
442  def _getMaskTransparency(self, maskName):
443  """Return the current mask's transparency"""
444  transparency = None
445  if maskName in self._maskTransparencies:
446  transparency = self._maskTransparencies[maskName]
447  return transparency
448 
449  def _setMaskPlaneColor(self, maskName, color):
450  """Specify mask color """
451  _fireflyClient.remove_mask(plot_id=str(self.display.frame),
452  mask_id=maskName)
453  self._maskPlaneColors[maskName] = color
454  if (color.lower() != 'ignore'):
455  _fireflyClient.add_mask(bit_number=self._maskDict[maskName],
456  image_number=1,
457  plot_id=str(self.display.frame),
458  mask_id=maskName,
459  color=self.display.getMaskPlaneColor(maskName),
460  file_on_server=self._fireflyFitsID)
461 
462  def _show(self):
463  """Show the requested window"""
464  if self._client.render_tree_id is not None:
465  # we are using Jupyterlab
466  self._client.dispatch(self._client.ACTION_DICT['StartLabWindow'],
467  {})
468  else:
469  localbrowser, url = _fireflyClient.launch_browser(verbose=self.verbose)
470  if not localbrowser and not self.verbose:
471  _fireflyClient.display_url()
472 
473  #
474  # Zoom and Pan
475  #
476 
477  def _zoom(self, zoomfac):
478  """Zoom display by specified amount
479 
480  Parameters:
481  -----------
482  zoomfac: `float`
483  zoom level in screen pixels per image pixel
484  """
485  self._lastZoom = zoomfac
486  _fireflyClient.set_zoom(plot_id=str(self.display.frame), factor=zoomfac)
487 
488  def _pan(self, colc, rowc):
489  """Pan to specified pixel coordinates
490 
491  Parameters:
492  -----------
493  colc, rowc : `float`
494  column and row in units of pixels (zero-based convention,
495  with the xy0 already subtracted off)
496  """
497  self._lastPan = [colc+0.5, rowc+0.5] # saved for future use in _mtv
498  # Firefly's internal convention is first pixel is (0.5, 0.5)
499  _fireflyClient.set_pan(plot_id=str(self.display.frame), x=colc, y=rowc)
500 
501  # Extensions to the API that are specific to using the Firefly backend
502 
503  def getClient(self):
504  """Get the instance of FireflyClient for this display
505 
506  Returns:
507  --------
508  `firefly_client.FireflyClient`
509  Instance of FireflyClient used by this display
510  """
511  return self._client
512 
513  def clearViewer(self):
514  """Reinitialize the viewer
515  """
516  self._client.reinit_viewer()
517 
518  def resetLayout(self):
519  """Reset the layout of the Firefly Slate browser
520 
521  Clears the display and adds Slate cells to display image in upper left,
522  plot area in upper right, and plots stretch across the bottom
523  """
524  self.clearViewer()
525  try:
526  tables_cell_id = 'tables-' + str(_fireflyClient.render_tree_id)
527  except AttributeError:
528  tables_cell_id = 'tables'
529  self._client.add_cell(row=2, col=0, width=4, height=2, element_type='tables',
530  cell_id=tables_cell_id)
531  try:
532  image_cell_id = ('image-' + str(_fireflyClient.render_tree_id) + '-' +
533  str(self.frame))
534  except AttributeError:
535  image_cell_id = 'image-' + str(self.frame)
536  self._client.add_cell(row=0, col=0, width=2, height=3, element_type='images',
537  cell_id=image_cell_id)
538  try:
539  plots_cell_id = 'plots-' + str(_fireflyClient.render_tree_id)
540  except AttributeError:
541  plots_cell_id = 'plots'
542  self._client.add_cell(row=0, col=2, width=2, height=3, element_type='xyPlots',
543  cell_id=plots_cell_id)
544 
545  def overlayFootprints(self, catalog, color='rgba(74,144,226,0.60)',
546  highlightColor='cyan', selectColor='orange',
547  style='fill', layerString='detection footprints ',
548  titleString='catalog footprints '):
549  """Overlay outlines of footprints from a catalog
550 
551  Overlay outlines of LSST footprints from the input catalog. The colors
552  and style can be specified as parameters, and the base color and style
553  can be changed in the Firefly browser user interface.
554 
555  Parameters:
556  -----------
557  catalog : `lsst.afw.table.SourceCatalog`
558  Source catalog from which to display footprints.
559  color : `str`
560  Color for footprints overlay. Colors can be specified as a name
561  like 'cyan' or afwDisplay.RED; as an rgb value such as
562  'rgb(80,100,220)'; or as rgb plus alpha (transparency) such
563  as 'rgba('74,144,226,0.60)'.
564  highlightColor : `str`
565  Color for highlighted footprints
566  selectColor : `str`
567  Color for selected footprints
568  style : {'fill', 'outline'}
569  Style of footprints display, filled or outline
570  insertColumn : `int`
571  Column at which to insert the "family_id" and "category" columns
572  layerString: `str`
573  Name of footprints layer string, to concatenate with the frame
574  Re-using the layer_string will overwrite the previous table and
575  footprints
576  titleString: `str`
577  Title of catalog, to concatenate with the frame
578  """
579  footprintTable = createFootprintsTable(catalog)
580  with BytesIO() as fd:
581  footprintTable.to_xml(fd)
582  tableval = self._client.upload_data(fd, 'UNKNOWN')
583  self._client.overlay_footprints(footprint_file=tableval,
584  title=titleString + str(self.display.frame),
585  footprint_layer_id=layerString + str(self.display.frame),
586  plot_id=str(self.display.frame),
587  color=color,
588  highlightColor=highlightColor,
589  selectColor=selectColor,
590  style=style)
def _setMaskTransparency(self, transparency, maskName)
Definition: firefly.py:428
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
daf::base::PropertySet * set
Definition: fits.cc:884
Definition: Log.h:691
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def _flush(self)
Flush any I/O buffers.
Definition: firefly.py:236
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:168
def overlayFootprints(self, catalog, color='rgba(74, 144, 226, 0.60)', highlightColor='cyan', selectColor='orange', style='fill', layerString='detection footprints ', titleString='catalog footprints ')
Definition: firefly.py:548
def getMaskPlaneColor(name, frame=None)
Definition: ds9.py:77
def __init__(self, display, verbose=False, url=None, name=None, args, kwargs)
Definition: firefly.py:79
def createFootprintsTable(catalog, xy0=None, insertColumn=4)
Definition: footprints.py:62
daf::base::PropertyList * list
Definition: fits.cc:885