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