diff --git a/app.py b/app.py index bf250c20..7dcf249c 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,7 @@ import dash import dash_bootstrap_components as dbc from dash.long_callback import DiskcacheLongCallbackManager +from dash import DiskcacheManager # import jpype @@ -24,6 +25,7 @@ cache = diskcache.Cache("./cache") long_callback_manager = DiskcacheLongCallbackManager(cache) +background_callback_manager = DiskcacheManager(cache) args = yaml.load(open('config.yml'), yaml.Loader) @@ -56,7 +58,8 @@ "name": "viewport", "content": "width=device-width, initial-scale=1" }], - long_callback_manager=long_callback_manager + long_callback_manager=long_callback_manager, + background_callback_manager=background_callback_manager, ) diff --git a/apps/cards.py b/apps/cards.py index 06784baa..3b04a325 100644 --- a/apps/cards.py +++ b/apps/cards.py @@ -33,6 +33,43 @@ import numpy as np import urllib +lc_help = r""" +##### Difference magnitude + +Circles (●) with error bars show valid alerts that pass the Fink quality cuts. +In addition, the _Difference magnitude_ view shows: +- upper triangles with errors (▲), representing alert measurements that do not satisfy Fink quality cuts, but are nevetheless contained in the history of valid alerts and used by classifiers. +- lower triangles (▽), representing 5-sigma magnitude limit in difference image based on PSF-fit photometry contained in the history of valid alerts. + +If the `Color` switch is turned on, the view also shows the panel with `g - r` color, estimated by combining nearby (closer than 0.3 days) measurements in two filters. + +##### DC magnitude +DC magnitude is computed by combining the nearest reference image catalog magnitude (`magnr`), +differential magnitude (`magpsf`), and `isdiffpos` (positive or negative difference image detection) as follows: +$$ +m_{DC} = -2.5\log_{10}(10^{-0.4m_{magnr}} + \texttt{sign} 10^{-0.4m_{magpsf}}) +$$ + +where `sign` = 1 if `isdiffpos` = 't' or `sign` = -1 if `isdiffpos` = 'f'. +Before using the nearest reference image source magnitude (`magnr`), you will need +to ensure the source is close enough to be considered an association +(e.g., `distnr` $\leq$ 1.5 arcsec). It is also advised you check the other associated metrics +(`chinr` and/or `sharpnr`) to ensure it is a point source. ZTF recommends +0.5 $\leq$ `chinr` $\leq$ 1.5 and/or -0.5 $\leq$ `sharpnr` $\leq$ 0.5. + +The view also shows, with dashed horizontal lines, the levels corresponding to the magnitudes of the nearest reference image catalog entry (`magnr`) used in computing DC magnitudes. + +This view may be augmented with the photometric points from [ZTF Data Releases](https://www.ztf.caltech.edu/ztf-public-releases.html) by clicking `Get DR photometry` button. The points will be shown with semi-transparent dots (•). + +##### DC flux +DC flux (in Jansky) is constructed from DC magnitude by using the following: +$$ +f_{DC} = 3631 \times 10^{-0.4m_{DC}} +$$ + +Note that we display the flux in milli-Jansky. +""" + def card_lightcurve_summary(): """ Add a card containing the lightcurve @@ -41,50 +78,18 @@ def card_lightcurve_summary(): card: dbc.Card Card with the cutouts drawn inside """ - - lc_help = dcc.Markdown( - """ - ##### Difference magnitude - - Circles (●) with error bars show valid alerts that pass the Fink quality cuts. - In addition, the _Difference magnitude_ view shows: - - upper triangles with errors (▲), representing alert measurements that do not satisfy Fink quality cuts, but are nevetheless contained in the history of valid alerts and used by classifiers. - - lower triangles (▽), representing 5-sigma magnitude limit in difference image based on PSF-fit photometry contained in the history of valid alerts. - - ##### DC magnitude - DC magnitude is computed by combining the nearest reference image catalog magnitude (`magnr`), - differential magnitude (`magpsf`), and `isdiffpos` (positive or negative difference image detection) as follows: - $$ - m_{DC} = -2.5\\log_{10}(10^{-0.4m_{magnr}} + \\texttt{sign} 10^{-0.4m_{magpsf}}) - $$ - - where `sign` = 1 if `isdiffpos` = 't' or `sign` = -1 if `isdiffpos` = 'f'. - Before using the nearest reference image source magnitude (`magnr`), you will need - to ensure the source is close enough to be considered an association - (e.g., `distnr` $\\leq$ 1.5 arcsec). It is also advised you check the other associated metrics - (`chinr` and/or `sharpnr`) to ensure it is a point source. ZTF recommends - 0.5 $\\leq$ `chinr` $\\leq$ 1.5 and/or -0.5 $\\leq$ `sharpnr` $\\leq$ 0.5. - - ##### DC flux - DC flux (in Jansky) is constructed from DC magnitude by using the following: - $$ - f_{DC} = 3631 \\times 10^{-0.4m_{DC}} - $$ - - Note that we display the flux in milli-Jansky. - """, mathjax=True - ) - card = dmc.Paper( [ - dcc.Graph( - id='lightcurve_cutouts', - style={ - 'width': '100%', - 'height': '30pc' - }, - config={'displayModeBar': False}, - className="mb-2" + loading( + dcc.Graph( + id='lightcurve_cutouts', + style={ + 'width': '100%', + 'height': '30pc' + }, + config={'displayModeBar': False}, + className="mb-2" + ) ), dbc.Row( dbc.Col( @@ -101,7 +106,41 @@ def card_lightcurve_summary(): ) ) ), - help_popover(lc_help, 'help_lc'), + dmc.Group( + [ + dmc.Switch( + "Color", + id='lightcurve_show_color', + color='gray', + radius='xl', + size='sm', + persistence=True + ), + dmc.Button( + "Get DR photometry", + id='lightcurve_request_release', + variant="outline", + color='gray', + radius='xl', size='xs', + compact=False, + ), + help_popover( + dcc.Markdown( + lc_help, mathjax=True + ), + 'help_lc', + trigger=dmc.ActionIcon( + DashIconify(icon="mdi:help"), + id='help_lc', + color='gray', + variant="outline", + radius='xl', + size='md', + ) + ), + ], + position='center', align='center' + ) ], radius='xl', p='md', shadow='xl', withBorder=True ) return card diff --git a/apps/plotting.py b/apps/plotting.py index 81a7d413..31345ce8 100644 --- a/apps/plotting.py +++ b/apps/plotting.py @@ -17,6 +17,7 @@ import numpy as np from gatspy import periodic from scipy.optimize import curve_fit +from copy import deepcopy import datetime import copy @@ -33,8 +34,9 @@ from dash_iconify import DashIconify from dash.exceptions import PreventUpdate -from apps.utils import convert_jd, readstamp, _data_stretch, convolve +from apps.utils import convert_jd, readstamp, _data_stretch, convolve, extract_color from fink_utils.photometry.conversion import apparent_flux, dc_mag +from fink_utils.photometry.utils import is_source_behind from apps.utils import sine_fit from apps.utils import class_colors from apps.statistics import dic_names @@ -78,6 +80,7 @@ } layout_lightcurve = dict( + autosize=True, automargin=True, margin=dict(l=50, r=30, b=0, t=0), hovermode="closest", @@ -94,12 +97,14 @@ ), xaxis={ 'title': 'Observation date', - 'automargin': True + 'automargin': True, + 'zeroline': False }, yaxis={ 'autorange': 'reversed', 'title': 'Magnitude', - 'automargin': True + 'automargin': True, + 'zeroline': False } ) @@ -690,9 +695,11 @@ def plot_classbar(object_data): Input('url', 'pathname'), Input('object-data', 'children'), Input('object-upper', 'children'), - Input('object-uppervalid', 'children') + Input('object-uppervalid', 'children'), + Input('object-release', 'children'), + Input('lightcurve_show_color', 'checked') ]) -def draw_lightcurve(switch: int, pathname: str, object_data, object_upper, object_uppervalid) -> dict: +def draw_lightcurve(switch: int, pathname: str, object_data, object_upper, object_uppervalid, object_release, show_color) -> dict: """ Draw object lightcurve with errorbars Parameters @@ -709,39 +716,70 @@ def draw_lightcurve(switch: int, pathname: str, object_data, object_upper, objec ---------- figure: dict """ + # Primary high-quality data points pdf_ = pd.read_json(object_data) cols = [ - 'i:jd', 'i:magpsf', 'i:sigmapsf', 'i:fid', + 'i:jd', 'i:magpsf', 'i:sigmapsf', 'i:fid', 'i:distnr', 'i:magnr', 'i:sigmagnr', 'i:magzpsci', 'i:isdiffpos', 'i:candid' ] pdf = pdf_.loc[:, cols] + # Upper limits + pdf_upper = pd.read_json(object_upper) + + # Lower-quality data points + pdf_upperv = pd.read_json(object_uppervalid) + # type conversion dates = pdf['i:jd'].apply(lambda x: convert_jd(float(x), to='iso')) + dates_upper = pdf_upper['i:jd'].apply(lambda x: convert_jd(float(x), to='iso')) + dates_upperv = pdf_upperv['i:jd'].apply(lambda x: convert_jd(float(x), to='iso')) + + if object_release: + # Data release photometry + pdf_release = pd.read_json(object_release) + dates_release = pdf_release['mjd'].apply(lambda x: convert_jd(float(x)+2400000.5, to='iso')) + else: + pdf_release = pd.DataFrame() + + # Exclude lower-quality points overlapping higher-quality ones + mask = np.in1d(pdf_upperv['i:jd'].values, pdf['i:jd'].values) + pdf_upperv,dates_upperv = [_[~mask] for _ in (pdf_upperv,dates_upperv)] # shortcuts mag = pdf['i:magpsf'] err = pdf['i:sigmapsf'] + # Should we correct DC magnitudes for the nearby source?.. + is_dc_corrected = is_source_behind(pdf['i:distnr'].values[0]) + + # We should never modify global variables!!! + layout = deepcopy(layout_lightcurve) + if switch == "Difference magnitude": - layout_lightcurve['yaxis']['title'] = 'Difference magnitude' - layout_lightcurve['yaxis']['autorange'] = 'reversed' + layout['yaxis']['title'] = 'Difference magnitude' + layout['yaxis']['autorange'] = 'reversed' scale = 1.0 elif switch == "DC magnitude": - # inplace replacement - mag, err = np.transpose( - [ - dc_mag(*args) for args in zip( - mag.astype(float).values, - err.astype(float).values, - pdf['i:magnr'].astype(float).values, - pdf['i:sigmagnr'].astype(float).values, - pdf['i:isdiffpos'].values - ) - ] - ) - layout_lightcurve['yaxis']['title'] = 'Apparent DC magnitude' - layout_lightcurve['yaxis']['autorange'] = 'reversed' + if is_dc_corrected: + # inplace replacement for DC corrected flux + mag, err = np.transpose( + [ + dc_mag(*args) for args in zip( + mag.astype(float).values, + err.astype(float).values, + pdf['i:magnr'].astype(float).values, + pdf['i:sigmagnr'].astype(float).values, + pdf['i:isdiffpos'].values + ) + ] + ) + # Keep only "good" measurements + idx = err < 1 + pdf, dates, mag, err = [_[idx] for _ in [pdf, dates, mag, err]] + + layout['yaxis']['title'] = 'Apparent DC magnitude' + layout['yaxis']['autorange'] = 'reversed' scale = 1.0 elif switch == "DC flux": # inplace replacement @@ -750,178 +788,267 @@ def draw_lightcurve(switch: int, pathname: str, object_data, object_upper, objec apparent_flux(*args) for args in zip( mag.astype(float).values, err.astype(float).values, - pdf['i:magnr'].astype(float).values, + pdf['i:magnr'].astype(float).values if is_dc_corrected else [99.0]*len(pdf.index), pdf['i:sigmagnr'].astype(float).values, pdf['i:isdiffpos'].values ) ] ) - layout_lightcurve['yaxis']['title'] = 'Apparent DC flux (milliJansky)' - layout_lightcurve['yaxis']['autorange'] = True + + layout['yaxis']['title'] = 'Apparent DC flux (milliJansky)' + layout['yaxis']['autorange'] = True scale = 1e3 - hovertemplate = r""" - %{yaxis.title.text}: %{customdata[1]}%{y:.2f} ± %{error_y.array:.2f}
- %{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
- mjd: %{customdata[0]} - - """ + layout['shapes'] = [] + figure = { - 'data': [ + 'data': [], + "layout": layout + } + + for fid,fname,color in ( + (1, "g", COLORS_ZTF[0]), + (2, "r", COLORS_ZTF[1]) + ): + # High-quality measurements + hovertemplate = r""" + %{yaxis.title.text}: %{customdata[1]}%{y:.2f} ± %{error_y.array:.2f}
+ %{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
+ mjd: %{customdata[0]} + + """ + idx = pdf['i:fid'] == fid + figure['data'].append( { - 'x': dates[pdf['i:fid'] == 1], - 'y': mag[pdf['i:fid'] == 1] * scale, + 'x': dates[idx], + 'y': mag[idx] * scale, 'error_y': { 'type': 'data', - 'array': err[pdf['i:fid'] == 1] * scale, + 'array': err[idx] * scale, 'visible': True, - 'color': COLORS_ZTF[0] + 'width': 0, + 'opacity': 0.5, + 'color': color }, 'mode': 'markers', - 'name': 'g band', + 'name': '{} band'.format(fname), 'customdata': np.stack( ( - pdf['i:jd'].apply(lambda x: x - 2400000.5)[pdf['i:fid'] == 1], - pdf['i:isdiffpos'].apply(lambda x: '(-) ' if x == 'f' else '')[pdf['i:fid'] == 1], - ), axis=-1 - ), - 'hovertemplate': hovertemplate, - "legendgroup": "g band", - 'legendrank': 110, - 'marker': { - 'size': 12, - 'color': COLORS_ZTF[0], - 'symbol': 'o'} - }, - { - 'x': dates[pdf['i:fid'] == 2], - 'y': mag[pdf['i:fid'] == 2] * scale, - 'error_y': { - 'type': 'data', - 'array': err[pdf['i:fid'] == 2] * scale, - 'visible': True, - 'color': COLORS_ZTF[1] - }, - 'mode': 'markers', - 'name': 'r band', - 'legendrank': 120, - 'customdata': np.stack( - ( - pdf['i:jd'].apply(lambda x: x - 2400000.5)[pdf['i:fid'] == 2], - pdf['i:isdiffpos'].apply(lambda x: '(-) ' if x == 'f' else '')[pdf['i:fid'] == 2], + pdf['i:jd'].apply(lambda x: x - 2400000.5)[idx], + pdf['i:isdiffpos'].apply(lambda x: '(-) ' if x == 'f' else '')[idx], ), axis=-1 ), 'hovertemplate': hovertemplate, - "legendgroup": "r band", + "legendgroup": "{} band".format(fname), + 'legendrank': 100 + 10*fid, 'marker': { 'size': 12, - 'color': COLORS_ZTF[1], + 'color': color, 'symbol': 'o'} } - ], - "layout": layout_lightcurve - } + ) - if switch == "Difference magnitude": - pdf_upper = pd.read_json(object_upper) - # candid: %{customdata[0]}
not available in index tables... - hovertemplate_upper = r""" - Upper limit: %{y:.2f}
+ if switch == "Difference magnitude": + # Upper limits + if not pdf_upper.empty: + # candid: %{customdata[0]}
not available in index tables... + hovertemplate_upper = r""" + Upper limit: %{y:.2f}
+ %{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
+ mjd: %{customdata} + + """ + idx = pdf_upper['i:fid'] == fid + figure['data'].append( + { + 'x': dates_upper[idx], + 'y': pdf_upper['i:diffmaglim'][idx], + 'mode': 'markers', + 'name': '', + 'customdata': pdf_upper['i:jd'].apply(lambda x: x - 2400000.5)[idx], + 'hovertemplate': hovertemplate_upper, + "legendgroup": "{} band upper".format(fname), + 'legendrank': 101 + 10*fid, + 'marker': { + 'color': color, + 'symbol': 'triangle-down-open' + }, + # 'showlegend': False + } + ) + + # Lower-quality data points + if not pdf_upperv.empty: + # candid: %{customdata[0]}
not available in index tables... + hovertemplate_upperv = r""" + %{yaxis.title.text} (low quality): %{y:.2f} ± %{error_y.array:.2f}
+ %{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
+ mjd: %{customdata} + + """ + idx = pdf_upperv['i:fid'] == fid + figure['data'].append( + { + 'x': dates_upperv[idx], + 'y': pdf_upperv['i:magpsf'][idx], + 'error_y': { + 'type': 'data', + 'array': pdf_upperv['i:sigmapsf'][idx], + 'visible': True, + 'width': 0, + 'opacity': 0.5, + 'color': color + }, + 'mode': 'markers', + 'customdata': pdf_upperv['i:jd'].apply(lambda x: x - 2400000.5)[idx], + 'hovertemplate': hovertemplate_upperv, + "legendgroup": "{} band".format(fname), + 'marker': { + 'color': color, + 'symbol': 'triangle-up' + }, + 'showlegend': False + } + ) + + elif switch == "DC magnitude": + if is_dc_corrected: + # Overplot the levels of nearby source magnitudes + idx = pdf['i:fid'] == fid + if np.sum(idx): + ref = np.mean(pdf['i:magnr'][idx]) + + figure['layout']['shapes'].append( + { + 'type': 'line', + 'yref': 'y', + 'y0': ref, 'y1': ref, # adding a horizontal line + 'xref': 'paper', 'x0': 0, 'x1': 1, + 'line': {'color': color, 'dash': 'dash', 'width': 1}, + 'legendgroup': '{} band'.format(fname), + 'opacity': 0.3, + } + ) + + # Data release photometry + if not pdf_release.empty: + hovertemplate_release = r""" + Data release magnitude: %{y:.2f} ± %{error_y.array:.2f}
+ %{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
+ mjd: %{customdata} + + """ + idx = pdf_release['filtercode'] == 'z' + fname + figure['data'].append( + { + 'x': dates_release[idx], + 'y': pdf_release['mag'][idx], + 'error_y': { + 'type': 'data', + 'array': pdf_release['magerr'][idx], + 'visible': True, + 'width': 0, + 'opacity': 0.5, + 'color': color + }, + 'mode': 'markers', + 'name': '', + 'customdata': pdf_release['mjd'][idx], + 'hovertemplate': hovertemplate_release, + "legendgroup": "{} band release".format(fname), + 'legendrank': 102 + 10*fid, + 'marker': { + 'color': color, + 'symbol': '.' + }, + 'opacity': 0.5, + # 'showlegend': False + } + ) + + if show_color: + hovertemplate_gr = r""" + g - r: %{y:.2f} ± %{error_y.array:.2f}
%{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
mjd: %{customdata} """ - if not pdf_upper.empty: - dates2 = pdf_upper['i:jd'].apply(lambda x: convert_jd(float(x), to='iso')) - figure['data'].append( - { - 'x': dates2[pdf_upper['i:fid'] == 1], - 'y': pdf_upper['i:diffmaglim'][pdf_upper['i:fid'] == 1], - 'mode': 'markers', - 'name': '', - 'customdata': pdf_upper['i:jd'].apply(lambda x: x - 2400000.5)[pdf_upper['i:fid'] == 1], - 'hovertemplate': hovertemplate_upper, - "legendgroup": "g band upper", - 'legendrank': 111, - 'marker': { - 'color': COLORS_ZTF[0], - 'symbol': 'triangle-down-open' - }, - # 'showlegend': False - } - ) - figure['data'].append( - { - 'x': dates2[pdf_upper['i:fid'] == 2], - 'y': pdf_upper['i:diffmaglim'][pdf_upper['i:fid'] == 2], - 'mode': 'markers', - 'name': '', - 'customdata': pdf_upper['i:jd'].apply(lambda x: x - 2400000.5)[pdf_upper['i:fid'] == 2], - 'hovertemplate': hovertemplate_upper, - "legendgroup": "r band upper", - 'legendrank': 121, - 'marker': { - 'color': COLORS_ZTF[1], - 'symbol': 'triangle-down-open' - }, - # 'showlegend': False - } + + pdf_ = None + + if switch == "Difference magnitude": + pdf_ = pd.concat([pdf, pdf_upperv]) + elif switch == "DC magnitude": + pdf_ = pd.DataFrame( + {'i:jd': pdf['i:jd'], 'i:fid': pdf['i:fid'], 'i:magpsf': mag, 'i:sigmapsf': err} ) - pdf_upperv = pd.read_json(object_uppervalid) - # candid: %{customdata[0]}
not available in index tables... - hovertemplate_upperv = r""" - %{yaxis.title.text} (low quality): %{y:.2f} ± %{error_y.array:.2f}
- %{xaxis.title.text}: %{x|%Y/%m/%d %H:%M:%S.%L}
- mjd: %{customdata} - - """ - if not pdf_upperv.empty: - dates2 = pdf_upperv['i:jd'].apply(lambda x: convert_jd(float(x), to='iso')) - mask = np.array([False if i in pdf['i:jd'].values else True for i in pdf_upperv['i:jd'].values]) - dates2 = dates2[mask] - pdf_upperv = pdf_upperv[mask] + + if not pdf_release.empty: + pdf_ = pd.concat( + [ + pdf_, + pd.DataFrame( + { + 'i:jd': pdf_release['mjd'] + 2400000.5, + 'i:fid': pdf_release['filtercode'].map({'zg': 1, 'zr': 2}), + 'i:magpsf': pdf_release['mag'], + 'i:sigmapsf': pdf_release['magerr'], + } + ), + ] + ) + + if pdf_ is not None: + pdf_gr = extract_color(pdf_) + dates_gr = pdf_gr['i:jd'].apply(lambda x: convert_jd(float(x), to='iso')) + color = '#3C8DFF' + figure['data'].append( { - 'x': dates2[pdf_upperv['i:fid'] == 1], - 'y': pdf_upperv['i:magpsf'][pdf_upperv['i:fid'] == 1], + 'x': dates_gr, + 'y': pdf_gr['v:g-r'], 'error_y': { 'type': 'data', - 'array': pdf_upperv['i:sigmapsf'][pdf_upperv['i:fid'] == 1], + 'array': pdf_gr['v:sigma_g-r'], 'visible': True, - 'color': COLORS_ZTF[0] + 'width': 0, + 'opacity': 0.5, + 'color': color }, 'mode': 'markers', - 'customdata': pdf_upperv['i:jd'].apply(lambda x: x - 2400000.5)[pdf_upperv['i:fid'] == 1], - 'hovertemplate': hovertemplate_upperv, - "legendgroup": "g band", + 'name': 'g - r', + 'customdata': pdf_gr['i:jd'].apply(lambda x: x - 2400000.5), + 'hovertemplate': hovertemplate_gr, + 'legendgroup': 'g-r', 'marker': { - 'color': COLORS_ZTF[0], - 'symbol': 'triangle-up' + 'color': color, + 'symbol': 'o', }, - 'showlegend': False + 'showlegend': True, + 'xaxis': 'x', + 'yaxis': 'y2', } ) - figure['data'].append( + + figure['layout']['yaxis2'] = { + 'automargin': True, + 'title': 'g-r', + 'domain': [0.0, 0.3], + 'zeroline': False, + } + figure['layout']['yaxis']['domain'] = [0.35, 1.0] + figure['layout']['xaxis']['anchor'] = 'free' + + figure['layout']['shapes'].append( { - 'x': dates2[pdf_upperv['i:fid'] == 2], - 'y': pdf_upperv['i:magpsf'][pdf_upperv['i:fid'] == 2], - 'error_y': { - 'type': 'data', - 'array': pdf_upperv['i:sigmapsf'][pdf_upperv['i:fid'] == 2], - 'visible': True, - 'color': COLORS_ZTF[1] - }, - 'mode': 'markers', - 'customdata': pdf_upperv['i:jd'].apply(lambda x: x - 2400000.5)[pdf_upperv['i:fid'] == 2], - 'hovertemplate': hovertemplate_upperv, - "legendgroup": "r band", - 'marker': { - 'color': COLORS_ZTF[1], - 'symbol': 'triangle-up' - }, - 'showlegend': False + 'type': 'line', + 'yref': 'paper', 'y0':0.325, 'y1': 0.325, + 'xref': 'paper', 'x0': -0.1, 'x1': 1.05, + 'line': {'color': 'gray', 'width': 4}, + 'opacity': 0.1, } ) + return figure @app.callback( diff --git a/apps/summary.py b/apps/summary.py index 1269389d..29cc40ce 100644 --- a/apps/summary.py +++ b/apps/summary.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import dash -from dash import html, dcc, Input, Output, State +from dash import html, dcc, Input, Output, State, no_update from dash.exceptions import PreventUpdate import dash_bootstrap_components as dbc @@ -60,9 +60,21 @@ dcc.Location(id='url', refresh=False) -def tab1_content(pdf, extra_div): +def tab1_content(pdf): """ Summary tab """ + + distnr = pdf['i:distnr'].values[0] + if is_source_behind(distnr): + extra_div = dbc.Alert( + "It looks like there is a source behind, at {:.1f} arcsec. You might want to check the DC magnitude, and get DR photometry to see its long-term behaviour.".format(distnr), + dismissable=True, + is_open=True, + color="light" + ) + else: + extra_div = html.Div() + tab1_content_ = html.Div([ dmc.Space(h=10), dbc.Row( @@ -80,10 +92,21 @@ def tab1_content(pdf, extra_div): ), ], justify='around' ), - dbc.Row([ - dbc.Col([extra_div, loading(card_lightcurve_summary())], md=8), - dbc.Col(card_id(pdf), md=4) - ], className='g-1'), + dbc.Row( + [ + dbc.Col( + [ + extra_div, + card_lightcurve_summary() + ], + md=8 + ), + dbc.Col( + card_id(pdf), + md=4 + ) + ], className='g-1' + ), ]) out = tab1_content_ @@ -408,30 +431,20 @@ def tab6_content(object_tracklet): return tab6_content_ def tabs(pdf): - distnr = pdf['i:distnr'].values[0] - if is_source_behind(distnr): - extra_div = dbc.Alert( - "It looks like there is a source behind. You might want to check the DC magnitude instead.", - dismissable=True, - is_open=True, - color="light" - ) - else: - extra_div = html.Div() tabs_ = dmc.Tabs( [ dmc.TabsList( [ dmc.Tab("Summary", value="Summary"), - dmc.Tab("Supernovae", value="Supernovae"), - dmc.Tab("Variable stars", value="Variable stars"), - dmc.Tab("Microlensing", value="Microlensing"), + dmc.Tab("Supernovae", value="Supernovae", disabled=len(pdf.index) == 1), + dmc.Tab("Variable stars", value="Variable stars", disabled=len(pdf.index) == 1), + dmc.Tab("Microlensing", value="Microlensing", disabled=len(pdf.index) == 1), dmc.Tab("Solar System", value="Solar System", disabled=not is_sso(pdf)), dmc.Tab("Tracklets", value="Tracklets", disabled=not is_tracklet(pdf)), dmc.Tab("GRB", value="GRB", disabled=True) ], position='right' ), - dmc.TabsPanel(tab1_content(pdf, extra_div), value="Summary"), + dmc.TabsPanel(tab1_content(pdf), value="Summary"), dmc.TabsPanel(tab2_content(), value="Supernovae"), dmc.TabsPanel(tab3_content(), value="Variable stars"), dmc.TabsPanel(tab4_content(), value="Microlensing"), @@ -567,8 +580,54 @@ def store_query(name): pdftracklet = pd.read_json(r.content) else: pdftracklet = pd.DataFrame() + return pdfs.to_json(), pdfsU.to_json(), pdfsUV.to_json(), pdfsso.to_json(), pdftracklet.to_json() +@app.callback( + [ + Output('object-release', 'children'), + Output('lightcurve_request_release', 'children'), + Output('switch-mag-flux', 'value'), + ], + Input('lightcurve_request_release', 'n_clicks'), + State('object-data', 'children'), + prevent_initial_call=True, + background=True, + running=[ + (Output('lightcurve_request_release', 'disabled'), True, True), + (Output('lightcurve_request_release', 'loading'), True, False), + ], +) +def store_release_photometry(n_clicks, object_data): + if not n_clicks or not object_data: + raise PreventUpdate + + pdf = pd.read_json(object_data) + + mean_ra = np.mean(pdf['i:ra']) + mean_dec = np.mean(pdf['i:dec']) + + try: + pdf_release = pd.read_csv( + 'https://irsa.ipac.caltech.edu/cgi-bin/ZTF/nph_light_curves?POS=CIRCLE%20{}%20{}%20{}&BAD_CATFLAGS_MASK=32768&FORMAT=CSV'.format( + mean_ra, + mean_dec, + 2.0/3600 + ) + ) + + if not pdf_release.empty: + pdf_release = pdf_release[['mjd', 'mag', 'magerr', 'filtercode']] + + return pdf_release.to_json(), "DR photometry: {} points".format(len(pdf_release.index)), 'DC magnitude' + + except: + import traceback + traceback.print_exc() + pass + + return no_update, "No DR photometry", no_update + @app.callback( Output('qrcode', 'children'), [ @@ -640,6 +699,7 @@ def layout(name): html.Div(id='object-uppervalid', style={'display': 'none'}), html.Div(id='object-sso', style={'display': 'none'}), html.Div(id='object-tracklet', style={'display': 'none'}), + html.Div(id='object-release', style={'display': 'none'}), ], className='bg-opaque-90' ) diff --git a/apps/utils.py b/apps/utils.py index 62822912..c4669603 100644 --- a/apps/utils.py +++ b/apps/utils.py @@ -560,6 +560,50 @@ def extract_delta_color(pdf: pd.DataFrame, filter_: int): return vec, rate +def extract_color(pdf: pd.DataFrame, tolerance: float = 0.3, colnames=None): + """ Extract g-r values for single object a pandas DataFrame + + Parameters + ---------- + pdf: pandas DataFrame + DataFrame containing alert parameters from an API call + tolerance: float + Maximal distance in days between data points to be associated + colnames: list + List of extra column names to keep in the output. + + Returns + ---------- + pdf_gr: pandas DataFrame + DataFrame containing the time and magnitudes of matched points, + along with g-r color and its error + """ + if colnames is None: + colnames = ['i:jd', 'i:magpsf', 'i:sigmapsf'] + else: + colnames = ['i:jd', 'i:magpsf', 'i:sigmapsf'] + colnames + colnames = list(np.unique(colnames)) + + pdf_g = pdf[pdf['i:fid'] == 1][colnames] + pdf_r = pdf[pdf['i:fid'] == 2][colnames] + + # merge_asof expects sorted data + pdf_g = pdf_g.sort_values('i:jd') + pdf_r = pdf_r.sort_values('i:jd') + + # As merge_asof does not keep the second jd column - let's make it manually + pdf_g['v:mjd'] = pdf_g['i:jd'] - 2400000.5 + pdf_r['v:mjd'] = pdf_r['i:jd'] - 2400000.5 + + pdf_gr = pd.merge_asof(pdf_g, pdf_r, on='i:jd', suffixes=('_g', '_r'), direction='nearest', tolerance=0.3) + pdf_gr = pdf_gr[~pdf_gr.isna()['i:magpsf_r']] # Keep only matched rows + + pdf_gr['v:g-r'] = pdf_gr['i:magpsf_g'] - pdf_gr['i:magpsf_r'] + pdf_gr['v:sigma_g-r'] = np.hypot(pdf_gr['i:sigmapsf_g'], pdf_gr['i:sigmapsf_r']) + pdf_gr['v:delta_jd'] = pdf_gr['v:mjd_g'] - pdf_gr['v:mjd_r'] + + return pdf_gr + def queryMPC(number, kind='asteroid'): """Query MPC for information about object 'designation'.