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.

1429 lines
74 KiB

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""EVN Observation Planner.
Program to compute the source elevation visibility
and expected thermal noise level for a given EVN observation.
"""
__author__ = "Benito Marcote"
__credits__ = "Benito Marcote"
__license__ = "LGPLv3+"
__date__ = "2020/10/26"
__version__ = "1.0.1"
__maintainer__ = "Benito Marcote"
__email__ = "marcote@jive.eu"
__status__ = "Production" # Prototype, Development, Production.
import os
from os import path
from time import sleep
import itertools
import functools
from importlib import resources
import multiprocessing as mp
import datetime
# import time
from datetime import datetime as dt
import numpy as np
import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from astropy.time import Time
from astropy import coordinates as coord
from astropy import units as u
## THIS WILL NEED TO GO AWAY IN THE NEW VERSION OF ASTROPY, WHICH IS STILL NOT
## SUPPORTED BY THE CURRENT VERSION OF ASTROPLAN
# Tweak to not let astroplan crashing...
# from astropy.utils.data import clear_download_cache
# from astropy.utils import iers
# clear_download_cache() # to be sure it is really working
#
# iers.conf.auto_download = False
# iers.conf.iers_auto_url = None
# iers.conf.auto_max_age = None
# iers.conf.remote_timeout = 100.0
# iers.conf.download_cache_lock_attempts = 10
from astroplan import FixedTarget
current_directory = path.dirname(path.realpath(__file__))
if path.isfile(current_directory + '/.astropy/cache/download/py3/lock'):
os.remove(current_directory + '/.astropy/cache/download/py3/lock')
######### All the previous part will be removed with astropy 4.1+ and astroplan 0.7+
from vlbiplanobs import freqsetups as fs
from vlbiplanobs import stations
from vlbiplanobs import observation
from vlbiplanobs import graphical_elements as ge
# adding the possibility of disabled. Will be implemented in a future version of dash_bootstrap_components
from vlbiplanobs.Checkbox import Checkbox
all_antennas = stations.Stations.get_stations_from_configfile()
default_arrays = stations.Stations.get_network_names_from_configfile()
sorted_networks = {'EVN': 'EVN: European VLBI Network', 'eMERLIN': 'eMERLIN (out-stations)',
'VLBA': 'VLBA: Very Long Baseline Array',
'LBA': 'LBA: Australian Long Baseline Array',
'KVN': 'KVN: Korean VLBI Network',
'Other': 'Other antennas',
'Decom': 'Decommissioned antennas'}
# Safety check that all these antennas are available in the file
for a_array in default_arrays:
for a_station in default_arrays[a_array]['default_antennas']:
assert a_station in all_antennas.codenames
doc_files = {'About the EVN Observation Planner': 'doc-contact.md',
'About the antennas': 'doc-antennas.md',
'Technical background': 'doc-estimations.md'}
selected_band = '6cm'
obs = observation.Observation()
external_stylesheets = []
external_scripts = ["https://kit.fontawesome.com/69c65a0ab5.js"]
app = dash.Dash(__name__, title='EVN Observation Planner', external_scripts=external_scripts,
assets_folder=current_directory+'/assets/')
app.config.suppress_callback_exceptions = True # Avoids error messages for id's that haven't been loaded yet
server = app.server
def smap(f):
return f()
def get_doc_text():
"""Reads the doc files and returns it as a Div object.
"""
temp = []
for i,a_topic in enumerate(doc_files):
with resources.open_text("doc", doc_files[a_topic]) as f:
# Some files will have references to images/files in the form '{src:filename}'
# We parse this
parsed_text = f.read()
while '{src:' in parsed_text:
i0 = parsed_text.index('{src:')
i1 = i0 + parsed_text[i0:].index('}')
filename = parsed_text[i0+5:i1]
parsed_text = parsed_text.replace( parsed_text[i0:i1+1],
app.get_asset_url(filename) )
if a_topic == 'About the antennas':
temp += [ge.create_accordion_card(a_topic,
1 year ago
[dcc.Markdown(parsed_text), ge.antenna_cards(app, all_antennas)], id=str(i), is_open=False)]
else:
temp += [ge.create_accordion_card(a_topic, dcc.Markdown(parsed_text),
1 year ago
id=str(i), is_open=False)]
return html.Div(temp, className='col-12 accordion shadow-1-strong')
@app.callback([Output(f"collapse-{i}", "is_open") for i in range(len(doc_files))],
[Input(f"group-{i}-toggle", "n_clicks") for i in range(len(doc_files))],
[State(f"collapse-{i}", "is_open") for i in range(len(doc_files))])
def toggle_accordion(*args):
"""Allows the expansion/collapse of an HTML accordion block.
"""
defaults = list(args[len(doc_files):])
ctx = dash.callback_context
if not ctx.triggered:
return [dash.no_update]*len(doc_files)
else:
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
for i in range(len(doc_files)):
if (button_id == f"group-{i}-toggle") and (args[i] is not None):
defaults[i] = not defaults[i]
return defaults
def error_text(an_error):
"""Standard error message written in a modal error window.
It returns a str mentioning 'an_error' and the contact details to report it.
"""
return f"An error occured.\n{an_error}.\nPlease report to marcote@jive.eu " \
"or in https://github.com/bmarcote/vlbi_calculator."
def convert_colon_coord(colon_coord):
"""Converts some coordinates given in a str format 'HH:MM:SS DD:MM:SS' to
'HHhMMmSSs DDdMMdSSs'.
If ':' are not present in colon_coord, then it returns the same str.
"""
if ':' not in colon_coord:
return colon_coord
for l in ('h', 'm', 'd', 'm'):
colon_coord = colon_coord.replace(':', l, 1)
return ' '.join([f"{s}s" for s in colon_coord.split()])
def alert_message(message, title="Warning!"):
"""Produces an alert-warning message.
'message' can be either a string or a list with different string/dash components.
"""
if type(message) == str:
return [html.Br(), \
dbc.Alert([html.H4(title, className='alert-heading'), message], \
color='warning', dismissable=True)]
else:
return [html.Br(), \
dbc.Alert([html.H4(title, className='alert-heading'), *message], \
color='warning', dismissable=True)]
def update_sensitivity(obs):
"""Given the observation, it sets the text for all summary cards
with information about the observation.
"""
cards = []
cards += ge.summary_card_times(app, obs)
cards += ge.summary_card_frequency(app, obs)
cards += ge.summary_card_antennas(app, obs)
cards += ge.summary_card_beam(app, obs)
cards += ge.summary_card_rms(app, obs)
cards += ge.summary_card_fov(app, obs)
return [html.Div(className='card-deck col-12 justify-content-center',
children=cards),
html.Br(),
html.Div(style={'height': '5rem'}),
html.Div(className='col-12 justify-content-center',
children=ge.summary_card_worldmap(app, obs))]
def arrays_with_band(arrays, a_band):
"""Returns the arrays that can observe the given band with at least two antennas.
It excludes e-EVN if it is included in arrays.
Inputs
- arrays : dict
The keys are the name of the array and the values must be a dict with the codenames
of the antennas in an array inside the value 'default_antennas'.
- a_band : str
The band to be observed, following the criteria in fs.bands.
Returns
- arrays_with_band : str
Comma-separated list of the arrays that can observe the given band.
"""
tmp = [] # the list of arrays that can observe the given band
for an_array in arrays:
if an_array != 'e-EVN':
if a_band in arrays[an_array]['observing_bands']:
tmp.append(an_array)
if len(tmp) == 0:
return 'none'
elif len(tmp) == 2:
return ' and '.join(tmp)
elif len(tmp) in (1, 3):
return ', '.join(tmp)
else: # >= 4
return ', '.join(tmp[:-1]) + ' and ' + tmp[-1]
@app.callback(Output('initial-pickband-label', 'children'),
[Input('initial-band', 'value')])
def update_pickband_tooltip(a_wavelength):
a_band = tuple(fs.bands)[a_wavelength]
return [dbc.Card(dbc.CardBody([
html.H5([html.Img(height='30rem',
src=app.get_asset_url(f"waves-{a_band.replace('.', '_')}.png"),
alt='Band: ', className='d-inline-block'),
html.Span(f"{fs.bands[a_band].split('(')[0].strip()}",
style={'float': 'right'})
], className="card-title"),
html.P([html.Span("Wavelength: ", style={'color': '#888888'}),
f"{fs.bands[a_band].split('(')[1].split('or')[0].strip()}.",
html.Br(),
html.Span("Frequency: ", style={'color': '#888888'}),
f"{fs.bands[a_band].split('(')[1].split('or')[1].replace(')', '').strip()}.",
html.Br(),
html.Span(html.Small(f"Can be observed with the {arrays_with_band(default_arrays, a_band)}."),
style={'color': '#888888'})
], className="card-text"),
]), className="col-sm-3 my-2 shadow-1-strong")
]
# @app.callback([Output('initial-timeselection-div-guess', 'hidden'),
# Output('initial-timeselection-div-epoch', 'hidden')],
# [Input('initial-timeselection', 'value')])
# def type_initial_time_selection(time_selection_selected):
# """Modifies the hidden message related to the two options about how to pick the observing time.
# """
# print('checked')
# return [time_selection_selected, not time_selection_selected]
@app.callback([Output('initial-timeselection-div-guess', 'hidden'),
Output('initial-timeselection-div-epoch', 'hidden')],
[Input('initial-timeselection', 'value')])
def type_initial_time_selection(time_selection_selected):
"""Modifies the hidden message related to the two options about how to pick the observing time.
"""
return [time_selection_selected, not time_selection_selected]
@app.callback([Output('timeselection-div-guess', 'hidden'),
Output('timeselection-div-epoch', 'hidden')],
[Input('timeselection', 'value')])
def type_time_selection(time_selection_selected):
"""Modifies the hidden message related to the two options about how to pick the observing time.
"""
return [time_selection_selected, not time_selection_selected]
@app.callback(Output('band', 'value'),
Input('initial-band', 'value'), prevent_initial_call=True)
def band_from_initial(initial_value):
return tuple(fs.bands)[initial_value] if initial_value is not None else dash.no_update
@app.callback(Output('array', 'value'),
[Input(f'network-{network.lower()}', 'value') for network in default_arrays if network != 'e-EVN'],
prevent_initial_call=True)
def array_from_initial(*selected_networks):
return [network for (network,selected) in \
zip([n for n in default_arrays if n != 'e-EVN'], selected_networks) if selected]
@app.callback(Output('e-EVN', 'value'),
Input('initial-e-EVN', 'value'), prevent_initial_call=True)
def e_EVN_from_initial(initial_value):
return initial_value if initial_value is not None else dash.no_update
@app.callback(Output('timeselection', 'value'),
Input('initial-timeselection', 'value'), prevent_initial_call=True)
def timeselection_from_initial(initial_value):
return initial_value if initial_value is not None else dash.no_update
@app.callback(Output('starttime', 'date'),
Input('initial-starttime', 'date'), prevent_initial_call=True)
def starttime_from_initial(initial_value):
return initial_value if initial_value is not None else dash.no_update
@app.callback(Output('starthour', 'value'),
Input('initial-starthour', 'value'), prevent_initial_call=True)
def starthour_from_initial(initial_value):
return initial_value if initial_value is not None else dash.no_update
@app.callback(Output('duration', 'value'),
Input('initial-duration', 'value'), prevent_initial_call=True)
def duration_from_initial(initial_value):
return initial_value if initial_value is not None else dash.no_update
@app.callback(Output('source', 'value'),
Input('initial-source', 'value'), prevent_initial_call=True)
def source_from_initial(initial_value):
return initial_value if initial_value is not None else dash.no_update
@app.callback([Output('subbands', 'value'),
Output('channels', 'value'),
Output('pols', 'value'),
Output('inttime', 'value')],
Input('is_line', 'value'), prevent_initial_call=True)
def line_cont_setup(is_line_exp):
if is_line_exp is None:
return dash.no_update, dash.no_update, dash.no_update, dash.no_update
if is_line_exp:
return 1, 4096, 4, 2
else:
return 8, 32, 4, 2
@app.callback([Output('button-picknetwork', 'disabled'),
Output('button-picknetwork', 'children')],
[Input(f"network-{network.lower()}", 'value') for network in default_arrays if network != 'e-EVN'])
def continue_from_networks(*networks):
"""Verifies that the user has selected at least one VLBI network during the wizard screen
"""
for n in networks:
if True in n:
return False, 'Continue'
return True, 'Select network(s) to continue'
# len([True for n in networks if True in n]) > 0 % Slower
@app.callback([Output('button-pickband', 'disabled'),
Output('button-pickband', 'children')],
Input('initial-band', 'value'),
[State(f"network-{network.lower()}", 'value') for network in default_arrays if network != 'e-EVN'])
def continue_from_band(selected_band, *networks):
"""Verifies that the selected band can be observed by the given network.
"""
for n,nname in zip(networks, [network for network in default_arrays if network != 'e-EVN']):
if True in n:
if tuple(fs.bands.keys())[selected_band] in default_arrays[nname]['observing_bands']:
return False, 'Continue'
return True, 'The selected network cannot observe at this band'
@app.callback([Output('button-picktimes', 'disabled'),
Output('button-picktimes', 'children')],
[Input('initial-timeselection', 'value'),
Input('initial-starttime', 'date'),
Input('initial-starthour', 'value'),
Input('initial-duration', 'value'),
Input('initial-source', 'value')])
def continue_from_times(time_selection, time_date, time_hour, time_duration, source):
"""Verifies that the user has selected and introduced the required data before continue.
"""
if (source is None) or (not verify_recognized_source(source)):
return True, 'Specify epoch and target before continue'
if time_selection:
if (time_date is not None) and (time_hour is not None) and (time_duration is not None):
try:
dummy = float(time_duration)
return (True, 'Specify epoch and target before continue') if (dummy <= 0) or (dummy > 4*24) \
else (False, 'Continue')
except:
return True, 'Specify epoch and target before continue'
else:
return True, 'Specify epoch and target before continue'
return False, 'Continue'
@app.callback(Output('main-window2', 'children'),
Output('is_line', 'value'),
[Input('button-pickband', 'n_clicks'),
Input('button-picknetwork', 'n_clicks'),
Input('button-picktimes', 'n_clicks'),
Input('button-mode-continuum', 'n_clicks'),
Input('button-mode-line', 'n_clicks')])
def intro_choices(clicks_pickband, clicks_picknetwork, clicks_picktimes, clicks_continuum, clicks_line):
if clicks_picknetwork is not None:
return choice_page('band'), dash.no_update
elif clicks_pickband is not None:
return choice_page('time'), dash.no_update
elif clicks_picktimes is not None:
return choice_page('mode'), dash.no_update
elif clicks_continuum is not None:
return choice_page('final'), False
elif clicks_line is not None:
return choice_page('final'), True
else:
return dash.no_update, dash.no_update
def initial_page():
"""Initial window with the two options to select: guided or manual setup of the observation.
"""
return [
html.Div(className='row justify-content-center', id='main-window2',
children=html.Div(className='col-sm-6 justify-content-center',
children=[html.Div(className='justify-content-center',
children=[#html.H3("Welcome!"),
html.P(["The EVN Observation Planner allows you to plan observations with the ",
html.A(href="https://www.evlbi.org", children="European VLBI Network"),
" (EVN) and other Very Long Baseline Interferometry (VLBI) networks. "
"The EVN Observation Planner helps you to determine when your source "
"can be observed by the different antennas, and provides the expected "
"outcome of these observations, like the expected sensitivity or resolution."]),
html.Br(),
html.Div(ge.initial_window_start(app))
])
])
)]
@app.callback(Output('full-window', 'children'),
[Input('button-initial-wizard', 'n_clicks'),
Input('button-initial-expert', 'n_clicks')])
def choice_for_setup(do_wizard, do_expert):
if (do_expert is not None) or (do_wizard is not None):
return [
# order inverted to improve loading times
html.Div(id='main-window2', hidden=do_expert is not None,
children=[dbc.Checklist(id='is_line', options=[{'label': 'line obs', 'value': False}],
value=[])] if do_expert is not None else choice_page('network')),
html.Div(id='main-window', hidden=do_expert is None,
children=main_page(show_compute_button=do_expert is not None))
]
else:
return dash.no_update
def choice_page(choice_card):
"""Initial window with the introduction to the EVN Observation Planner and the band selection.
"""
return [
html.Div(className='row justify-content-center', id='main-window2',
children=html.Div(className='col-sm-6 justify-content-center',
children=[html.Div(className='justify-content-center',
children=[#html.H3("Welcome!"),
html.P(["The EVN Observation Planner allows you to plan observations with the ",
html.A(href="https://www.evlbi.org", children="European VLBI Network"),
" (EVN) and other Very Long Baseline Interferometry (VLBI) networks. "
"The EVN Observation Planner helps you to determine when your source "
"can be observed by the different antennas, and provides the expected "
"outcome of these observations, like the expected sensitivity or resolution."]),
html.Br(),
*[
# html.Div(hidden=False if choice_card == 'choice' else True,
# children=ge.initial_window_start(app)),
html.Div(hidden=False if choice_card == 'band' else True,
children=ge.initial_window_pick_band()),
html.Div(hidden=False if choice_card == 'network' else True,
children=ge.initial_window_pick_network(app, default_arrays)),
html.Div(hidden=False if choice_card == 'time' else True,
children=ge.initial_window_pick_time()),
html.Div(hidden=False if choice_card == 'mode' else True,
children=ge.initial_window_pick_mode(app)),
html.Div(hidden=False if choice_card == 'final' else True,
children=ge.initial_window_final()),
],
], style={'text:align': 'justify !important'})
])
)]
def main_page(results_visible=False, summary_output=None, fig_elev_output=None,
fig_ant_output=None, fig_uv_output=None, fig_dirty_map_output=False, show_compute_button=True):
return [# First row containing all buttons/options, list of telescopes, and button with text output
dcc.ConfirmDialog(id='global-error', message=''),
# Elements in second column (checkboxes with all stations)
html.Div(className='container-fluid', children=[
html.Div(className='row justify-content-center', children=[
html.Div(className='col-sm-3', style={'max-width': '350px','float': 'left',
1 year ago
'min-width': '17rem'}, children=[
html.Div(className='form-group', children=[
html.Div('', style={'height': '70px'}),
dcc.Loading(id="loading2", children=[html.Div(id="loading-output2"), html.Br()],
type="dot"),
html.Button('Compute observation',
id='antenna-selection-button',
className='btn btn-primary btn-lg',
style={'width': '100%', 'margin-bottom': '1rem'})#if show_compute_button else html.Br(),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Your observing Band',
*ge.tooltip(idname='popover-band',
message="This will update the "
"antenna list showing the ones that can observe "
"at that given frequency.")
]),
dcc.Dropdown(id='band', persistence=True, value='18cm',
options=[{'label': fs.bands[b], 'value': b} for b \
# in fs.bands], value='18cm'),
in fs.bands], placeholder='Select observing band...'),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Real-time correlation?',
*ge.tooltip(idname='popover-eevn',
message="Only available for the EVN: real-time correlation mode."
"The data are transferred and correlated in real-time, but "
"not all telescopes are capable for this and the bandwidth "
"may be limited. Observations during the e-EVN epochs.")
]),
dbc.Checklist(id='e-EVN', className='checkbox', persistence=True,
options=[{'label': ' e-EVN mode',
'value': 'e-EVN'}], value=[]),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Source (name or coordinates)',
*ge.tooltip(idname='popover-target',
message="Source name or coordinates. " \
"You may see an error if the given name is not properly resolved. "
"J2000 coordinates are assumed in both forms: 00:00:00 00:00:00 or " \
"00h00m00s 00d00m00s.")
]),
dcc.Input(id='source', value=None, type='text',
className='form-control', placeholder="hh:mm:ss dd:mm:ss",
persistence=True),
html.Small(id='error_source',
className='form-text text-muted'),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6('Epoch for observation'),
dbc.FormGroup([
dbc.RadioItems(options=[{"label": "I don't have a preferred epoch", "value": False},
{"label": "I know the observing epoch", "value": True}],
value=True, id="timeselection", inline=True, persistence=True),
], inline=True),
html.Div(children=[
html.Div(id='timeselection-div-guess', className='row justify-content-center',
hidden=False, children=[
html.Small("Choose the first option to find out when your source "
"may be visible (by >3 telescopes).", style={'color': '#999999'}),
html.Small("Note that this option may not provide the best (expected) "
"results in case of combining different networks very far apart "
"(e.g. LBA and EVN).", style={'color': '#999999'})
]),
html.Div(id='timeselection-div-epoch', hidden=True, children=[
html.Label('Start of observation (UTC)'),
*ge.tooltip(idname='popover-startime', message="Select the date and "
"time of the start of the observation (Universal, UTC, "
"time). You will also see the day of the year (DOY) in "
"brackets once the date is selected."),
html.Br(),
dcc.DatePickerSingle(id='starttime', date=None, min_date_allowed=dt(1900, 1, 1),
max_date_allowed=dt(2100, 1, 1),
display_format='DD-MM-YYYY (DDD)',
placeholder='Start date',
first_day_of_week=1,
initial_visible_month=dt.today(),
persistence=True,
className='form-picker'),
dcc.Dropdown(id='starthour', placeholder="Start time (UTC)", value=None,
options=[{'label': f"{hm//60:02n}:{hm % 60:02n}", \
'value': f"{hm//60:02n}:{hm % 60:02n}"} \
for hm in range(0, 24*60, 15)],
persistence=True, className='form-hour'),
html.Small(id='error_starttime', style={'color': 'red'},
className='form-text text-muted'),
html.Label('Duration of the observation (in hours)'),
html.Div(className='form-group', children=[
dcc.Input(id='duration', value=None, type='number', className='form-control',
placeholder="Duration in hours", persistence=True, inputMode='numeric'),
html.Small(id='error_duration', className='form-text text-danger')
])
])
]),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['% of on-target time',
*ge.tooltip(idname='popover-ontarget',
message="Assumes that you will only spend this amount of the total " \
"observing time on the given target source. It affects the " \
"expected sensitivity."),
]),
dcc.Slider(id='onsourcetime', min=20, max=100, step=5, value=70,
marks= {i: str(i) for i in range(20, 101, 10)},
persistence=True),
html.Label(id='onsourcetime-label', style={'color': '#999999'},
children='70% of the observation.'),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Datarate per station',
*ge.tooltip(idname='popover-datarate',
message=["Expected datarate for each station, assuming all " \
"of them run at the same rate.",
html.Ul([
html.Li("The EVN can run typically at up to 2 Gbps (1 Gbps at L band), " \
"although a few antennas may observe at lower datarates."),
html.Li("The VLBA can now observe up to 4 Gbps."),
html.Li("The LBA typically runs at 512 Mbps but can reach up to 1 Gbps."),
html.Li("Check the documentation from other networks to be " \
"sure about their capabilities.")])])
]),
dcc.Dropdown(id='datarate',
placeholder="Select the data rate...",
options=[{'label': fs.data_rates[dr], 'value': dr} \
for dr in fs.data_rates], value=2048, persistence=True),
html.Label(id='bandwidth-label', style={'color': '#999999'}, children='')
]),
html.Div(className='form-group', children=[
html.H6(['Number of subbands',
*ge.tooltip(idname='popover-subbands',
message="Number of subbands to split the total observed bandwidth "
" during correlation (IFs in AIPS).")
]),
dcc.Dropdown(id='subbands', placeholder="Select no. subbands...",
options=[{'label': fs.subbands[sb], 'value': sb} \
for sb in fs.subbands], value=8, persistence=True),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Number of spectral channels',
*ge.tooltip(idname='popover-channels',
message="How many channels per subband will be produced "
"during correlation.")
]),
dcc.Dropdown(id='channels', placeholder="Select no. channels...",
options=[{'label': fs.channels[ch],
'value': ch} \
for ch in fs.channels], value=32, persistence=True),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Number of polarizations',
*ge.tooltip(idname='popover-pols',
message="Number of polarizations to correlate. Note that VLBI uses circular " \
"polarizations. Full polarization implies the four stokes: RR, LL, RL, LR; " \
"while dual polarization implies RR and LL only.")
]),
dcc.Dropdown(id='pols', placeholder="Select polarizations...",
options=[{'label': fs.polarizations[p], 'value': p} \
for p in fs.polarizations], value=4, persistence=True),
]),
html.Br(),
html.Div(className='form-group', children=[
html.H6(['Integration time',
*ge.tooltip(idname='popover-inttime',
message="Integration time to compute each visibility. Note that for continuum " \