Browse Source

dbc.checkbox, graphycal summary output

pull/1/head
Benito Marcote 2 years ago
parent
commit
091d484f7e
  1. 405
      app.py
  2. 8
      assets/arrow.svg
  3. BIN
      assets/baseline-long.png
  4. BIN
      assets/baseline-short.png
  5. BIN
      assets/favicon.ico
  6. BIN
      assets/icon-16.png
  7. BIN
      assets/icon-180.png
  8. BIN
      assets/icon-32.png
  9. 36
      assets/icon-black.svg
  10. 1
      assets/icon-file.svg
  11. 137
      assets/style.css
  12. 131
      assets/waves.svg
  13. 2
      doc/doc-estimations.md
  14. 33
      doc/notes.md
  15. 2
      requirements.txt
  16. BIN
      src/__pycache__/functions.cpython-36.pyc
  17. 6
      src/functions.py
  18. 374
      src/graphical_elements.py
  19. 20
      test/test.py

405
app.py

@ -45,6 +45,7 @@ from src import freqsetups as fs
from src import stations
from src import functions as fx
from src import observation
from src import graphical_elements as ge
current_directory = path.dirname(path.realpath(__file__))
@ -64,12 +65,12 @@ all_antennas = fx.get_stations_from_configfile(f"{current_directory}/data/statio
sorted_networks = {'EVN': 'EVN: European VLBI Network', 'eMERLIN': 'eMERLIN (out-stations)',
'VLBA': 'VLBA: Very Long Baseline Array',
'LBA': 'LBA: Australian Long Baseline Array',
'KVN': 'Korean VLBI Network',
'KVN': 'KVN: Korean VLBI Network',
'Other': 'Other antennas'}
default_arrays = {'EVN': ['Ef', 'Hh', 'Jb2', 'Mc', 'Nt', 'Ur', 'On', 'Sr', 'T6', 'Tr',
'Ys', 'Wb', 'Bd', 'Sv', 'Zc', 'Ir'],
'e-EVN': ['Ef', 'Hh', 'Ir', 'Jb2', 'Mc', 'Nt', 'On', 'T6', 'Tr', 'Ys', 'Wb',
'Bd', 'Sv', 'Zc', 'Ir', 'Sr', 'Ur'],
'Bd', 'Sv', 'Zc', 'Ir', 'Sr', 'Ur', 'Cm', 'Kn', 'Pi', 'Da', 'De'],
'eMERLIN': ['Cm', 'Kn', 'Pi', 'Da', 'De', 'Jb2'],
'LBA': ['ATCA', 'Pa', 'Mo', 'Ho', 'Cd', 'Td', 'Ww'],
'VLBA': ['Br', 'Fd', 'Hn', 'Kp', 'La', 'Mk', 'Nl', 'Ov', 'Pt', 'Sc'],
@ -94,12 +95,36 @@ doc_files = {'About this tool': '/doc/doc-contact.md',
'Technical background': '/doc/doc-estimations.md'}
# Initial values
target_source = observation.Source('1h2m3s +50d40m30s', 'Source')
target_source = observation.Source('10h2m3s +50d40m30s', 'Source')
# obs_times = Time('1967-04-17 10:00') + np.arange(0, 600, 15)*u.min
selected_band = '18cm'
obs = observation.Observation(target=target_source)
obs.times = Time('2020-06-15 20:00', scale='utc') + np.arange(0, 1200, 30)*u.min
obs.band = selected_band
obs.datarate = 1024
obs.subbands = 8
obs.channels = 32
obs.polarizations = 2
obs.inttime = 2
def get_selected_antennas(list_of_selected_antennas):
"""Given a list of antenna codenames, it returns a Stations object containing
all given antennas.
"""
selected_antennas = stations.Stations('Observation', [])
for ant in list_of_selected_antennas:
selected_antennas.add(all_antennas[ant])
return selected_antennas
evn6 = ['Ef', 'Jb2', 'On', 'Hh', 'T6', 'Wb', 'Sv', 'Zc']
obs.stations = get_selected_antennas(evn6)
# external_stylesheets = ["https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css", "http://jive.eu/~marcote/style.css"]
# external_stylesheets = ["https://bmarcote.github.io/temp/style.css"]
@ -123,54 +148,6 @@ server = app.server
# app.css.append_css({"external_url": "https://codepen.io/chriddyp/pen/brPBPO.css"})
def tooltip(message, idname, trigger='?', placement='right', **kwargs):
"""Defines a tooltip (popover) that will be shown in the rendered page.
It will place a <sup>`trigger`</sup> in the page within a span tag.
Returns the list with all html elements.
"""
return [html.Span(children=html.Sup(trigger, className='popover-link'), id=idname),
dbc.Tooltip(message, target=idname, placement=placement,
# className='tooltip-class', #innerClassName='tooltip-class-inner',
**kwargs)]
def antenna_card(station):
"""Generates a card showing the basic information for the given station
"""
s = lambda st : st[::-1].replace(' ,',' dna ',1)[::-1]
card = dbc.Card([
dbc.CardImg(src=app.get_asset_url(f"ant-{station.name.replace(' ','_').lower()}.jpg"),
top=True, className='card-img'),
dbc.CardBody([
html.H4(station.name, className='card-title'),
html.H6(station.fullname if station.fullname != station.name else '',
className='card-title2'),
html.H6(station.country, className='card-subtitle'),
# html.P(f"&#127462; Participates in {station.all_networks}.\n"
dcc.Markdown(f"Listed for the {s(station.all_networks)}.\n" if \
station.all_networks != '' else '', className='card-text'),
dcc.Markdown("Can observe at "
f"{', '.join([i.replace('cm', '') for i in station.bands])} cm.",
className='card-text')
])
], className='card-antenna')
return card
def antenna_cards():
cards = dbc.Row([antenna_card(s) for s in all_antennas],
className='row justify-content-center')
return cards
def create_accordion_card(title, text, id, is_open=True):
"""Given a title (header) and a text (which can be either text, a dcc/html object),
it will return a dbc.Card object which is one input for an accordion.
"""
card_header = dbc.CardHeader(html.H2(dbc.Button(title, color='link',
id=f"group-{id}-toggle", className='')), className='accordion-header')
card_body = dbc.Collapse(dbc.CardBody(text), id=f"collapse-{id}", is_open=is_open,
className='accordion-collapse')
return dbc.Card([card_header, card_body], className='accordion-card')
def get_doc_text():
@ -190,10 +167,11 @@ def get_doc_text():
app.get_asset_url(filename) )
if a_topic == 'About the antennas':
temp += [create_accordion_card(a_topic,
[dcc.Markdown(parsed_text), antenna_cards()], id=str(i))]
temp += [ge.create_accordion_card(a_topic,
[dcc.Markdown(parsed_text), ge.antenna_cards(app, all_antennas)], id=str(i))]
else:
temp += [create_accordion_card(a_topic, dcc.Markdown(parsed_text), id=str(i))]
temp += [ge.create_accordion_card(a_topic, dcc.Markdown(parsed_text),
id=str(i))]
return html.Div(temp, className='col-12 accordion')
@ -216,6 +194,60 @@ def toggle_accordion(*args):
return defaults
def error_text(an_error):
"""Message written in a modal error window.
"""
return f"An error occured.\n{an_error}.\nPlease report to marcote@jive.eu."
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 colon_coord.replace(' ', 's ')+'s'
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 about all properties of the observation.
"""
cards = []
# The time card
cards += ge.summary_card_times(app, obs)
cards += ge.summary_card_antennas(app, obs)
cards += ge.summary_card_beam(app, obs)
cards += ge.summary_card_frequency(app, obs)
cards += ge.summary_card_rms(app, obs)
cards += ge.summary_card_fov(app, obs)
# return [html.Div(className='card-columns col-12 justify-content-center', children=cards)]
return [html.Div(className='card-deck col-12 justify-content-center', children=cards)]
##################### This is the webpage layout
app.layout = html.Div([
@ -235,7 +267,7 @@ app.layout = html.Div([
alt='European VLBI Network (EVN)',
className="d-inline-block align-top"),
]),
html.H2('EVN Source Visibility', className='d-inline-block align-middle mx-auto'),
html.H2('EVN Observation Planner', className='d-inline-block align-middle mx-auto'),
html.A(className='d-inline-block ml-auto pull-right', href="https://www.jive.eu", children=[
html.Img(src=app.get_asset_url("logo_jive.png"), height='70px',
alt='Joinst Institute for VLBI ERIC (JIVE)')
@ -255,16 +287,9 @@ app.layout = html.Div([
html.Div(className='row justify-content-center', children=[
html.Div(className='col-sm-3', style={'max-width': '300px','float': 'left',
'padding': '2%'}, children=[
html.Div(className='form-group', children=[
html.Label('Observing Band'),
*tooltip(idname='popover-band', message="First select the observing band. Antenna list will be updated and only the ones that can observe at this band will be enable."),
dcc.Dropdown(id='band', persistence=True,
options=[{'label': fs.bands[b], 'value': b} for b \
in fs.bands], value='18cm'),
]),
html.Div(className='form-group', children=[
html.Label('Select default VLBI Network(s)'),
*tooltip(idname='popover-network', message="Automatically selects the default antennas for the selected VLBI network(s)."),
*ge.tooltip(idname='popover-network', message="Automatically selects the default antennas for the selected VLBI network(s)."),
dcc.Dropdown(id='array', options=[{'label': n, 'value': n} \
for n in default_arrays if n != 'e-EVN'], value=['EVN'],
multi=True),
@ -273,7 +298,7 @@ app.layout = html.Div([
dcc.Checklist(id='e-EVN', className='checkbox', persistence=True,
options=[{'label': ' e-EVN (real-time) mode',
'value': 'e-EVN'}], value=[]),
*tooltip(idname='popover-eevn',
*ge.tooltip(idname='popover-eevn',
message="Only available for the EVN: real-time correlation mode.")
]),
html.Div(className='form-group', children=[
@ -285,6 +310,18 @@ app.layout = html.Div([
html.Small(id='error_starttime', style={'color': 'red'},
className='form-text text-muted')
]),
######################
# html.Div(className='form-group', children=[
# html.Label('Start of observation (UTC)'),
# dcc.DatePickerSingle(id='starttime2', min_date_allowed=dt(1900, 1, 1),
# max_date_allowed=dt(2100, 1, 1),
# display_format='D MMM YYYY (DDD)',
# placeholder='Start date',
# first_day_of_week=1,
# initial_visible_month=dt.today(),
# persistence=True,
# className='form-picker')
# ]),
html.Div(className='form-group', children=[
html.Label('End of observation (UTC)'),
# dcc.Input(id='endtime', value='DD/MM/YYYY HH:MM', type='text',
@ -296,7 +333,7 @@ app.layout = html.Div([
]),
html.Div(className='form-group', children=[
html.Label('Target Source Coordinates'),
*tooltip(idname='popover-target',
*ge.tooltip(idname='popover-target',
message="J2000 coordinates are assumed."),
# dcc.Input(id='source', value='hh:mm:ss dd:mm:ss', type='text',
dcc.Input(id='source', value='12:29:06.7 +02:03:08.6', type='text',
@ -308,7 +345,7 @@ app.layout = html.Div([
html.Div(className='form-group', children=[
html.Label(id='onsourcetime-label',
children='% of on-target time'),
*tooltip(idname='popover-ontarget',
*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=75,
marks= {i: str(i) for i in range(20, 101, 10)},
@ -316,7 +353,7 @@ app.layout = html.Div([
]),
html.Div(className='form-group', children=[
html.Label('Datarate per station (in Mbps)'),
*tooltip(idname='popover-datarate',
*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."),
@ -329,7 +366,7 @@ app.layout = html.Div([
]),
html.Div(className='form-group', children=[
html.Label('Number of subbands'),
*tooltip(idname='popover-subbands',
*ge.tooltip(idname='popover-subbands',
message="In how many subbands the total band will be split during correlation."),
dcc.Dropdown(id='subbands', placeholder="Select no. subbands...",
options=[{'label': str(sb), 'value': sb} \
@ -337,7 +374,7 @@ app.layout = html.Div([
]),
html.Div(className='form-group', children=[
html.Label('Number of spectral channels'),
*tooltip(idname='popover-channels',
*ge.tooltip(idname='popover-channels',
message="How many channels per subband will be produced after correlation."),
dcc.Dropdown(id='channels', placeholder="Select no. channels...",
options=[{'label': str(ch), 'value': ch} \
@ -345,7 +382,7 @@ app.layout = html.Div([
]),
html.Div(className='form-group', children=[
html.Label('Number of polarizations'),
*tooltip(idname='popover-pols',
*ge.tooltip(idname='popover-pols',
message="Number of polarizations to correlate. Note that VLBI observes 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} \
@ -353,7 +390,7 @@ app.layout = html.Div([
]),
html.Div(className='form-group', children=[
html.Label('Integration time (s)'),
*tooltip(idname='popover-inttime',
*ge.tooltip(idname='popover-inttime',
message="Integration time to compute each visibility. Note that for continuum observations values of 1-2 seconds are typical."),
dcc.Dropdown(id='inttime', placeholder="Select integration time...",
options=[{'label': fs.inttimes[it], 'value': it} \
@ -362,18 +399,46 @@ app.layout = html.Div([
]),
# html.Div(style={'margin-top': '20px'}, children=[
html.Div(className='col-9', children=[
html.Div(id='first-advise', children=[
html.P(["This is the ", html.B("EVN Observation Planner"),
". First select the "
"band (frequency/wavelength) at which you want to observe, "
"then customize your observation setup (left options and "
"select wished antennas), and finally "
"run ", html.B("'compute observation'"),
". You will get a detailed "
"summary of the planned observation (like when the source "
"is visible, expected rms noise level, etc.) in the different "
"tabs."]),
], style={'margin-top': '2rem', 'margin-bottom': '2rem'}),
html.Div(className='col-9 form-group row align-items-end', children=[
html.Div(className='col-md-6', children=[
html.Label('First select your observing Band'),
*ge.tooltip(idname='popover-band',
message="First select at which frequency/wavelength "
"you want to observe. This will update the "
"antenna list showing the ones that can observe "
"at that given frequency."),
dcc.Dropdown(id='band', persistence=True,
options=[{'label': fs.bands[b], 'value': b} for b \
# in fs.bands], value='18cm'),
in fs.bands], placeholder='Select observing band...')
]),
html.Div(className='col-sm-3', children=[
html.Button('Compute Observation', id='antenna-selection-button',
className='btn btn-primary btn-lg'),
])
]),
html.Div(className='col-9 text-center justify-content-center', children=[
html.Button('Compute Observation', id='antenna-selection-button',
className='btn btn-primary btn-lg'),
dcc.Loading(id="loading", children=[html.Div(id="loading-output")],
type="dot")
]),
html.Div(id='antennas-div', className='container', children=[
# List with all antennas
html.Div(className='antcheck', children=[html.Br(), html.Br(),
html.Label(html.H4(f"{sorted_networks[an_array]}")),
html.Label(html.H4(f"{sorted_networks[an_array]}"),
style={'width': '100%'}),
html.Br(),
dcc.Checklist(id=f"list_stations_{an_array}",
dbc.Checklist(id=f"list_stations_{an_array}", inline=True,
className='antcheck',
labelClassName='form-check-label',
inputClassName='form-check-input',
@ -396,10 +461,7 @@ app.layout = html.Div([
# children="Set the observation first.")
html.Div(className='col-10 justify-content-center',
id='sensitivity-output',
children=[html.Div(className='col-6 justify-content-center',
children=[html.Br(),
html.P("You need to set the observation and click in the 'Compute Observation' buttom first (go to the previous tab).")])
])
children=update_sensitivity(obs))
])
]),
dcc.Tab(label='Elevations', className='custom-tab',
@ -426,7 +488,7 @@ app.layout = html.Div([
],className='tex2jax_ignore')
])])
]),
dcc.Tab(label='Imaging', className='custom-tab',
dcc.Tab(label='Coverage', className='custom-tab',
selected_className='custom-tab--selected', children=[
# Images
html.Div(className='row justify-content-center', children=[
@ -453,158 +515,6 @@ app.layout = html.Div([
def error_text(an_error):
"""Message written in a modal error window.
"""
return f"An error occured.\n{an_error}.\nPlease report to marcote@jive.eu."
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 colon_coord.replace(' ', 's ')+'s'
def get_selected_antennas(list_of_selected_antennas):
"""Given a list of antenna codenames, it returns a Stations object containing
all given antennas.
"""
selected_antennas = stations.Stations('Observation', [])
for ant in list_of_selected_antennas:
selected_antennas.add(all_antennas[ant])
return selected_antennas
def optimal_units(value, units):
"""Given a value (with some units), returns the unit choice from all
`units` possibilities that better suits the value.
It is meant for the following use:
Given 0.02*u.Jy and units = [u.kJy, u.Jy, u.mJy, u.uJy], it will
return 20*u.mJy.
units should have a decreasing-scale of units, and all of them
compatible with the units in `value`.
"""
for a_unit in units:
if 0.8 < value.to(a_unit).value <= 800:
return value.to(a_unit)
# Value too high or too low
if value.to(units[0]).value > 1:
return value.to(units[0])
return value.to(units[-1])
def create_sensitivity_card(title, message):
"""Defines one of the cards that are shown in the Sensitivity tab. Each tab
shows a title `title` and a message `message`.
If message is a list (of strings), it is assumed as different paragraphs.
It returns the HTML code of the card.
"""
ps = []
if type(message) is list:
for a_msg in message:
ps.append(html.P(className='card-text', children=a_msg))
else:
ps = [html.P(className='card-text', children=message)]
# return [html.Div(className='card', style={'min-width': '15rem', 'max-width': '25rem'}, children=[
return [html.Div(className='card m-3', children=[
html.Div(className='card-body', children=[
html.H5(className='card-title', children=title)] + ps)])
]
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 about all properties of the observation.
"""
pol_dict = {1: 'single', 2: 'dual', 4: 'full'}
rms = optimal_units(obs.thermal_noise(), [u.MJy, u.kJy, u.Jy, u.mJy, u.uJy])
rms_time = optimal_units(obs.thermal_noise()/np.sqrt(obs.inttime/obs.duration),
[u.MJy, u.kJy, u.Jy, u.mJy, u.uJy])
rms_channel = optimal_units(rms*np.sqrt(obs.subbands*obs.channels),
[u.MJy, u.kJy, u.Jy, u.mJy, u.uJy])
ants_up = obs.is_visible()
ant_no_obs = []
for an_ant in ants_up:
if len(ants_up[an_ant][0]) == 0:
ant_no_obs.append(an_ant)
ant_text = ', '.join([ant for ant in obs.stations.keys() if ant not in ant_no_obs]) + '.'
cards = []
# The time card
temp_msg = [f"{fx.print_obs_times(obs)}."]
temp_msg += [f"Observing for {optimal_units(obs.duration, [u.h, u.min, u.s, u.ms]):.3n} in total, with {optimal_units(obs.ontarget_time, [u.h, u.min, u.s, u.ms]):.3n} on target."]
temp_msg += [f"With a time integration of {optimal_units(obs.inttime, [u.s,u.ms,u.us]):.2n} the expected FITS file size is {optimal_units(obs.datasize(), [u.TB, u.GB, u.MB, u.kB]):.3n}."]
cards += create_sensitivity_card('Observing Time', temp_msg)
# Antennas
longest_bl = obs.longest_baseline()[1]
# Using dummy units to allow the conversion
longest_bl_lambda = longest_bl/obs.wavelength
longest_bl_lambda = optimal_units(longest_bl_lambda*u.m, [u.Gm, u.Mm, u.km])
temp_msg = [f"{len(ants_up)-len(ant_no_obs)} participating antennas: {ant_text}"]
if len(ant_no_obs) > 0:
temp_msg += [html.P(className='text-danger', children=f"Note that {', '.join(ant_no_obs)} cannot observe the source.")]
temp_msg += [f"The longest (projected) baseline is {optimal_units(longest_bl, [u.km, u.m]):.5n} ({longest_bl_lambda.value:.3n} {longest_bl_lambda.unit.name[0]}lambda)."]
synthbeam = obs.synthesized_beam()
synthbeam_units = optimal_units(synthbeam['bmaj'], [u.arcsec, u.mas, u.uas]).unit
temp_msg += [html.P([f"The expected synthesized beam will be approx. {synthbeam['bmaj'].to(synthbeam_units).value:.2n} x {synthbeam['bmin'].to(synthbeam_units):.2n}", html.Sup("2"), \
f", PA = {synthbeam['pa']:.3n}."])]
cards += create_sensitivity_card('Antennas', temp_msg)
# Frequency
temp_msg = [f"Observing at a central frequency of {optimal_units(obs.frequency, [u.GHz, u.MHz]):.3n} ({optimal_units(obs.wavelength, [u.m, u.cm, u.mm]):.2n})."]
temp_msg += [f"The total bandwidth of {optimal_units(obs.bandwidth, [u.GHz, u.MHz, u.kHz]):.3n} will be divided into {obs.subbands} subbands of {optimal_units(obs.bandwidth/obs.subbands, [u.GHz, u.MHz, u.kHz]):.3n} each, with {obs.channels} channels ({optimal_units(obs.bandwidth/(obs.subbands*obs.channels), [u.GHz, u.MHz, u.kHz, u.Hz]):.3n} wide)."]
temp_msg += [f"Recording {pol_dict[obs.polarizations]} circular polarization."]
cards += create_sensitivity_card('Frequency Setup', temp_msg)
# RMS
temp_msg = [f"Considering the sensitivities of the antennas, the estimated thermal noise is {rms:.3n}/beam."]
temp_msg += [f"This would imply a rms of {rms_channel:.3n}/beam per spectral channel, or approx. {rms_time:.3n}/beam per time integration ({optimal_units(obs.inttime, [u.s,u.ms,u.us]):.3n})."]
cards += create_sensitivity_card('Sensitivity', temp_msg)
# resolution and FITS files
# temp_msg = [f""]
# cards += create_sensitivity_card('Resolution')
# FoV smearing
shortest_bl = obs.shortest_baseline()[1]
largest_ang_scales = ((2.063e8*u.mas)*(obs.wavelength/shortest_bl)).to(u.mas)
temp_msg = [f"The Field of View would be limited by time smearing to {optimal_units(obs.time_smearing(), [u.arcmin, u.arcsec]):.3n} and by frequency smearing to {optimal_units(obs.bandwidth_smearing(), [u.arcmin, u.arcsec]):.3n} (for a 10% loss)."]
temp_msg += [f"Considering the shortest baseline in the array ({optimal_units(shortest_bl, [u.km, u.m]):.5n}), you will filter out emission on angular scales larger than {optimal_units(largest_ang_scales, [u.arcmin, u.arcsec, u.mas]):.3n}."]
cards += create_sensitivity_card('FoV limitations', temp_msg)
return [html.Div(className='card-deck col-12 justify-content-center', children=cards)]
@app.callback(Output('onsourcetime-label', 'children'),
@ -628,14 +538,17 @@ def select_antennas(selected_band, selected_networks, is_eEVN):
return [html.Div([html.Br(), html.Br(),
html.Label(html.H4(f"{sorted_networks[an_array]}")),
html.Br(),
dcc.Checklist(id=f"list_stations_{an_array}",
dbc.Checklist(id=f"list_stations_{an_array}", inline=True,
className='antcheck',
labelClassName='form-check-label',
inputClassName='form-check-input',
options=[{'label': s.name, 'value': s.codename,
'disabled': (not s.has_band(selected_band)) or \
(not s.codename in default_arrays['e-EVN'])}
for s in all_antennas if s.network == an_array],
'disabled': (not s.has_band(selected_band)) or \
(not s.codename in default_arrays['e-EVN'])}
for s in all_antennas if s.network == an_array],
value=selected_antennas if an_array=='EVN' else [],
className='antcheck', labelClassName='form-check-label',
inputClassName='form-check-input')]) for an_array in sorted_networks]
)]) for an_array in sorted_networks
]
else:
for an_array in selected_networks:
selected_antennas += [ant for ant in default_arrays[an_array] \
@ -644,14 +557,18 @@ def select_antennas(selected_band, selected_networks, is_eEVN):
return [html.Div([html.Br(), html.Br(),
html.Label(html.H4(f"{sorted_networks[an_array]}")),
html.Br(),
dcc.Checklist(id=f"list_stations_{an_array}",
dbc.Checklist(id=f"list_stations_{an_array}", inline=True,
className='antcheck',
labelClassName='form-check-label',
inputClassName='form-check-input',
options=[{'label': s.name, 'value': s.codename,
'disabled': not s.has_band(selected_band)}
for s in all_antennas if s.network == an_array],
'disabled': not s.has_band(selected_band)}
for s in all_antennas if s.network == an_array],
value=[s.codename for s in all_antennas \
if (s.codename in selected_antennas) and (s.network == an_array)],
className='antcheck', labelClassName='form-check-label',
inputClassName='form-check-input')]) for an_array in sorted_networks]
)]) for an_array in sorted_networks
]
@app.callback([Output('error_starttime', 'children'),

8
assets/arrow.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
assets/baseline-long.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/baseline-short.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icon-16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

BIN
assets/icon-180.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
assets/icon-32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

36
assets/icon-black.svg

@ -0,0 +1,36 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M964 5102 c-52 -25 -87 -60 -139 -141 -142 -222 -242 -487 -280 -743
-3 -23 -8 -56 -11 -73 -2 -16 -6 -120 -8 -230 -7 -385 60 -724 221 -1114 30
-74 155 -327 183 -371 5 -8 37 -60 72 -115 52 -85 126 -194 155 -230 5 -5 22
-28 38 -50 17 -22 32 -42 35 -45 3 -3 27 -32 54 -65 63 -76 92 -108 208 -225
492 -500 1090 -834 1723 -961 46 -9 71 -14 145 -24 197 -28 582 -28 720 0 14
3 41 8 60 11 62 10 242 64 335 101 158 63 376 187 423 242 37 42 54 92 55 156
0 108 -6 116 -402 511 l-361 362 0 823 0 824 59 27 c112 52 209 162 251 284
97 279 -66 582 -356 658 -64 17 -194 13 -262 -7 -121 -37 -242 -135 -298 -241
-16 -31 -33 -56 -39 -56 -5 -1 -383 -1 -839 -2 l-828 -1 -337 336 c-185 184
-352 343 -371 353 -53 26 -156 29 -206 6z m2586 -1082 c0 -3 -144 -149 -319
-324 l-320 -319 -321 320 c-176 177 -316 323 -310 325 16 5 1270 4 1270 -2z
m258 -906 l2 -637 -321 322 c-177 177 -320 323 -318 326 43 50 625 633 629
630 3 -2 7 -290 8 -641z"/>
<path d="M652 2279 c-38 -85 -78 -215 -96 -314 -14 -81 -15 -338 -2 -400 4
-16 13 -56 20 -88 15 -65 26 -17 -106 -457 -27 -91 -60 -201 -72 -245 -13 -44
-51 -172 -85 -285 -34 -113 -73 -243 -87 -290 -14 -47 -34 -111 -44 -142 l-19
-58 946 0 c896 0 945 1 938 18 -4 9 -8 25 -10 35 -2 9 -13 47 -25 85 -33 108
-108 367 -106 369 0 1 21 4 46 7 45 7 72 11 120 22 14 3 35 7 47 10 87 18 353
133 353 152 0 5 -44 30 -97 55 -181 87 -329 173 -508 295 -60 42 -112 78 -115
81 -3 3 -32 26 -65 51 -33 24 -65 49 -71 55 -6 5 -38 32 -70 60 -61 51 -194
174 -205 188 -3 4 -37 40 -75 80 -38 40 -94 101 -124 137 -30 36 -57 67 -61
70 -13 12 -168 220 -204 275 -22 33 -43 62 -47 63 -5 2 -8 7 -8 12 0 9 -115
199 -129 213 -6 6 -20 -14 -39 -54z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

1
assets/icon-file.svg

@ -0,0 +1 @@
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="fileIconTitle" stroke="#A01d26" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#A01d26"> <title id="fileIconTitle">File</title> <path stroke-linecap="round" d="M13 3v6h6"/> <path d="M13 3l6 6v12H5V3z"/> </svg>

After

Width:  |  Height:  |  Size: 363 B

137
assets/style.css

@ -815,6 +815,26 @@ pre code {
transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out
}
.form-picker, .SingleDataPicker {
width: 100%;
}
.SingleDataPickerInput {
display: block;
width: 100%;
height: calc(1.5em + .75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out
}
@media (prefers-reduced-motion: reduce) {
.form-control {
transition:none
@ -1270,7 +1290,7 @@ textarea.form-control {
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
padding: .375rem .7rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
@ -3283,16 +3303,35 @@ input[type=checkbox]:disabled:after {
margin-left: -0.1rem;
}
/* #band { */
/* background-color: #F0959B; */
/* } */
.antcheck label {
padding-left: 10pt;
padding-right: 10pt;
padding-left: 0pt;
padding-right: 2pt;
width: 135pt;
}
/* input[type=checkbox]:disabled ~ .antcheck2 label { */
.custom-control-input:disabled ~ .custom-control-label {
text-decoration: line-through;
}
.custom-control-input:checked ~ .custom-control-label {
color: green;
}
.custom-checkbox .custom-control-input:checked~.custom-control-label::before {
border: none;
background-color: green;
}
.card {
min-width: 19rem;
max-width: 19rem;
min-width: 21rem;
max-width: 21rem;
border: 1px solid #CCCCCC;
border-left: 5px solid #a01d26;
}
@ -3305,8 +3344,9 @@ input[type=checkbox]:disabled:after {
color: #a01d26;
background-color: #F0959B;
border-color: #F0959B;
margin-top: 50px;
width: 20rem;
height: 2.3rem;
padding: 0;
}
.btn-primary:hover {
@ -3451,6 +3491,22 @@ input[type=checkbox]:disabled:after {
.tooltip-class.bottom-left .tooltip-class-arrow { top: 0; right: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #a01d26;}
.tooltip-class.bottom-right .tooltip-class-arrow { top: 0; left: 5px; margin-top: -5px; border-width: 0 5px 5px; border-bottom-color: #a01d26;}
.tooltip-card-inner {
border-color: 0 !important;
border: 0;
padding: 0;
margin: 0;
background-color: transparent !important;
}
.arrow.tooltip-card-arrow {
color: transparent;
border: 0;
background-color: transparent !important;
}
.alert-warning {
/* color:#07767a; */
color: #856404;
@ -3580,12 +3636,12 @@ img[alt=equation2] {
}
.card.card-antenna .card-img {
object-fit: cover;
width: 100%;
height: 40%;
object-fit: cover;
width: 100%;
height: 40%;
overflow: hidden;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.card.card-antenna .card-title {
@ -3612,6 +3668,65 @@ img[alt=equation2] {
margin-bottom: 0px;
}
.popover-link {
color: #a01d26;
}
.baseline-table {
align: center;
margin-bottom: -0.2rem;
}
.baseline-tr {
width: 100%;
}
.baseline-td-img {
vertical-align: middle;
}
.baseline-td-hr {
vertical-align: bottom;
}
.hr-baseline {
border: 2px solid #a01d26 !important;
border-radius: 4rem;
}
.col.col-baseline {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
column-width: 31px !important;
}
.img-baseline {
margin-left: 0.4rem;
margin-right: 0.4rem;
padding-left: 0;
padding-right: 0;
vertical-align: baseline;
}
.fig-on-card {
width: 30rem;
}
.bandpass {
border: 3px solid #a01d26;
border-bottom: 0px solid #a01d26;
border-top-left-radius: 20%;
border-top-right-radius: 20%;
display: inline-block;
margin-right: -2px;
margin-left: -1px;
margin-bottom: 0;
margin-top: 1rem;
}
/* .accordion button:after { */
/* content: '\02795'; /* Unicode character for "plus" sign (+) */ */

131
assets/waves.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

2
doc/doc-estimations.md

@ -7,7 +7,7 @@ VLBI observations are typically limited by the data rate: the amount of data tha
The data rate for a given station is determined as
![equation2]({src:eq-datarate.png})
where _&#916;&#957;_ is the total observed bandwidth, _Np_ is the number of recorded polarizations (one or two), _N<sub>b</sub>_ is the number of bits used to sample the data (VLBI observations typically record at 2-bit sampling), and the last 2 is related to the Nyquist sampling.
where _&#916;&#957;_ is the total observed bandwidth, _Np_ is the number of recorded polarizations (one or two), _Nb_ is the number of bits used to sample the data (VLBI observations typically record at 2-bit sampling), and the last 2 is related to the Nyquist sampling.

33
doc/notes.md

@ -12,6 +12,7 @@
- sources.py
- plots.py
- functions.py
- graphical_elements.py
- data/
- antenna_positions.txt
- antenna_sefd.txt
@ -33,15 +34,33 @@ Layout:
- pols
- inttime
+ get_doc_text()
### graphical_elements.py
+ tooltip(message, idname, trigger='?', placement='right', \*\*kwargs)
+ tooltip_card(a_card, idname, trigger='?', placement='right', \*\*kwargs)
+ create_accordion_card(title, text, id, is_open=True)
+ antenna_card(app, station)
+ antenna_cards(app, stations)
+ baseline_img(app, is_long=True)
### summary_cards.py
### stations.py
Station
- observer : Observer
- name : str
- fullname : str
- country : str
- all_networks : str
- fullname : str
- country : str
- all_networks : str
- codename : str
- network : str
- location : EarthLocation
@ -82,7 +101,7 @@ Observation
- target : FixedTarget
- times : Time
- gstimes : Longitude (hourangle)
- duration : Time
- duration : Time
- band : str
- wavelength : u.Quantity
- frequency : u.Quantity
@ -92,7 +111,7 @@ Observation
- polarizations : int
- inttime : u.Quantity
- ontarget_fraction : float
- ontarget_time : Time
- ontarget_time : Time
- bandwidth : u.Quantity
- bitsampling : u.Quantity
- stations : Stations
@ -102,8 +121,8 @@ Observation
+ elevations() --> dict[codename]: list
+ altaz() --> dict[codename]: list
+ is_visible() --> dict[codename]: list
+ longest_baseline() --> (str, u.Quantity)
+ shortest_baseline() --> (str, u.Quantity)
+ longest_baseline() --> (str, u.Quantity)
+ shortest_baseline() --> (str, u.Quantity)
+ bandwidth_smearing() --> u.Quantity
+ time_smearing() --> u.Quantity
+ datasize() --> u.Quantity

2
requirements.txt

@ -16,7 +16,9 @@ itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
numpy==1.18.4
pandas==1.0.5
plotly==4.7.1
python-dateutil==2.8.1
pytz==2020.1
retrying==1.3.3
six==1.14.0

BIN
src/__pycache__/functions.cpython-36.pyc

Binary file not shown.

6
src/functions.py

@ -173,7 +173,11 @@ def print_obs_times(obs, date_format='%d %b %Y'):
"""
if obs.times[0].datetime.date() == obs.times[-1].datetime.date():
return "{} {}-{} UTC".format(obs.times[0].datetime.strftime(date_format),
return "{}\n{}-{} UTC".format(obs.times[0].datetime.strftime(date_format),
obs.times[0].datetime.strftime('%H:%M'),
obs.times[-1].datetime.strftime('%H:%M'))
elif (obs.times[-1] - obs.times[0]) < 24*u.h:
return "{}\n{}-{} UTC (+1d)".format(obs.times[0].datetime.strftime(date_format),
obs.times[0].datetime.strftime('%H:%M'),
obs.times[-1].datetime.strftime('%H:%M'))
else:

374
src/graphical_elements.py

@ -0,0 +1,374 @@
import numpy as np
from astropy import units as u
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 src import functions as fx
def tooltip(message, idname, trigger='?', placement='right',trigger_is_sup=True, **kwargs):
"""Defines a tooltip (popover) that will be shown in the rendered page.
It will place a <sup>`trigger`</sup> in the page within a span tag.
Returns the list with all html elements.
"""
if trigger_is_sup:
return [html.Span(children=html.Sup(trigger, className='popover-link'), id=idname),
dbc.Tooltip(message, target=idname, placement=placement, **kwargs)]
else:
return [html.Span(children=trigger, className='popover-link', id=idname),
dbc.Tooltip(message, target=idname, placement=placement, **kwargs)]
def tooltip_card(a_card, idname, trigger, placement='right', **kwargs):
"""Defines a tooltip (popover) that shows a card.
"""
return [html.Span(children=trigger, className='popover-link', id=idname),
dbc.Tooltip(a_card, target=idname, placement=placement,
innerClassName='tooltip-card-inner', hide_arrow=True,
**kwargs)]
def create_accordion_card(title, text, id, is_open=True):
"""Given a title (header) and a text (which can be either text, a dcc/html object),
it will return a dbc.Card object which is one input for an accordion.
"""
card_header = dbc.CardHeader(html.H2(dbc.Button(title, color='link',
id=f"group-{id}-toggle", className='')), className='accordion-header')
card_body = dbc.Collapse(dbc.CardBody(text), id=f"collapse-{id}", is_open=is_open,
className='accordion-collapse')
return dbc.Card([card_header, card_body], className='accordion-card')
def create_sensitivity_card(title, message):
"""Defines one of the cards that are shown in the Sensitivity tab. Each tab
shows a title `title` and a message `message`.
If message is a list (of strings), it is assumed as different paragraphs.
It returns the HTML code of the card.
"""
ps = []
if type(message) is list:
for a_msg in message:
ps.append(html.P(className='card-text', children=a_msg))
else:
ps = [html.P(className='card-text', children=message)]
# return [html.Div(className='card', style={'min-width': '15rem', 'max-width': '25rem'}, children=[
return [html.Div(className='card m-3', children=[
html.Div(className='card-body', children=[
html.H5(className='card-title', children=title)] + ps)])
]
#################################################################################
# It is all about cards in this section
def antenna_card(app, station):
"""Generates a card showing the basic information for the given station
"""
s = lambda st : st[::-1].replace(' ,',' dna ',1)[::-1]
card = dbc.Card([
dbc.CardImg(src=app.get_asset_url(f"ant-{station.name.replace(' ','_').lower()}.jpg"),
top=True, className='card-img'),
dbc.CardBody([
html.H4(station.name, className='card-title'),
html.H6(station.fullname if station.fullname != station.name else '',
className='card-title2'),
html.H6(station.country, className='card-subtitle'),
# html.P(f"&#127462; Participates in {station.all_networks}.\n"
dcc.Markdown(f"Listed for the {s(station.all_networks)}.\n" if \
station.all_networks != '' else '', className='card-text'),
dcc.Markdown("Can observe at "
f"{', '.join([i.replace('cm', '') for i in station.bands])} cm.",
className='card-text')
])
], className='card-antenna')
return card
def antenna_cards(app, stations):
cards = dbc.Row([antenna_card(app, s) for s in stations],
className='row justify-content-center')
return cards
def summary_card_antennas(app, obs):
"""Generates the summary card with the information about which
antennas can observe the given source, and the longest/shortest baselines.
"""
ants_up = obs.is_visible()
ant_no_obs = []
for an_ant in ants_up:
if len(ants_up[an_ant][0]) == 0:
ant_no_obs.append(an_ant)
ant_text = ', '.join([ant for ant in obs.stations.keys() if ant not in ant_no_obs]) + '.'
ant_text = []
for ant in obs.stations.keys():
if ant not in ant_no_obs:
ant_text += tooltip_card(antenna_card(app, obs.stations[ant]),
idname=f"basel-{ant}", trigger=ant, placement='top')
ant_text += [html.Span(", ")]
# Remove the trailing ,
ant_text[-1] = html.Span(".")
# TODO: This is the worldmap... I think it does not fit here.
# temp_msg = [ge.worldmap_plot([obs.stations[a] for a in obs.stations.keys() \
# if a not in ant_no_obs])]
temp_msg = []
longest_bl = obs.longest_baseline()
ant_l1, ant_l2 = longest_bl[0].split('-')
# Using dummy units to allow the conversion
longest_bl_lambda = longest_bl[1]/obs.wavelength
longest_bl_lambda = optimal_units(longest_bl_lambda*u.m, [u.Gm, u.Mm, u.km])
temp_msg += [[f"{len(ants_up)-len(ant_no_obs)} participating antennas: ", *ant_text]]
if len(ant_no_obs) > 0:
for ant in ant_no_obs:
ant_text += tooltip_card(antenna_card(app, obs.stations[ant]),
idname=f"basel-{ant}", trigger=ant, placement='top')
ant_text += [html.Span(", ")]
# Remove the trailing ,
ant_text[-1] = html.Span(".")
temp_msg += [html.P(className='text-danger', children=["Note that", ant_text,
"cannot observe the source."])]
temp_msg += [[*baseline_img(app, is_long=True),
*tooltip_card(antenna_card(app, obs.stations[ant_l1]), idname='basel-l1',
trigger=ant_l1, placement='top'),
html.Span("-"),
*tooltip_card(antenna_card(app, obs.stations[ant_l2]), idname='basel-l2',
trigger=ant_l2, placement='top'),
f" is the longest (projected) baseline with {optimal_units(longest_bl[1], [u.km, u.m]):.5n} ({longest_bl_lambda.value:.3n} {longest_bl_lambda.unit.name[0]}\u03BB)."]]
shortest_bl = obs.shortest_baseline()
ant_s1, ant_s2 = shortest_bl[0].split('-')
# Using dummy units to allow the conversion
shortest_bl_lambda = shortest_bl[1]/obs.wavelength
shortest_bl_lambda = optimal_units(shortest_bl_lambda*u.m, [u.Gm, u.Mm, u.km])
temp_msg += [[*baseline_img(app, is_long=False),
*tooltip_card(antenna_card(app, obs.stations[ant_s1]), idname='basel-s1',
trigger=ant_s1, placement='top'),
html.Span("-"),
*tooltip_card(antenna_card(app, obs.stations[ant_s2]), idname='basel-s2',
trigger=ant_s2, placement='top'),
f" is the shortest one with {optimal_units(shortest_bl[1], [u.km, u.m]):.5n} ({shortest_bl_lambda.value:.3n} {shortest_bl_lambda.unit.name[0]}\u03BB)."]]
# )]
return create_sensitivity_card('Antennas', temp_msg)
def summary_card_beam(app, obs):
"""Creates a summary card showing the expected synthesized beam.
"""
synthbeam = obs.synthesized_beam()
synthbeam_units = optimal_units(synthbeam['bmaj'], [u.arcsec, u.mas, u.uas]).unit
temp_msg = [html.Div(className='row', style={'height': '1rem'}),
html.Div(className='row justify-content-center',
children=[ellipse(bmaj="5rem",
bmin=f"{5*synthbeam['bmin'].to(u.mas)/synthbeam['bmaj'].to(u.mas)}rem",
pa=f"{synthbeam['pa'].to(u.deg).value}deg")])]
# TODO: Check that the rotation is the correct.
temp_msg += [html.P([f"The expected synthesized beam will be approx. {synthbeam['bmaj'].to(synthbeam_units).value:.2n} x {synthbeam['bmin'].to(synthbeam_units):.2n}", html.Sup("2"), \
f", PA = {synthbeam['pa']:.3n}."])]
temp_msg += [html.P("Note that the synthesized beam can significantly change depending "
"on the weighting used during imaging.")]
return create_sensitivity_card('Resolution', temp_msg)
def summary_card_times(app, obs):
"""Creates a summary card showing the observing times, and the resulting data size.
"""
prtobstimes = fx.print_obs_times(obs)
if '\n' in prtobstimes:
tmp = [html.Span(t) for t in fx.print_obs_times(obs).split('\n')]
tmp.insert(1, html.Br())
temp_msg = [tmp]
else:
temp_msg = [f"{fx.print_obs_times(obs)}."]
temp_msg += [f"The observation lasts for {optimal_units(obs.duration, [u.h, u.min, u.s, u.ms]):.3n}, of which {optimal_units(obs.ontarget_time, [u.h, u.min, u.s, u.ms]):.3n} are on target."]
n_files = int(np.ceil(obs.datasize()/(2.0*u.GB)))
if n_files < 10:
img_fits = [html.Img(src=app.get_asset_url("icon-file.svg"), height='35rem',
style={'display': 'inline'}) for i in range(n_files)]
else:
img_fits = [html.Img(src=app.get_asset_url("icon-file.svg"), height='35rem',
style={'display': 'inline'}) for i in range(10)]
img_fits += ["+"]
temp_msg += [[*img_fits, html.Br(),
f"With a time integration of {optimal_units(obs.inttime, [u.s,u.ms,u.us]):.2n} the "
f"expected FITS file size is "
f"{optimal_units(obs.datasize(), [u.TB, u.GB, u.MB, u.kB]):.3n} "
f"(divided in {n_files} 2-GB files)."]]
return create_sensitivity_card('Observing Time', temp_msg)
def summary_card_frequency(app, obs):
pol_dict = {1: 'single', 2: 'dual', 4: 'full'}
bw = optimal_units(obs.bandwidth, [u.GHz, u.MHz, u.kHz])
bwwl = optimal_units((30*u.cm/(obs.bandwidth.to(u.GHz).value)), [u.m, u.cm, u.mm])
temp_msg = [html.Div(className='row justify-content-center',
children=[html.Div(className='bandpass', style={'height': '4rem',
'width': f"{90/obs.subbands}%"})]*obs.subbands)]
temp_msg += [html.Div(className='row justify-content-center',
children=[html.Table(className='baseline-table',
style={'width': '100%', 'font-size': '0.8rem', 'margin-top':'-1rem'},
children=[html.Tr([
html.Td(f"-{bw/2:.3n}", style={'text-align': 'left'}),
html.Td(f"{optimal_units(obs.frequency, [u.GHz, u.MHz]):.4n}",
style={'text-align': 'center'}),
html.Td(f"+{bw/2:.3n}", style={'text-align': 'right'})
])]
)])]
temp_msg += [f"The central frequency is {optimal_units(obs.frequency, [u.GHz, u.MHz]):.3n} ({optimal_units(obs.wavelength, [u.m, u.cm, u.mm]):.2n})."]
temp_msg += [f"The total bandwidth of {optimal_units(obs.bandwidth, [u.GHz, u.MHz, u.kHz]):.3n} will be divided into {obs.subbands} subbands of {optimal_units(obs.bandwidth/obs.subbands, [u.GHz, u.MHz, u.kHz]):.3n} each, with {obs.channels} channels ({optimal_units(obs.bandwidth/(obs.subbands*obs.channels), [u.GHz, u.MHz, u.kHz, u.Hz]):.3n} wide)."]
temp_msg += [f"Recording {pol_dict[obs.polarizations]} circular polarization."]
return create_sensitivity_card('Frequency Setup', temp_msg)
def summary_card_fov(app, obs):
"""Creates a summary card showing the expected FoV limitations.
"""
# primary_beam =
shortest_bl = obs.shortest_baseline()[1]
largest_ang_scales = ((2.063e8*u.mas)*(obs.wavelength/shortest_bl)).to(u.mas)
pb_scale = ((2.063e8*u.mas)*(obs.wavelength/(100*u.m))).to(u.arcsec)
bw_smearing = obs.bandwidth_smearing()
tm_smearing = obs.time_smearing()
smearing_ratio = bw_smearing/tm_smearing
smearing_ratio = smearing_ratio if smearing_ratio <= 1.0 else 1/smearing_ratio
temp_msg = [html.Div(className='row', style={'height': '1rem'}),
html.Div(className='row justify-content-center',
children=[ellipse(bmaj="5rem", bmin="5rem", pa="0deg"),
ellipse(bmaj=f"2rem", bmin=f"2rem", pa="0deg", color="#F0959B",
z_index=3, position='absolute', margin_top='7%'),
ellipse(bmaj=f"{2*smearing_ratio}rem",
bmin=f"{2*smearing_ratio}rem",
pa="0deg", color="white", z_index=4, position='absolute',
margin_top=f'{8+2*bw_smearing/tm_smearing}%')])]
temp_msg += [f"The Field of View would be limited by time smearing to {optimal_units(tm_smearing, [u.arcmin, u.arcsec]):.3n} and by frequency smearing to {optimal_units(bw_smearing, [u.arcmin, u.arcsec]):.3n} (considering a 10% loss)."]
temp_msg += [f"Considering the shortest baseline in the array, "
"you will filter out emission on angular scales larger than "
f"{optimal_units(largest_ang_scales, [u.arcmin, u.arcsec, u.mas]):.3n}."]
return create_sensitivity_card('FoV limitations', temp_msg)
def summary_card_rms(app, obs):
"""Creates a summary card showing the reached sensitivity.
"""
rms = optimal_units(obs.thermal_noise(), [u.MJy, u.kJy, u.Jy, u.mJy, u.uJy])
rms_time = optimal_units(obs.thermal_noise()/np.sqrt(obs.inttime/obs.duration),
[u.MJy, u.kJy, u.Jy, u.mJy, u.uJy])
rms_channel = optimal_units(rms*np.sqrt(obs.subbands*obs.channels),
[u.MJy, u.kJy, u.Jy, u.mJy, u.uJy])
temp_msg = [html.Div(className='row', style={'height': '0.7rem'}),
html.Div(className='row justify-content-center',
children=html.Img(src=app.get_asset_url("waves.svg"), width='100%',
height='75rem', style={'display': 'inline'}))]
temp_msg += [html.P(f"Considering the sensitivities of the antennas, "
f"the estimated thermal noise is {rms:.3n}/beam.")]
temp_msg += [html.P(f"This would imply a rms of {rms_channel:.3n}/beam per spectral "
f"channel, or approx. {rms_time:.3n}/beam per time integration "
f"({optimal_units(obs.inttime, [u.s,u.ms,u.us]):.3n}).")]
return create_sensitivity_card('Sensitivity', temp_msg)
#################################################################################
# Some small graphical elements
def ellipse(bmaj, bmin, pa, color='#a01d26', z_index=1, position='relative', margin_top=''):
"""Returns a html.Div element that draws an ellipse with a semimajor axis bmaj,
semiminor axis bmin, and position angle (as defined in radio astronomy) pa.
bmaj,bmin, pa must be strings recognized by HTML/CSS.
"""