EVN Observation Planner. Helps you to plan a VLBI observation. Given a date, source coordinates, and a VLBI array, it will tell you when the source can be observed by each antenna, the reached rms noise level and resolution, among other details.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1480 lines
77 KiB

  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """EVN Observation Planner.
  4. Program to compute the source elevation visibility
  5. and expected thermal noise level for a given EVN observation.
  6. """
  7. __author__ = "Benito Marcote"
  8. __credits__ = "Benito Marcote"
  9. __license__ = "LGPLv3+"
  10. __date__ = "2020/10/26"
  11. __version__ = "1.0.1"
  12. __maintainer__ = "Benito Marcote"
  13. __email__ = "marcote@jive.eu"
  14. __status__ = "Production" # Prototype, Development, Production.
  15. import os
  16. from os import path
  17. from time import sleep
  18. import itertools
  19. import functools
  20. from importlib import resources
  21. import multiprocessing as mp
  22. import datetime
  23. # import time
  24. from datetime import datetime as dt
  25. import numpy as np
  26. import dash
  27. from dash.dependencies import Input, Output, State
  28. import dash_core_components as dcc
  29. import dash_html_components as html
  30. import dash_bootstrap_components as dbc
  31. import plotly.express as px
  32. from plotly.subplots import make_subplots
  33. import plotly.graph_objects as go
  34. from astropy.time import Time
  35. from astropy import coordinates as coord
  36. from astropy import units as u
  37. ## THIS WILL NEED TO GO AWAY IN THE NEW VERSION OF ASTROPY, WHICH IS STILL NOT
  38. ## SUPPORTED BY THE CURRENT VERSION OF ASTROPLAN
  39. # Tweak to not let astroplan crashing...
  40. # from astropy.utils.data import clear_download_cache
  41. # from astropy.utils import iers
  42. # clear_download_cache() # to be sure it is really working
  43. #
  44. # iers.conf.auto_download = False
  45. # iers.conf.iers_auto_url = None
  46. # iers.conf.auto_max_age = None
  47. # iers.conf.remote_timeout = 100.0
  48. # iers.conf.download_cache_lock_attempts = 10
  49. from astroplan import FixedTarget
  50. current_directory = path.dirname(path.realpath(__file__))
  51. if path.isfile(current_directory + '/.astropy/cache/download/py3/lock'):
  52. os.remove(current_directory + '/.astropy/cache/download/py3/lock')
  53. ######### All the previous part will be removed with astropy 4.1+ and astroplan 0.7+
  54. from vlbiplanobs import freqsetups as fs
  55. from vlbiplanobs import stations
  56. from vlbiplanobs import observation
  57. from vlbiplanobs import graphical_elements as ge
  58. # adding the possibility of disabled. Will be implemented in a future version of dash_bootstrap_components
  59. from vlbiplanobs.Checkbox import Checkbox
  60. all_antennas = stations.Stations.get_stations_from_configfile()
  61. default_arrays = stations.Stations.get_network_names_from_configfile()
  62. sorted_networks = {'EVN': 'EVN: European VLBI Network', 'eMERLIN': 'eMERLIN (out-stations)',
  63. 'VLBA': 'VLBA: Very Long Baseline Array',
  64. 'LBA': 'LBA: Australian Long Baseline Array',
  65. 'KVN': 'KVN: Korean VLBI Network',
  66. 'VERA': 'VERA: VLBI Exploration of Radio Astrometry',
  67. 'Other': 'Other antennas',
  68. 'Decom': 'Decommissioned antennas'}
  69. # Safety check that all these antennas are available in the file
  70. for a_array in default_arrays:
  71. for a_station in default_arrays[a_array]['default_antennas']:
  72. assert a_station in all_antennas.codenames, \
  73. f"{a_station} is not recognized from the station_catalog.inp file"
  74. doc_files = {'About the EVN Observation Planner': 'doc-contact.md',
  75. 'About the antennas': 'doc-antennas.md',
  76. 'Technical background': 'doc-estimations.md'}
  77. selected_band = '6cm'
  78. obs = observation.Observation()
  79. external_stylesheets = []
  80. external_scripts = ["https://kit.fontawesome.com/69c65a0ab5.js"]
  81. app = dash.Dash(__name__, title='EVN Observation Planner', external_scripts=external_scripts,
  82. assets_folder=current_directory+'/assets/')
  83. app.config.suppress_callback_exceptions = True # Avoids error messages for id's that haven't been loaded yet
  84. server = app.server
  85. def smap(f):
  86. return f()
  87. def get_doc_text():
  88. """Reads the doc files and returns it as a Div object.
  89. """
  90. temp = []
  91. for i,a_topic in enumerate(doc_files):
  92. with resources.open_text("doc", doc_files[a_topic]) as f:
  93. # Some files will have references to images/files in the form '{src:filename}'
  94. # We parse this
  95. parsed_text = f.read()
  96. while '{src:' in parsed_text:
  97. i0 = parsed_text.index('{src:')
  98. i1 = i0 + parsed_text[i0:].index('}')
  99. filename = parsed_text[i0+5:i1]
  100. parsed_text = parsed_text.replace( parsed_text[i0:i1+1],
  101. app.get_asset_url(filename) )
  102. if a_topic == 'About the antennas':
  103. temp += [ge.create_accordion_card(a_topic,
  104. [dcc.Markdown(parsed_text), ge.antenna_cards(app, all_antennas)], id=str(i), is_open=False)]
  105. else:
  106. temp += [ge.create_accordion_card(a_topic, dcc.Markdown(parsed_text),
  107. id=str(i), is_open=False)]
  108. return html.Div(temp, className='col-12 accordion shadow-1-strong')
  109. @app.callback([Output(f"collapse-{i}", "is_open") for i in range(len(doc_files))],
  110. [Input(f"group-{i}-toggle", "n_clicks") for i in range(len(doc_files))],
  111. [State(f"collapse-{i}", "is_open") for i in range(len(doc_files))])
  112. def toggle_accordion(*args):
  113. """Allows the expansion/collapse of an HTML accordion block.
  114. """
  115. defaults = list(args[len(doc_files):])
  116. ctx = dash.callback_context
  117. if not ctx.triggered:
  118. return [dash.no_update]*len(doc_files)
  119. else:
  120. button_id = ctx.triggered[0]["prop_id"].split(".")[0]
  121. for i in range(len(doc_files)):
  122. if (button_id == f"group-{i}-toggle") and (args[i] is not None):
  123. defaults[i] = not defaults[i]
  124. return defaults
  125. def error_text(an_error):
  126. """Standard error message written in a modal error window.
  127. It returns a str mentioning 'an_error' and the contact details to report it.
  128. """
  129. return f"An error occured.\n{an_error}.\nPlease report to marcote@jive.eu " \
  130. "or in https://github.com/bmarcote/vlbi_calculator."
  131. def convert_colon_coord(colon_coord):
  132. """Converts some coordinates given in a str format 'HH:MM:SS DD:MM:SS' to
  133. 'HHhMMmSSs DDdMMdSSs'.
  134. If ':' are not present in colon_coord, then it returns the same str.
  135. """
  136. if ':' not in colon_coord:
  137. return colon_coord
  138. for l in ('h', 'm', 'd', 'm'):
  139. colon_coord = colon_coord.replace(':', l, 1)
  140. return ' '.join([f"{s}s" for s in colon_coord.split()])
  141. def alert_message(message, title="Warning!"):
  142. """Produces an alert-warning message.
  143. 'message' can be either a string or a list with different string/dash components.
  144. """
  145. if type(message) == str:
  146. return [html.Br(), \
  147. dbc.Alert([html.H4(title, className='alert-heading'), message], \
  148. color='warning', dismissable=True)]
  149. else:
  150. return [html.Br(), \
  151. dbc.Alert([html.H4(title, className='alert-heading'), *message], \
  152. color='warning', dismissable=True)]
  153. def update_sensitivity(obs):
  154. """Given the observation, it sets the text for all summary cards
  155. with information about the observation.
  156. """
  157. cards = []
  158. cards += ge.summary_card_times(app, obs)
  159. cards += ge.summary_card_frequency(app, obs)
  160. cards += ge.summary_card_antennas(app, obs)
  161. cards += ge.summary_card_beam(app, obs)
  162. cards += ge.summary_card_rms(app, obs)
  163. cards += ge.summary_card_fov(app, obs)
  164. return [html.Div(className='card-deck col-12 justify-content-center',
  165. children=cards),
  166. html.Br(),
  167. html.Div(style={'height': '5rem'}),
  168. html.Div(className='col-12 justify-content-center',
  169. children=ge.summary_card_worldmap(app, obs))]
  170. def arrays_with_band(arrays, a_band):
  171. """Returns the arrays that can observe the given band with at least two antennas.
  172. It excludes e-EVN if it is included in arrays.
  173. Inputs
  174. - arrays : dict
  175. The keys are the name of the array and the values must be a dict with the codenames
  176. of the antennas in an array inside the value 'default_antennas'.
  177. - a_band : str
  178. The band to be observed, following the criteria in fs.bands.
  179. Returns
  180. - arrays_with_band : str
  181. Comma-separated list of the arrays that can observe the given band.
  182. """
  183. tmp = [] # the list of arrays that can observe the given band
  184. for an_array in arrays:
  185. if an_array != 'e-EVN':
  186. if a_band in arrays[an_array]['observing_bands']:
  187. tmp.append(an_array)
  188. if len(tmp) == 0:
  189. return 'none'
  190. elif len(tmp) == 2:
  191. return ' and '.join(tmp)
  192. elif len(tmp) in (1, 3):
  193. return ', '.join(tmp)
  194. else: # >= 4
  195. return ', '.join(tmp[:-1]) + ' and ' + tmp[-1]
  196. @app.callback(Output('initial-pickband-label', 'children'),
  197. [Input('initial-band', 'value')])
  198. def update_pickband_tooltip(a_wavelength):
  199. a_band = tuple(fs.bands)[a_wavelength]
  200. return [dbc.Card(dbc.CardBody([
  201. html.H5([html.Img(height='30rem',
  202. src=app.get_asset_url(f"waves-{a_band.replace('.', '_')}.png"),
  203. alt='Band: ', className='d-inline-block'),
  204. html.Span(f"{fs.bands[a_band].split('(')[0].strip()}",
  205. style={'float': 'right'})
  206. ], className="card-title"),
  207. html.P([html.Span("Wavelength: ", style={'color': '#888888'}),
  208. f"{fs.bands[a_band].split('(')[1].split('or')[0].strip()}.",
  209. html.Br(),
  210. html.Span("Frequency: ", style={'color': '#888888'}),
  211. f"{fs.bands[a_band].split('(')[1].split('or')[1].replace(')', '').strip()}.",
  212. html.Br(),
  213. html.Span(html.Small(f"Can be observed with the {arrays_with_band(default_arrays, a_band)}."),
  214. style={'color': '#888888'})
  215. ], className="card-text"),
  216. ]), className="col-sm-3 my-2 shadow-1-strong", style={'min-width': '15rem'})
  217. ]
  218. # @app.callback([Output('initial-timeselection-div-guess', 'hidden'),
  219. # Output('initial-timeselection-div-epoch', 'hidden')],
  220. # [Input('initial-timeselection', 'value')])
  221. # def type_initial_time_selection(time_selection_selected):
  222. # """Modifies the hidden message related to the two options about how to pick the observing time.
  223. # """
  224. # print('checked')
  225. # return [time_selection_selected, not time_selection_selected]
  226. @app.callback([Output('initial-timeselection-div-guess', 'hidden'),
  227. Output('initial-timeselection-div-epoch', 'hidden')],
  228. [Input('initial-timeselection', 'value')])
  229. def type_initial_time_selection(time_selection_selected):
  230. """Modifies the hidden message related to the two options about how to pick the observing time.
  231. """
  232. return [time_selection_selected, not time_selection_selected]
  233. @app.callback([Output('timeselection-div-guess', 'hidden'),
  234. Output('timeselection-div-epoch', 'hidden')],
  235. [Input('timeselection', 'value')])
  236. def type_time_selection(time_selection_selected):
  237. """Modifies the hidden message related to the two options about how to pick the observing time.
  238. """
  239. return [time_selection_selected, not time_selection_selected]
  240. @app.callback(Output('band', 'value'),
  241. Input('initial-band', 'value'), prevent_initial_call=True)
  242. def band_from_initial(initial_value):
  243. return tuple(fs.bands)[initial_value] if initial_value is not None else dash.no_update
  244. @app.callback(Output('array', 'value'),
  245. [Input(f'network-{network.lower()}', 'value') for network in default_arrays if network != 'e-EVN'],
  246. prevent_initial_call=True)
  247. def array_from_initial(*selected_networks):
  248. return [network for (network,selected) in \
  249. zip([n for n in default_arrays if n != 'e-EVN'], selected_networks) if selected]
  250. @app.callback(Output('e-EVN', 'value'),
  251. Input('initial-e-EVN', 'value'), prevent_initial_call=True)
  252. def e_EVN_from_initial(initial_value):
  253. return initial_value if initial_value is not None else dash.no_update
  254. @app.callback(Output('timeselection', 'value'),
  255. Input('initial-timeselection', 'value'), prevent_initial_call=True)
  256. def timeselection_from_initial(initial_value):
  257. return initial_value if initial_value is not None else dash.no_update
  258. @app.callback(Output('starttime', 'date'),
  259. Input('initial-starttime', 'date'), prevent_initial_call=True)
  260. def starttime_from_initial(initial_value):
  261. return initial_value if initial_value is not None else dash.no_update
  262. @app.callback(Output('starthour', 'value'),
  263. Input('initial-starthour', 'value'), prevent_initial_call=True)
  264. def starthour_from_initial(initial_value):
  265. return initial_value if initial_value is not None else dash.no_update
  266. @app.callback(Output('duration', 'value'),
  267. Input('initial-duration', 'value'), prevent_initial_call=True)
  268. def duration_from_initial(initial_value):
  269. return initial_value if initial_value is not None else dash.no_update
  270. @app.callback(Output('source', 'value'),
  271. Input('initial-source', 'value'), prevent_initial_call=True)
  272. def source_from_initial(initial_value):
  273. return initial_value if initial_value is not None else dash.no_update
  274. @app.callback([Output('subbands', 'value'),
  275. Output('channels', 'value'),
  276. Output('pols', 'value'),
  277. Output('inttime', 'value')],
  278. Input('is_line', 'value'), prevent_initial_call=True)
  279. def line_cont_setup(is_line_exp):
  280. if is_line_exp is None:
  281. return dash.no_update, dash.no_update, dash.no_update, dash.no_update
  282. if is_line_exp:
  283. return 1, 4096, 4, 2
  284. else:
  285. return 8, 32, 4, 2
  286. @app.callback([Output('button-picknetwork', 'disabled'),
  287. Output('button-picknetwork', 'children')],
  288. [Input(f"network-{network.lower()}", 'value') for network in default_arrays if network != 'e-EVN'])
  289. def continue_from_networks(*networks):
  290. """Verifies that the user has selected at least one VLBI network during the wizard screen
  291. """
  292. for n in networks:
  293. if True in n:
  294. return False, 'Continue'
  295. return True, 'Select network(s) to continue'
  296. # len([True for n in networks if True in n]) > 0 % Slower
  297. @app.callback([Output('button-pickband', 'disabled'),
  298. Output('button-pickband', 'children')],
  299. Input('initial-band', 'value'),
  300. [State(f"network-{network.lower()}", 'value') for network in default_arrays if network != 'e-EVN'])
  301. def continue_from_band(selected_band, *networks):
  302. """Verifies that the selected band can be observed by the given network.
  303. """
  304. for n,nname in zip(networks, [network for network in default_arrays if network != 'e-EVN']):
  305. if True in n:
  306. if tuple(fs.bands.keys())[selected_band] in default_arrays[nname]['observing_bands']:
  307. return False, 'Continue'
  308. return True, 'The selected network cannot observe at this band'
  309. @app.callback([Output('button-picktimes', 'disabled'),
  310. Output('button-picktimes', 'children')],
  311. [Input('initial-timeselection', 'value'),
  312. Input('initial-starttime', 'date'),
  313. Input('initial-starthour', 'value'),
  314. Input('initial-duration', 'value'),
  315. Input('initial-source', 'value')])
  316. def continue_from_times(time_selection, time_date, time_hour, time_duration, source):
  317. """Verifies that the user has selected and introduced the required data before continue.
  318. """
  319. if (source is None) or (not verify_recognized_source(source)):
  320. return True, 'Specify epoch and target before continue'
  321. if time_selection:
  322. if (time_date is not None) and (time_hour is not None) and (time_duration is not None):
  323. try:
  324. dummy = float(time_duration)
  325. return (True, 'Specify epoch and target before continue') if (dummy <= 0) or (dummy > 4*24) \
  326. else (False, 'Continue')
  327. except:
  328. return True, 'Specify epoch and target before continue'
  329. else:
  330. return True, 'Specify epoch and target before continue'
  331. return False, 'Continue'
  332. @app.callback(Output('main-window2', 'children'),
  333. Output('is_line', 'value'),
  334. [Input('button-pickband', 'n_clicks'),
  335. Input('button-picknetwork', 'n_clicks'),
  336. Input('button-picktimes', 'n_clicks'),
  337. Input('button-mode-continuum', 'n_clicks'),
  338. Input('button-mode-line', 'n_clicks')])
  339. def intro_choices(clicks_pickband, clicks_picknetwork, clicks_picktimes, clicks_continuum, clicks_line):
  340. if clicks_picknetwork is not None:
  341. return choice_page('band'), dash.no_update
  342. elif clicks_pickband is not None:
  343. return choice_page('time'), dash.no_update
  344. elif clicks_picktimes is not None:
  345. return choice_page('mode'), dash.no_update
  346. elif clicks_continuum is not None:
  347. return choice_page('final'), False
  348. elif clicks_line is not None:
  349. return choice_page('final'), True
  350. else:
  351. return dash.no_update, dash.no_update
  352. def initial_page():
  353. """Initial window with the two options to select: guided or manual setup of the observation.
  354. """
  355. return [
  356. html.Div(className='row justify-content-center', id='main-window2',
  357. children=html.Div(className='col-sm-6 justify-content-center',
  358. children=[html.Div(className='justify-content-center',
  359. children=[#html.H3("Welcome!"),
  360. html.P(["The EVN Observation Planner allows you to plan observations with the ",
  361. html.A(href="https://www.evlbi.org", children="European VLBI Network"),
  362. " (EVN) and other Very Long Baseline Interferometry (VLBI) networks. "
  363. "The EVN Observation Planner helps you to determine when your source "
  364. "can be observed by the different antennas, and provides the expected "
  365. "outcome of these observations, like the expected sensitivity or resolution."]),
  366. html.Br(),
  367. html.Div(ge.initial_window_start(app))
  368. ])
  369. ])
  370. )]
  371. @app.callback(Output('full-window', 'children'),
  372. [Input('button-initial-wizard', 'n_clicks'),
  373. Input('button-initial-expert', 'n_clicks')])
  374. def choice_for_setup(do_wizard, do_expert):
  375. if (do_expert is not None) or (do_wizard is not None):
  376. return [
  377. # order inverted to improve loading times
  378. html.Div(id='main-window2', hidden=do_expert is not None,
  379. children=[dbc.Checklist(id='is_line', options=[{'label': 'line obs', 'value': False}],
  380. value=[])] if do_expert is not None else choice_page('network')),
  381. html.Div(id='main-window', hidden=do_expert is None,
  382. children=main_page(show_compute_button=do_expert is not None))
  383. ]
  384. else:
  385. return dash.no_update
  386. def choice_page(choice_card):
  387. """Initial window with the introduction to the EVN Observation Planner and the band selection.
  388. """
  389. return [
  390. html.Div(className='row justify-content-center', id='main-window2',
  391. children=html.Div(className='col-sm-6 justify-content-center',
  392. children=[html.Div(className='justify-content-center',
  393. children=[#html.H3("Welcome!"),
  394. html.P(["The EVN Observation Planner allows you to plan observations with the ",
  395. html.A(href="https://www.evlbi.org", children="European VLBI Network"),
  396. " (EVN) and other Very Long Baseline Interferometry (VLBI) networks. "
  397. "The EVN Observation Planner helps you to determine when your source "
  398. "can be observed by the different antennas, and provides the expected "
  399. "outcome of these observations, like the expected sensitivity or resolution."]),
  400. html.Br(),
  401. *[
  402. # html.Div(hidden=False if choice_card == 'choice' else True,
  403. # children=ge.initial_window_start(app)),
  404. html.Div(hidden=False if choice_card == 'band' else True,
  405. children=ge.initial_window_pick_band()),
  406. html.Div(hidden=False if choice_card == 'network' else True,
  407. children=ge.initial_window_pick_network(app, default_arrays)),
  408. html.Div(hidden=False if choice_card == 'time' else True,
  409. children=ge.initial_window_pick_time()),
  410. html.Div(hidden=False if choice_card == 'mode' else True,
  411. children=ge.initial_window_pick_mode(app)),
  412. html.Div(hidden=False if choice_card == 'final' else True,
  413. children=ge.initial_window_final()),
  414. ],
  415. ], style={'text:align': 'justify !important'})
  416. ])
  417. )]
  418. def main_page(results_visible=False, summary_output=None, fig_elev_output=None,
  419. fig_ant_output=None, fig_uv_output=None, fig_dirty_map_output=False, show_compute_button=True):
  420. return [# First row containing all buttons/options, list of telescopes, and button with text output
  421. dcc.ConfirmDialog(id='global-error', message=''),
  422. # Elements in second column (checkboxes with all stations)
  423. html.Div(className='container-fluid', children=[
  424. html.Div(className='row justify-content-center', children=[
  425. html.Div(className='col-sm-3', style={'max-width': '350px','float': 'left',
  426. 'min-width': '17rem'}, children=[
  427. html.Div(className='form-group', children=[
  428. html.Div('', style={'height': '70px'}),
  429. dcc.Loading(id="loading2", children=[html.Div(id="loading-output2"), html.Br()],
  430. type="dot"),
  431. html.Button('Compute observation',
  432. id='antenna-selection-button',
  433. className='btn btn-primary btn-lg',
  434. style={'width': '100%', 'margin-bottom': '1rem'})#if show_compute_button else html.Br(),
  435. ]),
  436. html.Br(),
  437. html.Div(className='form-group', children=[
  438. html.H6(['Your observing Band',
  439. *ge.tooltip(idname='popover-band',
  440. message="This will update the "
  441. "antenna list showing the ones that can observe "
  442. "at that given frequency.")
  443. ]),
  444. dcc.Dropdown(id='band', persistence=True, value='18cm',
  445. options=[{'label': fs.bands[b], 'value': b} for b \
  446. # in fs.bands], value='18cm'),
  447. in fs.bands], placeholder='Select observing band...'),
  448. ]),
  449. html.Br(),
  450. html.Div(className='form-group', children=[
  451. html.H6(['Real-time correlation?',
  452. *ge.tooltip(idname='popover-eevn',
  453. message="Only available for the EVN: real-time correlation mode."
  454. "The data are transferred and correlated in real-time, but "
  455. "not all telescopes are capable for this and the bandwidth "
  456. "may be limited. Observations during the e-EVN epochs.")
  457. ]),
  458. dbc.Checklist(id='e-EVN', className='checkbox', persistence=True,
  459. options=[{'label': ' e-EVN mode',
  460. 'value': 'e-EVN'}], value=[]),
  461. ]),
  462. html.Br(),
  463. html.Div(className='form-group', children=[
  464. html.H6(['Source (name or coordinates)',
  465. *ge.tooltip(idname='popover-target',
  466. message="Source name or coordinates. " \
  467. "You may see an error if the given name is not properly resolved. "
  468. "J2000 coordinates are assumed in both forms: 00:00:00 00:00:00 or " \
  469. "00h00m00s 00d00m00s.")
  470. ]),
  471. dcc.Input(id='source', value=None, type='text',
  472. className='form-control', placeholder="hh:mm:ss dd:mm:ss",
  473. persistence=True),
  474. html.Small(id='error_source',
  475. className='form-text text-muted'),
  476. ]),
  477. html.Br(),
  478. html.Div(className='form-group', children=[
  479. html.H6('Epoch for observation'),
  480. dbc.FormGroup([
  481. dbc.RadioItems(options=[{"label": "I don't have a preferred epoch", "value": False},
  482. {"label": "I know the observing epoch", "value": True}],
  483. value=True, id="timeselection", inline=True, persistence=True),
  484. ], inline=True),
  485. html.Div(children=[
  486. html.Div(id='timeselection-div-guess', className='row justify-content-center',
  487. hidden=False, children=[
  488. html.Small("Choose the first option to find out when your source "
  489. "may be visible (by >3 telescopes).", style={'color': '#999999'}),
  490. html.Small("Note that this option may not provide the best (expected) "
  491. "results in case of combining different networks very far apart "
  492. "(e.g. LBA and EVN).", style={'color': '#999999'})
  493. ]),
  494. html.Div(id='timeselection-div-epoch', hidden=True, children=[
  495. html.Label('Start of observation (UTC)'),
  496. *ge.tooltip(idname='popover-startime', message="Select the date and "
  497. "time of the start of the observation (Universal, UTC, "
  498. "time). You will also see the day of the year (DOY) in "
  499. "brackets once the date is selected."),
  500. html.Br(),
  501. dcc.DatePickerSingle(id='starttime', date=None, min_date_allowed=dt(1900, 1, 1),
  502. max_date_allowed=dt(2100, 1, 1),
  503. display_format='DD-MM-YYYY (DDD)',
  504. placeholder='Start date',
  505. first_day_of_week=1,
  506. initial_visible_month=dt.today(),
  507. persistence=True,
  508. className='form-picker'),
  509. dcc.Dropdown(id='starthour', placeholder="Start time (UTC)", value=None,
  510. options=[{'label': f"{hm//60:02n}:{hm % 60:02n}", \
  511. 'value': f"{hm//60:02n}:{hm % 60:02n}"} \
  512. for hm in range(0, 24*60, 15)],
  513. persistence=True, className='form-hour'),
  514. html.Small(id='error_starttime', style={'color': 'red'},
  515. className='form-text text-muted'),
  516. html.Label('Duration of the observation (in hours)'),
  517. html.Div(className='form-group', children=[
  518. dcc.Input(id='duration', value=None, type='number', className='form-control',
  519. placeholder="Duration in hours", persistence=True, inputMode='numeric'),
  520. html.Small(id='error_duration', className='form-text text-danger')
  521. ])
  522. ])
  523. ]),
  524. ]),
  525. html.Br(),
  526. html.Div(className='form-group', children=[
  527. html.H6(['% of on-target time',
  528. *ge.tooltip(idname='popover-ontarget',
  529. message="Assumes that you will only spend this amount of the total " \
  530. "observing time on the given target source. It affects the " \
  531. "expected sensitivity."),
  532. ]),
  533. dcc.Slider(id='onsourcetime', min=20, max=100, step=5, value=70,
  534. marks= {i: str(i) for i in range(20, 101, 10)},
  535. persistence=True),
  536. html.Label(id='onsourcetime-label', style={'color': '#999999'},
  537. children='70% of the observation.'),
  538. ]),
  539. html.Br(),
  540. html.Div(className='form-group', children=[
  541. html.H6(['Datarate per station',
  542. *ge.tooltip(idname='popover-datarate',
  543. message=["Expected datarate for each station, assuming all " \
  544. "of them run at the same rate.",
  545. html.Ul([
  546. html.Li("The EVN can run typically at up to 2 Gbps (1 Gbps at L band), " \
  547. "although a few antennas may observe at lower datarates."),
  548. html.Li("The VLBA can now observe up to 4 Gbps."),
  549. html.Li("The LBA typically runs at 512 Mbps but can reach up to 1 Gbps."),
  550. html.Li("Check the documentation from other networks to be " \
  551. "sure about their capabilities.")])])
  552. ]),
  553. dcc.Dropdown(id='datarate',
  554. placeholder="Select the data rate...",
  555. options=[{'label': fs.data_rates[dr], 'value': dr} \
  556. for dr in fs.data_rates], value=2048, persistence=True),
  557. html.Label(id='bandwidth-label', style={'color': '#999999'}, children='')
  558. ]),
  559. html.Div(className='form-group', children=[
  560. html.H6(['Number of subbands',
  561. *ge.tooltip(idname='popover-subbands',
  562. message="Number of subbands to split the total observed bandwidth "
  563. " during correlation (IFs in AIPS).")
  564. ]),
  565. dcc.Dropdown(id='subbands', placeholder="Select no. subbands...",
  566. options=[{'label': fs.subbands[sb], 'value': sb} \
  567. for sb in fs.subbands], value=8, persistence=True),
  568. ]),
  569. html.Br(),
  570. html.Div(className='form-group', children=[
  571. html.H6(['Number of spectral channels',
  572. *ge.tooltip(idname='popover-channels',
  573. message="How many channels per subband will be produced "
  574. "during correlation.")
  575. ]),
  576. dcc.Dropdown(id='channels', placeholder="Select no. channels...",
  577. options=[{'label': fs.channels[ch],
  578. 'value': ch} \
  579. for ch in fs.channels], value=32, persistence=True),
  580. ]),
  581. html.Br(),
  582. html.Div(className='form-group', children=[
  583. html.H6(['Number of polarizations',
  584. *ge.tooltip(idname='popover-pols',
  585. message="Number of polarizations to correlate. Note that VLBI uses circular " \
  586. "polarizations. Full polarization implies the four stokes: RR, LL, RL, LR; " \
  587. "while dual polarization implies RR and LL only.")
  588. ]),
  589. dcc.Dropdown(id='pols', placeholder="Select polarizations...",
  590. options=[{'label': fs.polarizations[p], 'value': p} \
  591. for p in fs.polarizations], value=4, persistence=True),
  592. ]),
  593. html.Br(),
  594. html.Div(className='form-group', children=[
  595. html.H6(['Integration time',
  596. *ge.tooltip(idname='popover-inttime',
  597. message="Integration time to compute each visibility. Note that for continuum " \
  598. "observations values of 1-2 seconds are typical.")
  599. ]),
  600. dcc.Dropdown(id='inttime', placeholder="Select integration time...",
  601. options=[{'label': fs.inttimes[it], 'value': it} \
  602. for it in fs.inttimes], value=2, persistence=True),
  603. ])
  604. ]),
  605. dcc.Tabs(parent_className='custom-tabs col', className='custom-tabs-container', id='tabs',
  606. value='tab-setup', children=[
  607. dcc.Tab(label='Antenna Selection', className='custom-tab', value='tab-setup',
  608. selected_className='custom-tab--selected', children=[
  609. # Elements in first column
  610. html.Div(className='row justify-content-center', children=[
  611. html.Div(className='col-9', children=[
  612. html.Div(id='first-advise', className='col-sm-9', children=[
  613. html.H4("Customize your observation"),
  614. html.P(["Select which VLBI network(s) you want to use in your "
  615. "observation, or select an ad-hoc array of antennas. ", html.Br(),
  616. "Set the basic information from your observations: "
  617. "observing band, target source, epoch, and observing setup. ", html.Br(),
  618. "Finally, press the blue ", html.B("'compute observation'"),
  619. " button. ", html.Br(),
  620. "You will get a detailed "
  621. "summary of the planned observation and expected outcome in the different "
  622. "tabs."]),
  623. html.P(html.Em(["Note that only antennas that can observe at the selected band "
  624. "will be clickable."], className='form-text text-warning'))
  625. ], style={'margin-top': '2rem', 'margin-bottom': '2rem'}),
  626. html.Div(className='col-9 form-group row align-items-end', children=[
  627. html.Div(className='col-md-12', children=[
  628. html.Label('Select default VLBI Network(s)'),
  629. # style={'color': '#a01d26'}),
  630. *ge.tooltip(idname='popover-network', message="Automatically selects "
  631. "the default participating antennas for the selected VLBI network(s)."),
  632. dcc.Dropdown(id='array', options=[{'label': f"{n}: {default_arrays[n]['name']}" \
  633. if n != default_arrays[n]['name'] else n,
  634. 'value': n} for n in default_arrays if n != 'e-EVN'], value=[],
  635. persistence=True, multi=True),
  636. ]),
  637. ]),
  638. html.Div(className='col-9 text-center justify-content-center', children=[
  639. dcc.Loading(id="loading", children=[html.Br(), html.Div(id="loading-output")],
  640. type="dot")
  641. ]),
  642. html.Div([dbc.Tooltip(ge.antenna_card(app, s), placement='right',
  643. hide_arrow=True, target=f"_input_{s.codename}",
  644. innerClassName='tooltip-card-inner') for s in all_antennas
  645. ]),
  646. html.Div(id='antennas-div', className='container', children=[
  647. html.Div(className='antcheck', children=[html.Br(), html.Br(),
  648. html.Label(html.H4(f"{sorted_networks[an_array]}"),
  649. style={'width': '100%'}),
  650. html.Br(),
  651. html.Div(className='antcheck', children=[
  652. dbc.FormGroup([
  653. Checkbox(id=f"check_{s.codename}", persistence=True,
  654. className='custom-control-input',
  655. disabled=not s.has_band(selected_band)),
  656. dbc.Label(s.name, html_for=f"check_{s.codename}",
  657. id=f"_input_{s.codename}",
  658. className='custom-control-label')
  659. ], check=True, inline=True,
  660. className="custom-checkbox custom-control custom-control-inline")
  661. for s in all_antennas if s.network == an_array
  662. ])
  663. ]) for an_array in sorted_networks
  664. ]),
  665. html.Div(style={'height': '15rem'})
  666. ]),
  667. # html.Div(className='col-sm-2', style={'float': 'left'}, children=[
  668. # ])
  669. ])
  670. ]),
  671. dcc.Tab(label='Summary', className='custom-tab', value='tab-summary', id='tab-summary',
  672. selected_className='custom-tab--selected', disabled=not results_visible, children=[
  673. html.Div(className='row justify-content-center', children=[
  674. html.Div(className='col-10 justify-content-center',
  675. id='sensitivity-output',
  676. children=summary_output)
  677. # children=[html.Div(className='col-md-6', children=[
  678. # html.Br(), html.Br(), html.H2("Set the observation first"),
  679. # html.P("Here you will see a summary of your observation, "
  680. # "with information about all participating stations, longest and "
  681. # "shortest baseline, expected size of the data once is correlated, "
  682. # "reached resolution and sensitivity, and the limitations in your "
  683. # "field of view due to time and frequency smearing.")])
  684. # ])
  685. ])
  686. ]),
  687. dcc.Tab(label='Elevations', className='custom-tab', value='tab-elevation', id='tab-elevation',
  688. selected_className='custom-tab--selected',
  689. disabled=not results_visible, children=[
  690. html.Div(className='row justify-content-center', children=[
  691. html.Div(className='col-md-8 justify-content-center', children=[
  692. # Elevation VS time
  693. html.Br(),
  694. html.Div([
  695. html.Br(),
  696. html.H4("When is your source visible?"),
  697. html.Br(),
  698. dbc.Alert([html.H4("Interactive plots", className='alert-heading'),
  699. html.P(["A single click on one station in the legend will "
  700. "hide/show it.", html.Br(), "Double-click will hide/show "
  701. "all other antennas. You can also save the plot "
  702. "as png."]),
  703. ], color='info', dismissable=True),
  704. html.Br(),
  705. html.P("The following plot shows the source elevation for the "
  706. "different antennas during the proposed observation. The horizontal "
  707. "solid and dashed lines represent the elevation of 20 and 10 degrees, "
  708. "respectively.")
  709. ]),
  710. html.Div([
  711. dcc.Graph(id='fig-elev-time', children=fig_elev_output)
  712. ],className='tex2jax_ignore'),
  713. html.Br(),
  714. html.Div([
  715. html.P("""The following plot shows when the source may be observed
  716. for the different antennas, assuming a minimum elevation of 10 degrees
  717. for most antennas (except e.g. Arecibo). Note that some antennas may
  718. have additional constraints for particular azimuth or elevation
  719. angles that are not considered here.
  720. """)
  721. ]),
  722. html.Div([
  723. dcc.Graph(id='fig-ant-time', children=fig_ant_output)
  724. ],className='tex2jax_ignore')
  725. ])])
  726. ]),
  727. dcc.Tab(label='UV Coverage', className='custom-tab', value='tab-uv', id='tab-uv',
  728. selected_className='custom-tab--selected', disabled=fig_uv_output is None, children=[
  729. # Images
  730. html.Div(className='row justify-content-center', children=[
  731. html.Div(className='col-md-8 justify-content-center', children=[
  732. # dcc.Markdown(children="""To be implemented.
  733. # The uv coverage and expected dirty images will go here.""")
  734. html.Br(),
  735. html.Div([
  736. html.Br(),
  737. html.H4("Resulting (u,v) coverage"),
  738. html.Br()
  739. ]),
  740. html.Div(children=[dcc.Graph(id='fig-uvplane', children=fig_uv_output)],
  741. className='tex2jax_ignore'),
  742. html.Br(),
  743. html.Div([
  744. html.Br(),
  745. html.H4("Resulting dirty beam"),
  746. html.Br()
  747. ]),
  748. html.Div(children=[dcc.Graph(id='fig-dirtymap', figure=fig_dirty_map_output)],
  749. className='tex2jax_ignore')
  750. ])])
  751. ]),
  752. dcc.Tab(label='Documentation', className='custom-tab', value='tab-doc',
  753. selected_className='custom-tab--selected', children=[
  754. # Documentation
  755. html.Div(className='row justify-content-center', children=[
  756. html.Div([html.Br(), html.Br()]),
  757. html.Div(className='col-md-8', children=get_doc_text())
  758. ])
  759. ])
  760. ])
  761. ]),
  762. html.Div(className='container-fluid', children=[html.Br(), html.Br()])
  763. ])
  764. ]
  765. @app.callback(Output('onsourcetime-label', 'children'),
  766. [Input('onsourcetime', 'value'),
  767. Input('duration', 'value'),
  768. Input('timeselection', 'value')])
  769. def update_onsourcetime_label(onsourcetime, total_duration, defined_epoch):
  770. """Keeps the on-source time label updated with the value selected by the user.
  771. """
  772. if (total_duration is not None) and defined_epoch:
  773. return f"{onsourcetime}% of the total time ({ge.optimal_units(total_duration*u.h*onsourcetime/100, [u.h, u.min, u.s]):.03n})."
  774. return f"{onsourcetime}% of the total observation."
  775. @app.callback(Output('bandwidth-label', 'children'),
  776. [Input('datarate', 'value'),
  777. Input('pols', 'value')])
  778. def update_bandwidth_label(datarate, npols):
  779. """Updates the total bandwidth label as a function of the selected datarate and number of
  780. polarizations. Returns a string with the value and units.
  781. """
  782. if (None not in (datarate, npols)) and (datarate != -1):
  783. # Either 1 or 2 pols per station:
  784. temp = npols % 3 + npols // 3
  785. return [f"The total bandwidth is {ge.optimal_units(datarate*u.MHz/(temp*2*2), [u.GHz, u.MHz, u.kHz] )}.",
  786. html.Br(), html.Br()]
  787. return ''
  788. @app.callback([Output(f"check_{s.codename}", 'checked') for s in all_antennas] + \
  789. [Output(f"check_{s.codename}", 'disabled') for s in all_antennas] + \
  790. [Output('datarate', 'value')],
  791. [Input('band', 'value'),
  792. Input('array', 'value'),
  793. Input('e-EVN', 'value'),
  794. Input('is_line', 'value')])
  795. def select_antennas(selected_band, selected_networks, is_eEVN, is_line):
  796. """Given a selected band and selected default networks, it selects the associated
  797. antennas from the antenna list.
  798. """
  799. # Getting the data rate
  800. if 'EVN' in selected_networks or is_eEVN:
  801. datarate = default_arrays['EVN']['max_datarate'] if selected_band not in ('18cm', '21cm') else 1024
  802. else:
  803. datarate = -1
  804. for an_array in selected_networks:
  805. datarate = max(datarate, default_arrays[an_array]['max_datarate'])
  806. # Getting the selected antennas
  807. selected_antennas = []
  808. if is_eEVN:
  809. selected_antennas = [ant for ant in default_arrays['e-EVN']['default_antennas'] \
  810. if all_antennas[ant].has_band(selected_band)]
  811. for an_array in selected_networks:
  812. selected_antennas += [ant for ant in default_arrays[an_array]['default_antennas'] \
  813. if all_antennas[ant].has_band(selected_band) and (an_array != 'EVN' or not is_eEVN)]
  814. return [True if s.codename in selected_antennas else False for s in all_antennas] + \
  815. [False if (s.has_band(selected_band) and (s.real_time or not is_eEVN)) else True \
  816. for s in all_antennas] + [datarate if not is_line else 256]
  817. @app.callback([Output('initial-error_starttime', 'children'),
  818. Output('initial-error_duration', 'children')],
  819. [Input('initial-starttime', 'date'), Input('starthour', 'value'),
  820. Input('initial-duration', 'value')])
  821. def check_initial_obstime(starttime, starthour, duration):
  822. """Verify the introduced times/dates for correct values.
  823. Once the user has introduced all values for the start and end of the observation,
  824. it guarantees that they have the correct shape:
  825. - the duration of the observation is > 0 hours.
  826. - The total observing length is less than five days (value chosen for computational reasons).
  827. """
  828. if duration is None:
  829. return "", ""
  830. if (not isinstance(duration, float)) and (not isinstance(duration, int)):
  831. return "", "Must be a number"
  832. if duration <= 0.0:
  833. return "", "The duration must be a positive number"
  834. elif duration > 4*24:
  835. return "", "Please, put an observation shorter than 4 days"
  836. return "", ""
  837. @app.callback([Output('error_starttime', 'children'),
  838. Output('error_duration', 'children')],
  839. [Input('starttime', 'date'), Input('starthour', 'value'),
  840. Input('duration', 'value')])
  841. def check_obstime(starttime, starthour, duration):
  842. """Verify the introduced times/dates for correct values.
  843. Once the user has introduced all values for the start and end of the observation,
  844. it guarantees that they have the correct shape:
  845. - the duration of the observation is > 0 hours.
  846. - The total observing length is less than five days (value chosen for computational reasons).
  847. """
  848. if duration is None:
  849. return "", ""
  850. if (not isinstance(duration, float)) and (not isinstance(duration, int)):
  851. return "", "Must be a number"
  852. if duration <= 0.0:
  853. return "", "The duration must be a positive number"
  854. elif duration > 4*24:
  855. return "", "Please, put an observation shorter than 4 days"
  856. return "", ""
  857. @app.callback([Output('initial-error_source', 'children'),
  858. Output('initial-error_source', 'className')],
  859. [Input('initial-source', 'value')])
  860. def get_initial_source(source_coord):
  861. """Verifies that the introduced source coordinates have a right format.
  862. If they are correct, it does nothing. If they are incorrect, it shows an error label.
  863. """
  864. if source_coord != 'hh:mm:ss dd:mm:ss' and source_coord != None and source_coord != '':
  865. if len(source_coord) > 30:
  866. # Otherwise the source name check gets too slow
  867. return "Name too long.", 'form-text text-danger'
  868. try:
  869. dummy_target = observation.Source(convert_colon_coord(source_coord), 'Source')
  870. return '', dash.no_update
  871. except ValueError as e:
  872. try:
  873. dummy_target = coord.get_icrs_coordinates(source_coord)
  874. return dummy_target.to_string('hmsdms'), 'form-text text-muted'
  875. except coord.name_resolve.NameResolveError as e:
  876. return "Unrecognized name. Use 'hh:mm:ss dd:mm:ss' or 'XXhXXmXXs XXdXXmXXs'", \
  877. 'form-text text-danger'
  878. except ValueError as e:
  879. return "Wrong coordinates.", 'form-text text-danger'
  880. else:
  881. return '', dash.no_update
  882. def verify_recognized_source(a_source):
  883. """Equivalent to the previous function, but returns a bool for when a source name/coordinates
  884. have been introduced correctly or not.
  885. """
  886. if a_source is None:
  887. return False
  888. if len(a_source) > 30:
  889. return False
  890. try:
  891. dummy_target = observation.Source(convert_colon_coord(a_source), 'Source')
  892. return True
  893. except ValueError as e:
  894. try:
  895. dummy_target = coord.get_icrs_coordinates(a_source)
  896. return True
  897. except coord.name_resolve.NameResolveError as e:
  898. return False
  899. except ValueError as e:
  900. return False
  901. return False
  902. @app.callback([Output('error_source', 'children'),
  903. Output('error_source', 'className')],
  904. [Input('source', 'value')])
  905. def get_source(source_coord):
  906. """Verifies that the introduced source coordinates have a right format.
  907. If they are correct, it does nothing. If they are incorrect, it shows an error label.
  908. """
  909. if source_coord != 'hh:mm:ss dd:mm:ss' and source_coord != None and source_coord != '':
  910. if len(source_coord) > 30:
  911. # Otherwise the source name check gets too slow
  912. return "Name too long.", 'form-text text-danger'
  913. try:
  914. dummy_target = observation.Source(convert_colon_coord(source_coord), 'Source')
  915. return '', dash.no_update
  916. except ValueError as e:
  917. try:
  918. dummy_target = coord.get_icrs_coordinates(source_coord)
  919. return dummy_target.to_string('hmsdms'), 'form-text text-muted'
  920. except coord.name_resolve.NameResolveError as e:
  921. return "Unrecognized name. Use 'hh:mm:ss dd:mm:ss' or 'XXhXXmXXs XXdXXmXXs'", \
  922. 'form-text text-danger'
  923. except ValueError as e:
  924. return "Wrong coordinates.", 'form-text text-danger'
  925. else:
  926. return '', dash.no_update
  927. @app.callback([Output('loading-output', 'children'),
  928. Output('loading-output2', 'children'),
  929. Output('main-window', 'hidden'),
  930. Output('main-window2', 'hidden'),
  931. Output('sensitivity-output', 'children'),
  932. Output('fig-elev-time', 'figure'),
  933. Output('fig-ant-time', 'figure'),
  934. Output('fig-uvplane', 'figure'),
  935. Output('fig-dirtymap', 'figure'),
  936. Output('global-error', 'message'),
  937. Output('tabs', 'value'),
  938. Output('tab-summary', 'disabled'),
  939. Output('tab-elevation', 'disabled'),
  940. Output('tab-uv', 'disabled')],
  941. [Input('antenna-selection-button', 'n_clicks')],
  942. [State('band', 'value'),
  943. State('starttime', 'date'),
  944. State('starthour', 'value'),
  945. State('duration', 'value'),
  946. State('source', 'value'),
  947. State('onsourcetime', 'value'),
  948. State('datarate', 'value'),
  949. State('subbands', 'value'),
  950. State('channels', 'value'),
  951. State('pols', 'value'),
  952. State('inttime', 'value'),
  953. State('timeselection', 'value'),
  954. State('tabs', 'value')] + \
  955. [State(f"check_{s.codename}", 'checked') for s in all_antennas])
  956. def compute_observation(n_clicks, band, starttime, starthour, duration, source, onsourcetime,
  957. datarate, subbands, channels, pols, inttime, epoch_selected, selected_tab, *ants):
  958. """Computes all products to be shown concerning the set observation.
  959. """
  960. # To decide where to put the output message
  961. out_center = selected_tab == 'tab-setup' or selected_tab == 'tab-doc'
  962. if n_clicks is None:
  963. return '', '', dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  964. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  965. dash.no_update, dash.no_update
  966. if epoch_selected:
  967. # All options must be completed
  968. if None in (band, starttime, starthour, duration, source, datarate, subbands, channels, pols, inttime) \
  969. or source == "":
  970. missing = [label for label,atr in zip(('observing band', 'target source', 'start observing date',
  971. 'start observing time', 'duration of the observation', 'data rate', 'number of subbands',
  972. 'number of channels', 'number of polarizations', 'integration time'), (band, source,
  973. starttime, starthour, duration, datarate, subbands, channels, pols, inttime)) \
  974. if (atr is None) or (atr == "")]
  975. temp = [alert_message(["Complete all fields and options before computing the observation.\n" + \
  976. f"Currently it is missing: {', '.join(missing)}."]), '']
  977. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  978. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  979. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  980. else:
  981. # All options but the ones related to the observing epoch must be completed
  982. if None in (band, source, datarate, subbands, channels, pols, inttime) or source == "":
  983. missing = [label for label,atr in zip(('observing band', 'target source', 'data rate',
  984. 'number of subbands', 'number of channels', 'number of polarizations',
  985. 'integration time'), (band, source, starttime, starthour, duration, datarate, subbands,
  986. channels, pols, inttime)) if (atr is None) or (atr == "")]
  987. temp = [alert_message(["Complete all fields and options before computing the observation.\n" + \
  988. f"Currently it is missing: {', '.join(missing)}."]), '']
  989. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  990. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  991. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  992. if ants.count(True) == 0:
  993. temp = [alert_message(["You need to select the antennas you wish to observe your source. " \
  994. "Either manually or by selected a default VLBI network at your top left."]), '']
  995. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  996. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  997. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  998. # A single antenna computation is not supported
  999. if ants.count(True) == 1:
  1000. temp = [alert_message(["Single-antenna computations are not suported. " \
  1001. "Please choose at least two antennas"]), dash.no_update]
  1002. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1003. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1004. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1005. if datarate <= 0:
  1006. temp = [alert_message(["You need to select the data rate for the observation. "]), dash.no_update]
  1007. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1008. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1009. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1010. try:
  1011. target_source = observation.Source(convert_colon_coord(source), 'Source')
  1012. except ValueError as e:
  1013. try:
  1014. target_source = observation.Source(coord.get_icrs_coordinates(source), source)
  1015. except coord.name_resolve.NameResolveError as e:
  1016. temp = [alert_message(["Wrong source name or coordinates.", html.Br(),
  1017. "Either the source name hasn't been found or the coordinates format is incorrect."]), '']
  1018. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1019. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1020. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1021. except ValueError as e:
  1022. temp = [alert_message(["Wrong source name or coordinates.", html.Br(),
  1023. "Either the source name hasn't been found or the coordinates format is incorrect."]), '']
  1024. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1025. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1026. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1027. if not epoch_selected:
  1028. try:
  1029. utc_times, _ = observation.Observation.guest_times_for_source(target_source,
  1030. stations.Stations('Observation', itertools.compress(all_antennas, ants)))
  1031. except observation.SourceNotVisible:
  1032. temp = [alert_message([
  1033. html.P(["Your source cannot be observed within the arranged observation.",
  1034. html.Br(),
  1035. "There are no antennas that can simultaneously observe your source "
  1036. "during the given observing time."]),
  1037. html.P("Modify the observing time or change the selected antennas"
  1038. " to observe this source.")], title="Warning!"), '']
  1039. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1040. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1041. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1042. obs_times = utc_times[0] + np.linspace(0, (utc_times[1]-utc_times[0]).to(u.min).value, 50)*u.min
  1043. else:
  1044. try:
  1045. time0 = Time(dt.strptime(f"{starttime} {starthour}", '%Y-%m-%d %H:%M'),
  1046. format='datetime', scale='utc')
  1047. except ValueError as e:
  1048. temp = [alert_message("Incorrect format for the start time of the observation."), '']
  1049. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1050. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1051. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1052. if duration <= 0.0:
  1053. temp = [alert_message("The duration of the observation must be a positive number of hours."), '']
  1054. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1055. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1056. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1057. if duration > 4*24.0:
  1058. temp = [alert_message("Please, set an observation that lasts for less than 4 days."), '']
  1059. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1060. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1061. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1062. obs_times = time0 + np.linspace(0, duration*60, 50)*u.min
  1063. try:
  1064. obs = observation.Observation(target=target_source, times=obs_times, band=band,
  1065. datarate=datarate, subbands=subbands, channels=channels,
  1066. polarizations=pols, inttime=inttime, ontarget=onsourcetime/100.0,
  1067. stations=stations.Stations('Observation',
  1068. itertools.compress(all_antennas, ants)))
  1069. sensitivity_results = update_sensitivity(obs)
  1070. except observation.SourceNotVisible:
  1071. temp = [alert_message([
  1072. html.P(["Your source cannot be observed within the arranged observation.",
  1073. html.Br(),
  1074. "There are no antennas that can simultaneously observe your source "
  1075. "during the given observing time."]),
  1076. html.P("Modify the observing time or change the selected antennas"
  1077. " to observe this source.")], title="Warning!"), '']
  1078. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1079. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1080. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1081. except Exception as e:
  1082. # Let's print into the STDOUT the current state for later debugging...
  1083. print("--- UNEXPECTED ERROR")
  1084. print("When creating the observation or updating the sensitivity results. Current state:")
  1085. print(f"- target_source: {target_source}")
  1086. print(f"- obs_times: {obs_times}")
  1087. print(f"- band: {band}")
  1088. print(f"- datarate: {datarate}")
  1089. print(f"- subbands: {subbands}")
  1090. print(f"- channels: {channels}")
  1091. print(f"- polarizations: {pols}")
  1092. print(f"- inttime: {inttime}")
  1093. print(f"- onsourcetime: {onsourcetime/100.0}")
  1094. print(f"- stations: {ants}")
  1095. temp = [alert_message([f"Unknown Error: ({e}).", html.Br(), \
  1096. "Please, refresh and try again. Contact 'marcote (at) jive.eu' in case of further issues"]), '']
  1097. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1098. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1099. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1100. try:
  1101. with mp.Pool() as pool:
  1102. output_figs = pool.map(smap,
  1103. [functools.partial(get_fig_ant_elev, obs),
  1104. functools.partial(get_fig_ant_up, obs),
  1105. functools.partial(get_fig_uvplane, obs),
  1106. functools.partial(get_fig_dirty_map, obs)])
  1107. except Exception as e:
  1108. # Let's print into the STDOUT the current state for later debugging...
  1109. print("--- UNEXPECTED ERROR")
  1110. print("When creating the plots. Current state for obs {obs}:")
  1111. print(f"- target_source: {target_source}")
  1112. print(f"- obs_times: {obs_times}")
  1113. print(f"- band: {band}")
  1114. print(f"- datarate: {datarate}")
  1115. print(f"- subbands: {subbands}")
  1116. print(f"- channels: {channels}")
  1117. print(f"- polarizations: {pols}")
  1118. print(f"- inttime: {inttime}")
  1119. print(f"- onsourcetime: {onsourcetime/100.0}")
  1120. print(f"- stations: {ants}")
  1121. temp = [alert_message([f"Unknown Error: ({e}).", html.Br(), \
  1122. "Please, refresh and try again. Contact 'marcote (at) jive.eu' in case of further issues"]), '']
  1123. return *[temp if out_center else temp[::-1]][0], dash.no_update, dash.no_update, dash.no_update, \
  1124. dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, \
  1125. dash.no_update, dash.no_update, dash.no_update, dash.no_update
  1126. if out_center:
  1127. return dbc.Alert("Results have been updated.", color='info', dismissable=True), '', False, True, \
  1128. sensitivity_results, *list(output_figs), dash.no_update, \
  1129. 'tab-summary', False, False, False
  1130. else:
  1131. return '', dbc.Alert("Results have been updated.", color='info', dismissable=True), False, True, \
  1132. sensitivity_results, *list(output_figs), dash.no_update, \
  1133. dash.no_update, False, False, False
  1134. def get_fig_ant_elev(obs):
  1135. data_fig = []
  1136. data_dict = obs.elevations()
  1137. # Some reference lines at low elevations
  1138. for ant in data_dict:
  1139. data_fig.append({'x': obs.times.datetime, 'y': data_dict[ant].value,
  1140. 'mode': 'lines', 'hovertemplate': "Elev: %{y:.2n}º<br>%{x}",
  1141. 'name': obs.stations[ant].name})
  1142. data_fig.append({'x': obs.times.datetime, 'y': np.zeros_like(obs.times)+10,
  1143. 'mode': 'lines', 'hoverinfo': 'skip', 'name': 'Elev. limit 10º',
  1144. 'line': {'dash': 'dash', 'opacity': 0.5, 'color': 'gray'}})
  1145. data_fig.append({'x': np.unwrap(obs.gstimes.value*2*np.pi/24)*24/(2*np.pi), 'y': np.zeros_like(obs.times)+20,
  1146. 'xaxis': 'x2', 'mode': 'lines', 'hoverinfo': 'skip',
  1147. 'name': 'Elev. limit 20º', 'line': {'dash': 'dot', 'opacity': 0.5,
  1148. 'color': 'gray'}})
  1149. return {'data': data_fig,
  1150. 'layout': {'title': 'Source elevation during the observation',
  1151. 'hovermode': 'closest',
  1152. 'xaxis': {'title': 'Time (UTC)', 'showgrid': False,
  1153. 'ticks': 'inside', 'showline': True, 'mirror': False,
  1154. 'hovermode': 'closest', 'color': 'black'},
  1155. 'xaxis2': {'title': {'text': 'Time (GST)', 'standoff': 0},
  1156. 'showgrid': False, 'overlaying': 'x', #'dtick': 1.0,
  1157. 'tickvals': np.arange(np.ceil(obs.gstimes.value[0]),
  1158. np.floor(np.unwrap(obs.gstimes.value*2*np.pi/24)[-1]*24/(2*np.pi))+1),
  1159. 'ticktext': np.arange(np.ceil(obs.gstimes.value[0]),
  1160. np.floor(np.unwrap(obs.gstimes.value*2*np.pi/24)[-1]*24/(2*np.pi))+1)%24,
  1161. 'ticks': 'inside', 'showline': True, 'mirror': False,
  1162. 'hovermode': 'closest', 'color': 'black', 'side': 'top'},
  1163. 'yaxis': {'title': 'Elevation (degrees)', 'range': [0., 92.],
  1164. 'ticks': 'inside', 'showline': True, 'mirror': "all",
  1165. 'showgrid': False, 'hovermode': 'closest'},
  1166. 'zeroline': True, 'zerolinecolor': 'k'}}
  1167. def get_fig_ant_up(obs):
  1168. data_fig = []
  1169. data_dict = obs.is_visible()
  1170. gstimes = np.unwrap(obs.gstimes.value*2*np.pi/24)*24/(2*np.pi)
  1171. gstimes = np.array([dt(obs.times.datetime[0].year, obs.times.datetime[0].month, obs.times.datetime[0].day) \
  1172. + datetime.timedelta(seconds=gst*3600) for gst in gstimes])
  1173. for i,ant in enumerate(data_dict):
  1174. # xs = [obs.times.datetime[0].date() + datetime.timedelta(seconds=i*3600) for i in np.unwrap(obs.gstimes.value*2*np.pi/24)[data_dict[ant]]*24/(2*np.pi)]
  1175. xs = gstimes[data_dict[ant]]
  1176. data_fig.append({'x': xs,
  1177. 'y': np.zeros_like(data_dict[ant][0])-i, 'type': 'scatter',
  1178. 'hovertemplate': "GST %{x}",
  1179. 'mode': 'markers', 'marker_symbol': "41",
  1180. 'hoverinfo': "skip",
  1181. 'name': obs.stations[ant].name})
  1182. data_fig.append({'x': obs.times.datetime, 'y': np.zeros_like(obs.times)-0.5,
  1183. 'xaxis': 'x2',
  1184. 'mode': 'lines', 'hoverinfo': 'skip', 'showlegend': False,
  1185. 'line': {'dash': 'dot', 'opacity': 0.0, 'color': 'white'}})
  1186. return {'data': data_fig,
  1187. 'layout': {'title': {'text': 'Source visible during the observation',
  1188. 'y': 1, 'yanchor': 'top'},
  1189. 'hovermode': 'closest',
  1190. 'xaxis': {'title': 'Time (GST)', 'showgrid': False,
  1191. 'range': [gstimes[0], gstimes[-1]],
  1192. # 'tickvals': np.arange(np.ceil(obs.gstimes.value[0]),
  1193. # np.floor(np.unwrap(obs.gstimes.value*2*np.pi/24)[-1]*24/(2*np.pi))+1),
  1194. # 'ticktext': np.arange(np.ceil(obs.gstimes.value[0]),
  1195. # np.floor(np.unwrap(obs.gstimes.value*2*np.pi/24)[-1]*24/(2*np.pi))+1)%24,
  1196. 'tickformat': '%H:%M',
  1197. 'ticks': 'inside', 'showline': True, 'mirror': False,
  1198. 'hovermode': 'closest', 'color': 'black'},
  1199. 'xaxis2': {'title': {'text': 'Time (UTC)', 'standoff': 0},
  1200. 'showgrid': False, 'overlaying': 'x', #'dtick': 1.0,
  1201. 'ticks': 'inside', 'showline': True, 'mirror': False,
  1202. 'hovermode': 'closest', 'color': 'black', 'side': 'top'},
  1203. 'yaxis': {'ticks': '', 'showline': True, 'mirror': True,
  1204. 'showticklabels': False, 'zeroline': False,
  1205. 'showgrid': False, 'hovermode': 'closest',
  1206. 'startline': False}}}
  1207. data_fig = []
  1208. data_dict = obs.is_visible()
  1209. for i,ant in enumerate(data_dict):
  1210. data_fig.append({'x': obs.times.datetime[data_dict[ant]],
  1211. 'y': np.zeros_like(data_dict[ant][0])-i, 'type': 'scatter',
  1212. 'hovertemplate': "%{x}",
  1213. 'mode': 'markers', 'marker_symbol': "41",
  1214. 'hoverinfo': "skip",
  1215. 'name': obs.stations[ant].name})
  1216. data_fig.append({'x': np.unwrap(obs.gstimes.value), 'y': np.zeros_like(obs.times)-0.5,
  1217. 'xaxis': 'x2',
  1218. 'mode': 'lines', 'hoverinfo': 'skip', 'showlegend': False,
  1219. 'line': {'dash': 'dot', 'opacity': 0.0, 'color': 'white'}})
  1220. return {'data': data_fig,
  1221. 'layout': {'title': 'Source visible during the observation',
  1222. 'xaxis': {'title': 'Time (UTC)', 'showgrid': False,
  1223. 'ticks': 'inside', 'showline': True, 'mirror': False,
  1224. 'hovermode': 'closest', 'color': 'black'},
  1225. 'xaxis2': {'title': {'text': 'Time (GST)', 'standoff': 0},
  1226. 'showgrid': False, 'overlaying': 'x', #'dtick': 1.0,
  1227. 'tickvals': np.arange(np.ceil(obs.gstimes.value[0]),
  1228. np.floor(np.unwrap(obs.gstimes.value)[-1])+1),
  1229. 'ticktext': np.arange(np.ceil(obs.gstimes.value[0]),
  1230. np.floor(np.unwrap(obs.gstimes.value)[-1])+1) % 24,
  1231. 'ticks': 'inside', 'showline': True, 'mirror': False,
  1232. 'hovermode': 'closest', 'color': 'black', 'side': 'top'},
  1233. 'yaxis': {'ticks': '', 'showline': True, 'mirror': True,
  1234. 'showticklabels': False, 'zeroline': False,
  1235. 'showgrid': False, 'hovermode': 'closest',
  1236. 'startline': False}}}
  1237. def get_fig_uvplane(obs):
  1238. data_fig = []
  1239. bl_uv = obs.get_uv_baseline()
  1240. for bl_name in bl_uv:
  1241. # accounting for complex conjugate
  1242. uv = np.empty((2*len(bl_uv[bl_name]), 2))
  1243. uv[:len(bl_uv[bl_name]), :] = bl_uv[bl_name]
  1244. uv[len(bl_uv[bl_name]):, :] = -bl_uv[bl_name]
  1245. data_fig.append({'x': uv[:,0],
  1246. 'y': uv[:,1],
  1247. # 'type': 'scatter', 'mode': 'lines',
  1248. 'type': 'scatter', 'mode': 'markers',
  1249. 'marker': {'symbol': '.', 'size': 2},
  1250. 'name': bl_name, 'hovertext': bl_name, 'hoverinfo': 'name', 'hovertemplate': ''})
  1251. return {'data': data_fig,
  1252. 'layout': {'title': '', 'showlegend': False,
  1253. 'hovermode': 'closest',
  1254. 'width': 700, 'height': 700,
  1255. 'xaxis': {'title': 'u (lambda)', 'showgrid': False, 'zeroline': False,
  1256. 'ticks': 'inside', 'showline': True, 'mirror': "all",
  1257. 'color': 'black'},
  1258. 'yaxis': {'title': 'v (lambda)', 'showgrid': False, 'scaleanchor': 'x',
  1259. 'ticks': 'inside', 'showline': True, 'mirror': "all",
  1260. 'color': 'black', 'zeroline': False}}}
  1261. def get_fig_dirty_map(obs):
  1262. dirty_map_nat, laxis = obs.get_dirtymap(pixsize=1024, robust='natural', oversampling=4)
  1263. dirty_map_uni, laxis = obs.get_dirtymap(pixsize=1024, robust='uniform', oversampling=4)
  1264. fig1 = px.imshow(img=dirty_map_nat, x=laxis, y=laxis[::-1], labels={'x': 'RA (mas)', 'y': 'Dec (mas)'}, \
  1265. aspect='equal')
  1266. fig2 = px.imshow(img=dirty_map_uni, x=laxis, y=laxis[::-1], labels={'x': 'RA (mas)', 'y': 'Dec (mas)'}, \
  1267. aspect='equal')
  1268. fig = make_subplots(rows=1, cols=2, subplot_titles=('Natural weighting', 'Uniform weighting'),
  1269. shared_xaxes=True, shared_yaxes=True)
  1270. fig.add_trace(fig1.data[0], row=1, col=1)
  1271. fig.add_trace(fig2.data[0], row=1, col=2)
  1272. mapsize = 30*obs.synthesized_beam()['bmaj'].to(u.mas).value
  1273. fig.update_layout(coloraxis={'showscale': False, 'colorscale': 'Inferno'}, showlegend=False,
  1274. xaxis={'autorange': False, 'range': [mapsize, -mapsize]},
  1275. # This xaxis2 represents the xaxis for fig2.
  1276. xaxis2={'autorange': False, 'range': [mapsize, -mapsize]},
  1277. yaxis={'autorange': False, 'range': [-mapsize, mapsize]}, autosize=False)
  1278. fig.update_xaxes(title_text="RA (mas)", constrain="domain")
  1279. fig.update_yaxes(title_text="Dec (mas)", row=1, col=1, scaleanchor="x", scaleratio=1)
  1280. return fig
  1281. ##################### This is the webpage layout
  1282. app.layout = html.Div([
  1283. html.Div(id='banner', className='navbar-brand d-flex p-3 shadow-sm', children=[
  1284. html.A(className='d-inline-block mr-md-auto', href="https://www.evlbi.org", children=[
  1285. html.Img(height='70px', src=app.get_asset_url("logo_evn.png"),
  1286. alt='European VLBI Network (EVN)',
  1287. className="d-inline-block align-top"),
  1288. ]),
  1289. html.H2('EVN Observation Planner', className='d-inline-block align-middle mx-auto'),
  1290. html.A(className='d-inline-block ml-auto pull-right', href="https://www.jive.eu", children=[
  1291. html.Img(src=app.get_asset_url("logo_jive.png"), height='70px',
  1292. alt='Joinst Institute for VLBI ERIC (JIVE)')
  1293. ])
  1294. ]),
  1295. html.Div([html.Br(), html.Br()]),
  1296. # html.Div(id='full-window', children=html.Div(id='main-window', children=main_page(False)))])
  1297. html.Div(id='full-window', children=initial_page())])
  1298. if __name__ == '__main__':
  1299. # app.run_server(host='0.0.0.0', debug=True)
  1300. # app.run_server(debug=True)
  1301. app.run_server(host='0.0.0.0', debug=True)