LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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  extraParams = dict(Title=title,
168  MultiImageIdx=0,
169  PredefinedOverlayIds=' ',
170  viewer_id='image-' + str(self.frame))
171  # Firefly's Javascript API requires a space for parameters;
172  # otherwise the parameter will be ignored
173 
174  if self._lastZoom:
175  extraParams['InitZoomLevel'] = self._lastZoom
176  extraParams['ZoomType'] = 'LEVEL'
177  if self._lastPan:
178  extraParams['InitialCenterPosition'] = '{0:.3f};{1:.3f};PIXEL'.format(
179  self._lastPan[0], self._lastPan[1])
180  if self._lastStretch:
181  extraParams['RangeValues'] = self._lastStretch
182 
183  ret = _fireflyClient.show_fits(self._fireflyFitsID, plot_id=str(self.display.frame),
184  **extraParams)
185 
186  if not ret["success"]:
187  raise RuntimeError("Display of image failed")
188 
189  if mask:
190  if self.verbose:
191  print('displaying mask')
192  with tempfile.NamedTemporaryFile() as fdm:
193  displayLib.writeFitsImage(fdm.name, mask, wcs, title)
194  fdm.flush()
195  fdm.seek(0, 0)
196  self._fireflyMaskOnServer = _fireflyClient.upload_data(fdm, 'FITS')
197 
198  maskPlaneDict = mask.getMaskPlaneDict()
199  for k, v in maskPlaneDict.items():
200  self._maskDict[k] = v
201  self._maskPlaneColors[k] = self.display.getMaskPlaneColor(k)
202  usedPlanes = long(afwMath.makeStatistics(mask, afwMath.SUM).getValue())
203  for k in self._maskDict:
204  if (((1 << self._maskDict[k]) & usedPlanes) and
205  (k in self._maskPlaneColors) and
206  (self._maskPlaneColors[k] is not None) and
207  (self._maskPlaneColors[k].lower() != 'ignore')):
208  _fireflyClient.add_mask(bit_number=self._maskDict[k],
209  image_number=0,
210  plot_id=str(self.display.frame),
211  mask_id=k,
212  title=k + ' - bit %d'%self._maskDict[k],
213  color=self._maskPlaneColors[k],
214  file_on_server=self._fireflyMaskOnServer)
215  if k in self._maskTransparencies:
217  self._maskIds.append(k)
218 
219  def _remove_masks(self):
220  """Remove mask layers"""
221  for k in self._maskIds:
222  _fireflyClient.remove_mask(plot_id=str(self.display.frame), mask_id=k)
223  self._maskIds = []
224 
225  def _buffer(self, enable=True):
226  """!Enable or disable buffering of writes to the display
227  param enable True or False, as appropriate
228  """
229  self._isBuffered = enable
230 
231  def _flush(self):
232  """!Flush any I/O buffers
233  """
234  if not self._regions:
235  return
236 
237  if self.verbose:
238  print("Flushing %d regions" % len(self._regions))
239  print(self._regions)
240 
241  self._regionLayerId = self._getRegionLayerId()
242  _fireflyClient.add_region_data(region_data=self._regions, plot_id=str(self.display.frame),
243  region_layer_id=self._regionLayerId)
244  self._regions = []
245 
246  def _uploadTextData(self, regions):
247  self._regions += regions
248 
249  if not self._isBuffered:
250  self._flush()
251 
252  def _close(self):
253  """Called when the device is closed"""
254  if self.verbose:
255  print("Closing firefly device %s" % (self.display.frame if self.display else "[None]"))
256  if _fireflyClient is not None:
257  _fireflyClient.disconnect()
258  _fireflyClient.session.close()
259 
260  def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None):
261  """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates]
262  Possible values are:
263  + Draw a +
264  x Draw an x
265  * Draw a *
266  o Draw a circle
267  @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored)
268  An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored)
269  Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended
270  with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle
271  is ignored otherwise).
272 
273  N.b. objects derived from BaseCore include Axes and Quadrupole.
274  """
275  self._uploadTextData(ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle))
276 
277  def _drawLines(self, points, ctype):
278  """Connect the points, a list of (col,row)
279  Ctype is the name of a colour (e.g. 'red')"""
280 
281  self._uploadTextData(ds9Regions.drawLines(points, ctype))
282 
283  def _erase(self):
284  """Erase all overlays on the image"""
285  if self.verbose:
286  print('region layer id is {}'.format(self._regionLayerId))
287  if self._regionLayerId:
288  _fireflyClient.delete_region_layer(self._regionLayerId, plot_id=str(self.display.frame))
289 
290  def _setCallback(self, what, func):
291  if func != interface.noop_callback:
292  try:
293  status = _fireflyClient.add_extension('POINT' if False else 'AREA_SELECT', title=what,
294  plot_id=str(self.display.frame),
295  extension_id=what)
296  if not status['success']:
297  pass
298  except Exception as e:
299  raise RuntimeError("Cannot set callback. Browser must be (re)opened " +
300  "to %s%s : %s" %
301  (_fireflyClient.url_bw,
302  _fireflyClient.channel, e))
303 
304  def _getEvent(self):
305  """Return an event generated by a keypress or mouse click
306  """
307  ev = interface.Event("q")
308 
309  if self.verbose:
310  print("virtual[%s]._getEvent() -> %s" % (self.display.frame, ev))
311 
312  return ev
313  #
314  # Set gray scale
315  #
316 
317  def _scale(self, algorithm, min, max, unit=None, *args, **kwargs):
318  """Scale the image stretch and limits
319 
320  Parameters:
321  -----------
322  algorithm : `str`
323  stretch algorithm, e.g. 'linear', 'log', 'loglog', 'equal', 'squared',
324  'sqrt', 'asinh', powerlaw_gamma'
325  min : `float` or `str`
326  lower limit, or 'minmax' for full range, or 'zscale'
327  max : `float` or `str`
328  upper limit; overrriden if min is 'minmax' or 'zscale'
329  unit : `str`
330  unit for min and max. 'percent', 'absolute', 'sigma'.
331  if not specified, min and max are presumed to be in 'absolute' units.
332 
333  *args, **kwargs : additional position and keyword arguments.
334  The options are shown below:
335 
336  **Q** : `float`, optional
337  The asinh softening parameter for asinh stretch.
338  Use Q=0 for linear stretch, increase Q to make brighter features visible.
339  When not specified or None, Q is calculated by Firefly to use full color range.
340  **gamma**
341  The gamma value for power law gamma stretch (default 2.0)
342  **zscale_contrast** : `int`, optional
343  Contrast parameter in percent for zscale algorithm (default 25)
344  **zscale_samples** : `int`, optional
345  Number of samples for zscale algorithm (default 600)
346  **zscale_samples_perline** : `int`, optional
347  Number of samples per line for zscale algorithm (default 120)
348  """
349  stretch_algorithms = ('linear', 'log', 'loglog', 'equal', 'squared', 'sqrt',
350  'asinh', 'powerlaw_gamma')
351  interval_methods = ('percent', 'maxmin', 'absolute', 'zscale', 'sigma')
352  #
353  #
354  # Normalise algorithm's case
355  #
356  if algorithm:
357  algorithm = dict((a.lower(), a) for a in stretch_algorithms).get(algorithm.lower(), algorithm)
358 
359  if algorithm not in stretch_algorithms:
360  raise FireflyError('Algorithm %s is invalid; please choose one of "%s"' %
361  (algorithm, '", "'.join(stretch_algorithms)))
362  self._stretchAlgorithm = algorithm
363  else:
364  algorithm = 'linear'
365 
366  # Translate parameters for asinh and powerlaw_gamma stretches
367  if 'Q' in kwargs:
368  kwargs['asinh_q_value'] = kwargs['Q']
369  del kwargs['Q']
370 
371  if 'gamma' in kwargs:
372  kwargs['gamma_value'] = kwargs['gamma']
373  del kwargs['gamma']
374 
375  if min == 'minmax':
376  interval_type = 'percent'
377  unit = 'percent'
378  min, max = 0, 100
379  elif min == 'zscale':
380  interval_type = 'zscale'
381  else:
382  interval_type = None
383 
384  if not unit:
385  unit = 'absolute'
386 
387  units = ('percent', 'absolute', 'sigma')
388  if unit not in units:
389  raise FireflyError('Unit %s is invalid; please choose one of "%s"' % (unit, '", "'.join(units)))
390 
391  if unit == 'sigma':
392  interval_type = 'sigma'
393  elif unit == 'absolute' and interval_type is None:
394  interval_type = 'absolute'
395  elif unit == 'percent':
396  interval_type = 'percent'
397 
398  self._stretchMin = min
399  self._stretchMax = max
400  self._stretchUnit = unit
401 
402  if interval_type not in interval_methods:
403  raise FireflyError('Interval method %s is invalid' % interval_type)
404 
405  rval = {}
406  if interval_type != 'zscale':
407  rval = _fireflyClient.set_stretch(str(self.display.frame), stype=interval_type,
408  algorithm=algorithm, lower_value=min,
409  upper_value=max, **kwargs)
410  else:
411  if 'zscale_contrast' not in kwargs:
412  kwargs['zscale_contrast'] = 25
413  if 'zscale_samples' not in kwargs:
414  kwargs['zscale_samples'] = 600
415  if 'zscale_samples_perline' not in kwargs:
416  kwargs['zscale_samples_perline'] = 120
417  rval = _fireflyClient.set_stretch(str(self.display.frame), stype='zscale',
418  algorithm=algorithm, **kwargs)
419 
420  if 'rv_string' in rval:
421  self._lastStretch = rval['rv_string']
422 
423  def _setMaskTransparency(self, transparency, maskName):
424  """Specify mask transparency (percent); or None to not set it when loading masks"""
425  if maskName is not None:
426  masklist = [maskName]
427  else:
428  masklist = set(self._maskIds + list(self.display._defaultMaskPlaneColor.keys()))
429  for k in masklist:
430  self._maskTransparencies[k] = transparency
431  _fireflyClient.dispatch(action_type='ImagePlotCntlr.overlayPlotChangeAttributes',
432  payload={'plotId': str(self.display.frame),
433  'imageOverlayId': k,
434  'attributes': {'opacity': 1.0 - transparency/100.},
435  'doReplot': False})
436 
437  def _getMaskTransparency(self, maskName):
438  """Return the current mask's transparency"""
439  transparency = None
440  if maskName in self._maskTransparencies:
441  transparency = self._maskTransparencies[maskName]
442  return transparency
443 
444  def _setMaskPlaneColor(self, maskName, color):
445  """Specify mask color """
446  _fireflyClient.remove_mask(plot_id=str(self.display.frame),
447  mask_id=maskName)
448  self._maskPlaneColors[maskName] = color
449  if (color.lower() != 'ignore'):
450  _fireflyClient.add_mask(bit_number=self._maskDict[maskName],
451  image_number=1,
452  plot_id=str(self.display.frame),
453  mask_id=maskName,
454  color=self.display.getMaskPlaneColor(maskName),
455  file_on_server=self._fireflyFitsID)
456 
457  def _show(self):
458  """Show the requested window"""
459  if self._client.render_tree_id is not None:
460  # we are using Jupyterlab
461  self._client.dispatch(self._client.ACTION_DICT['StartLabWindow'],
462  {})
463  else:
464  localbrowser, url = _fireflyClient.launch_browser(verbose=self.verbose)
465  if not localbrowser and not self.verbose:
466  _fireflyClient.display_url()
467 
468  #
469  # Zoom and Pan
470  #
471 
472  def _zoom(self, zoomfac):
473  """Zoom display by specified amount
474 
475  Parameters:
476  -----------
477  zoomfac: `float`
478  zoom level in screen pixels per image pixel
479  """
480  self._lastZoom = zoomfac
481  _fireflyClient.set_zoom(plot_id=str(self.display.frame), factor=zoomfac)
482 
483  def _pan(self, colc, rowc):
484  """Pan to specified pixel coordinates
485 
486  Parameters:
487  -----------
488  colc, rowc : `float`
489  column and row in units of pixels (zero-based convention,
490  with the xy0 already subtracted off)
491  """
492  self._lastPan = [colc+0.5, rowc+0.5] # saved for future use in _mtv
493  # Firefly's internal convention is first pixel is (0.5, 0.5)
494  _fireflyClient.set_pan(plot_id=str(self.display.frame), x=colc, y=rowc)
495 
496  # Extensions to the API that are specific to using the Firefly backend
497 
498  def getClient(self):
499  """Get the instance of FireflyClient for this display
500 
501  Returns:
502  --------
503  `firefly_client.FireflyClient`
504  Instance of FireflyClient used by this display
505  """
506  return self._client
507 
508  def clearViewer(self):
509  """Reinitialize the viewer
510  """
511  self._client.reinit_viewer()
512 
513  def resetLayout(self):
514  """Reset the layout of the Firefly Slate browser
515 
516  Clears the display and adds Slate cells to display image in upper left,
517  plot area in upper right, and plots stretch across the bottom
518  """
519  self.clearViewer()
520  self._client.add_cell(row=2, col=0, width=4, height=2, element_type='tables',
521  cell_id='tables')
522  self._client.add_cell(row=0, col=0, width=2, height=3, element_type='images',
523  cell_id='image-%s' % str(self.display.frame))
524  self._client.add_cell(row=0, col=2, width=2, height=3, element_type='xyPlots',
525  cell_id='plots')
526 
527  def overlayFootprints(self, catalog, color='rgba(74,144,226,0.60)',
528  highlightColor='cyan', selectColor='orange',
529  style='fill', layerString='detection footprints ',
530  titleString='catalog footprints '):
531  """Overlay outlines of footprints from a catalog
532 
533  Overlay outlines of LSST footprints from the input catalog. The colors
534  and style can be specified as parameters, and the base color and style
535  can be changed in the Firefly browser user interface.
536 
537  Parameters:
538  -----------
539  catalog : `lsst.afw.table.SourceCatalog`
540  Source catalog from which to display footprints.
541  color : `str`
542  Color for footprints overlay. Colors can be specified as a name
543  like 'cyan' or afwDisplay.RED; as an rgb value such as
544  'rgb(80,100,220)'; or as rgb plus alpha (transparency) such
545  as 'rgba('74,144,226,0.60)'.
546  highlightColor : `str`
547  Color for highlighted footprints
548  selectColor : `str`
549  Color for selected footprints
550  style : {'fill', 'outline'}
551  Style of footprints display, filled or outline
552  insertColumn : `int`
553  Column at which to insert the "family_id" and "category" columns
554  layerString: `str`
555  Name of footprints layer string, to concatenate with the frame
556  Re-using the layer_string will overwrite the previous table and
557  footprints
558  titleString: `str`
559  Title of catalog, to concatenate with the frame
560  """
561  footprintTable = createFootprintsTable(catalog)
562  with BytesIO() as fd:
563  footprintTable.to_xml(fd)
564  tableval = self._client.upload_data(fd, 'UNKNOWN')
565  self._client.overlay_footprints(footprint_file=tableval,
566  title=titleString + str(self.display.frame),
567  footprint_layer_id=layerString + str(self.display.frame),
568  plot_id=str(self.display.frame),
569  color=color,
570  highlightColor=highlightColor,
571  selectColor=selectColor,
572  style=style)
def _setMaskTransparency(self, transparency, maskName)
Definition: firefly.py:423
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:832
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:231
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:530
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:833