Browse Source

New Feature: guest observing times

Now the app can find at which times the source is visible by a given number of antennas (in the GUI by default three).
tags/v2.0.3^0
Benito Marcote 5 months ago
parent
commit
c518c2e1fe
7 changed files with 296 additions and 78 deletions
  1. +15
    -0
      CHANGES.txt
  2. +2
    -2
      README.md
  3. +175
    -74
      app.py
  4. +8
    -0
      assets/style.css
  5. +1
    -1
      tests/test.py
  6. +1
    -1
      vlbiplanobs/graphical_elements.py
  7. +94
    -0
      vlbiplanobs/observation.py

+ 15
- 0
CHANGES.txt View File

@@ -1,6 +1,21 @@
v2.0.3 2020-12-XX -- Bug fix: The option of only one subband is back.
-- Minor wording updates.
-- Velocity information added to the summary (vel. resolution in full bandwidth and per channel).
-- Kunming added as a possible e-EVN station.
-- Dash GUI changes: the setup options are now always visible and the observation can be recomputed
when the user is in a different tab.
-- Dash GUI changes: Instead of asking for start and end times of the observation, now it asks for
start time (date and time) and duration of the observation to simplify user input.
-- Dash GUI changes: (finally!) fixed the slight offset in height between the start date and start time.
-- Fixed example in README that had one comma missing and one extra bracket.
--
--
--
--
--
--
--
--
v2.0.2 2020-11-30 -- Updated the SEFD from Kunming as it was previously wrong.
-- Minor tweaks in the graphical (Dash) interface for a better interaction with the user.
-- Now sliders also match the color style of the rest of the page.


+ 2
- 2
README.md View File

@@ -91,8 +91,8 @@ stations18cm = my_stations.stations_with_band('18cm')

# Finally, you can set the observation
obs = observation.Observation(target=source,
times=observation.Time('1967-04-17 10:00') + np.arange(0, 600, 15)*u.min), # list of times covering the observation.
band='18cm' # must be a string with the format XXcm.
times=observation.Time('1967-04-17 10:00') + np.arange(0, 600, 15)*u.min, # list of times covering the observation.
band='18cm', # must be a string with the format XXcm.
datarate=1024, # Mbps
subbands=8, # no. subbands (or IFs).
channels=64, # no. of channels per subband


+ 175
- 74
app.py View File

@@ -1,4 +1,4 @@
#! /usr/bin/env python
#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""EVN Observation Planner.

@@ -331,6 +331,8 @@ def update_onsourcetime_label(n_clicks, a_wavelength):
'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")],
type="dot"),
html.Div(id='div-antenna-selection-button2', children=[]),
html.Label('Your observing Band'),
*ge.tooltip(idname='popover-band',
@@ -365,35 +367,55 @@ def update_onsourcetime_label(n_clicks, a_wavelength):
html.Small(id='error_source', style={'color': '#999999'},
className='form-text'),
]),
html.Div(className='form-group', 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."),
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", 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.Div(className='form-group', children=[
html.Label('Duration of the observation (hours)'),
*ge.tooltip(idname='popover-duration', message="Select the total duration of the "
"observation (provided in hours)."),
dcc.Input(id='duration', value=None, type='number', className='form-control',
placeholder="In hours", persistence=True, inputMode='numeric'),
html.Small(id='error_duration', style={'color': 'red'}, className='form-text text-muted')
dbc.Tabs(children=[
dbc.Tab(label='Pick Epoch', id='tab-pick-epoch', tabClassName='tab-for-card', children=[
html.Div(className='form-group', children=[
dbc.Card(className='card-no-left-border', children=dbc.CardBody([
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."),
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", 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.Div(className='form-group', children=[
html.Label('Duration of the observation (hours)'),
*ge.tooltip(idname='popover-duration', message="Select the total duration of the "
"observation (provided in hours)."),
dcc.Input(id='duration', value=None, type='number', className='form-control',
placeholder="In hours", persistence=True, inputMode='numeric'),
html.Small(id='error_duration', style={'color': 'red'}, className='form-text text-muted')
])]))
]),
]),
dbc.Tab(label='Guest Times', id='tab-guest-times', tabClassName='tab-for-card', children=[
html.Div(className='form-group', children=[
dbc.Card(className='card-no-left-border', children=dbc.CardBody([
html.P("Choose this option if you just want to find out when your source "
"will be visible. It will pick the time range when more than 3 antennas "
"can observe."),
dbc.Checklist(id='guest-times', className='checkbox', persistence=True,
options=[{'label': " I don't have preferred times",
'value': 'guest-times'}], value=[]),
html.Small("Note that this option may not provide the wished results if "
"different networks far apart (e.g. LBA + EVN) are selected.",
style={'color': '#999999'})
])
)])
])
]),
html.Div(className='form-group', children=[
html.Label(id='onsourcetime-label',
@@ -474,7 +496,7 @@ def update_onsourcetime_label(n_clicks, a_wavelength):
html.P(["Here you can set up your observation.", html.Br(),
"Please select which network (or networks) you want to use in your "
"observations, or select a customized array of antennas. "
"On the left you can set the basic information from your observations: "
"On the left panel you can set the basic information from your observations: "
"times of the observations and target source to observe. ", html.Br(),
"Optionally, you can customize the configuration and correlation parameters "
"under 'advance setup'. Otherwise, default values based on your selection "
@@ -620,6 +642,21 @@ def update_onsourcetime_label(n_clicks, a_wavelength):



@app.callback([Output('tab-pick-epoch', 'label'),
Output('tab-guest-times', 'label')],
[Input('guest-times', 'value')])
def update_tab_time_labels(guest_time):
"""Updates the labels in the tabs where the user can either pick a specific observing
time or let the app to guest the correct times.
It will add a green tick or red cross to the option that is currently selected.
"""
if guest_time:
return "Pick Epoch ", "Guest Times ✔️"
else:
return "Pick Epoch ✔️", "Guest Times ❌"



@app.callback([Output('div-antenna-selection-button', 'children'),
Output('div-antenna-selection-button2', 'children')],
[Input('tabs', 'value')])
@@ -627,13 +664,13 @@ def move_compute_button(selected_tab):
"""Depending on which tab is selected, it will show the button to compute the observation
in one place or another, so it is always visible and clickable.
"""
if selected_tab == 'tab-setup':
if selected_tab == 'tab-setup' or selected_tab == 'tab-doc':
return html.Button('Compute Observation', id='antenna-selection-button',
className='btn btn-primary btn-lg'), html.Div('', style={'height': '2.3rem'})
else:
return html.Div('', style={'height': '2.3rem'}), html.Button('Compute again',
id='antenna-selection-button', className='btn btn-primary btn-lg',
style={'width': '100%', 'margin-bottom': '10px'}),
style={'width': '100%', 'margin-bottom': '1rem'}),


@app.callback(Output('onsourcetime-label', 'children'),
@@ -725,6 +762,7 @@ def get_source(source_coord):


@app.callback([Output('loading-output', 'children'),
Output('loading-output2', 'children'),
Output('sensitivity-output', 'children'),
Output('fig-elev-time', 'figure'),
Output('fig-ant-time', 'figure'),
@@ -740,32 +778,58 @@ def get_source(source_coord):
State('subbands', 'value'),
State('channels', 'value'),
State('pols', 'value'),
State('inttime', 'value')] + \
State('inttime', 'value'),
State('guest-times', 'value'),
State('tabs', 'value')] + \
[State(f"check_{s.codename}", 'checked') for s in all_antennas])
def compute_observation(n_clicks, band, starttime, starthour, duration, source,
onsourcetime, datarate, subbands, channels, pols, inttime, *ants):



def compute_observation(n_clicks, band, starttime, starthour, duration, source, onsourcetime,
datarate, subbands, channels, pols, inttime, guest_time, selected_tab, *ants):
"""Computes all products to be shown concerning the set observation.
"""
# To decide where to put the output message
out_center = selected_tab == 'tab-setup' or selected_tab == 'tab-doc'
if n_clicks is None:
return '', dash.no_update, dash.no_update, dash.no_update, \
return '', '', dash.no_update, dash.no_update, dash.no_update, \
dash.no_update, dash.no_update

# All options must be completed
if None in (band, starttime, starthour, duration, source, datarate, subbands, channels, pols, inttime) \
or source == "":
missing = [label for label,atr in zip(('observing band', 'target source', 'start observing date', 'start observing time', 'duration of the observation', 'data rate', 'number of subbands', 'number of channels', 'number of polarizations', 'integration time'), (band, source, starttime, starthour, duration, datarate, subbands, channels, pols, inttime)) if (atr is None) or (atr == "")]
return alert_message(["Complete all fields and options before computing the observation.\n" +\
f"Currently it is missing: {', '.join(missing)}."]), \
if not guest_time:
# All options must be completed
if None in (band, starttime, starthour, duration, source, datarate, subbands, channels, pols, inttime) \
or source == "":
missing = [label for label,atr in zip(('observing band', 'target source', 'start observing date',
'start observing time', 'duration of the observation', 'data rate', 'number of subbands',
'number of channels', 'number of polarizations', 'integration time'), (band, source,
starttime, starthour, duration, datarate, subbands, channels, pols, inttime)) \
if (atr is None) or (atr == "")]
temp = [alert_message(["Complete all fields and options before computing the observation.\n" + \
f"Currently it is missing: {', '.join(missing)}."]), '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
else:
# All options but the ones related to the observing epoch must be completed
if None in (band, source, datarate, subbands, channels, pols, inttime) or source == "":
missing = [label for label,atr in zip(('observing band', 'target source', 'data rate',
'number of subbands', 'number of channels', 'number of polarizations',
'integration time'), (band, source, starttime, starthour, duration, datarate, subbands,
channels, pols, inttime)) if (atr is None) or (atr == "")]
temp = [alert_message(["Complete all fields and options before computing the observation.\n" + \
f"Currently it is missing: {', '.join(missing)}."]), '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

if ants.count(True) == 0:
return alert_message(["You need to select the antennas you wish to observe your source. " \
"Either manually or by selected a default VLBI network at your top left."]), \
temp = [alert_message(["You need to select the antennas you wish to observe your source. " \
"Either manually or by selected a default VLBI network at your top left."]), '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
# A single antenna computation is not supported
if ants.count(True) == 1:
return alert_message(["Single-antenna computations are not suported. " \
"Please choose at least two antennas"]), \
temp = [alert_message(["Single-antenna computations are not suported. " \
"Please choose at least two antennas"]), dash.no_update]
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

try:
@@ -774,29 +838,57 @@ def compute_observation(n_clicks, band, starttime, starthour, duration, source,
try:
target_source = observation.Source(coord.get_icrs_coordinates(source), source)
except coord.name_resolve.NameResolveError as e:
return alert_message(["Wrong source name or coordinates.", html.Br(),
temp = [alert_message(["Wrong source name or coordinates.", html.Br(),
"Either the source name hasn't been found or the coordinates format is incorrect."]), \
"First, set correctly an observation in the previous tab.", \
dash.no_update, dash.no_update, dash.no_update, dash.no_update
try:
time0 = Time(dt.strptime(f"{starttime} {starthour}", '%Y-%m-%d %H:%M'),
format='datetime', scale='utc')
except ValueError as e:
return alert_message("Incorrect format for starttime."), \
"First, set correctly an observation in the previous tab.", \
dash.no_update, dash.no_update, dash.no_update, dash.no_update
"First, set correctly an observation in the previous tab.", '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
if guest_time:
try:
if starttime is not None:
utc_times, _ = observation.Observation.guest_times_for_source(target_source,
stations.Stations('Observation', itertools.compress(all_antennas, ants)),
Time(dt.strptime(f"{starttime} 00:00", "%Y-%m-%d %H:%M"), format='datetime',
scale='utc'))
else:
utc_times, _ = observation.Observation.guest_times_for_source(target_source,
stations.Stations('Observation', itertools.compress(all_antennas, ants)))
except observation.SourceNotVisible:
temp = [alert_message([
html.P(["Your source cannot be observed within the arranged observation.",
html.Br(),
"There are no antennas that can simultaneously observe your source "
"during the given observing time."]),
html.P("Modify the observing time or change the selected antennas"
" to observe this source.")], title="Warning!"), '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

obs_times = utc_times[0] + np.linspace(0, (utc_times[1]-utc_times[0]).to(u.min).value, 50)*u.min
else:
try:
time0 = Time(dt.strptime(f"{starttime} {starthour}", '%Y-%m-%d %H:%M'),
format='datetime', scale='utc')
except ValueError as e:
temp = [alert_message("Incorrect format for starttime."), \
"First, set correctly an observation in the previous tab.", '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

if duration <= 0.0:
return alert_message("The duration of the observation must be a positive number of hours"), \
"First, set correctly an observation in the previous tab.", \
dash.no_update, dash.no_update, dash.no_update, dash.no_update
if duration <= 0.0:
temp = [alert_message("The duration of the observation must be a positive number of hours"), \
"First, set correctly an observation in the previous tab.", '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

if duration > 4*24.0:
return alert_message("Please, set an observation that lasts for less than 4 days."), \
"First, set correctly an observation in the previous tab.", \
dash.no_update, dash.no_update, dash.no_update, dash.no_update
if duration > 4*24.0:
temp = [alert_message("Please, set an observation that lasts for less than 4 days."), \
"First, set correctly an observation in the previous tab.", '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

obs_times = time0 + np.linspace(0, duration*60, 50)*u.min

obs_times = time0 + np.linspace(0, duration*60, 50)*u.min
try:
obs = observation.Observation(target=target_source, times=obs_times, band=band,
datarate=datarate, subbands=subbands, channels=channels,
@@ -805,20 +897,26 @@ def compute_observation(n_clicks, band, starttime, starthour, duration, source,
itertools.compress(all_antennas, ants)))
sensitivity_results = update_sensitivity(obs)
except observation.SourceNotVisible:
return alert_message([
temp = [alert_message([
html.P(["Your source cannot be observed within the arranged observation.",
html.Br(),
"There are no antennas that can simultaneously observe your source "
"during the given observing time."]),
html.P("Modify the observing time or change the selected antennas"
" to observe this source.")], title="Warning!"), \
" to observe this source.")], title="Warning!"), '']
return *[temp if out_center else temp[::-1]][0], \
dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update

# TODO: parallelize all these functions
return [html.Br(),
dbc.Alert("You can check now the results in the different tabs", color='info', \
dismissable=True)], sensitivity_results, \
get_fig_ant_elev(obs), get_fig_ant_up(obs), get_fig_uvplane(obs), dash.no_update
# TODO: parallelize all these fig functions
if out_center:
return [html.Br(), dbc.Alert("You can check now the results in the different tabs", color='info', \
dismissable=True),
*alert_message("Note that you have selected the 'guest time' option. "
"The inserted times and durations are ignored.")], '', \
sensitivity_results, get_fig_ant_elev(obs), get_fig_ant_up(obs), get_fig_uvplane(obs), dash.no_update
else:
return '', dbc.Alert("Results have been updated.", color='info', dismissable=True), \
sensitivity_results, get_fig_ant_elev(obs), get_fig_ant_up(obs), get_fig_uvplane(obs), dash.no_update



@@ -859,6 +957,8 @@ def get_fig_ant_elev(obs):





def get_fig_ant_up(obs):
data_fig = []
data_dict = obs.is_visible()
@@ -921,6 +1021,7 @@ def get_fig_uvplane(obs):
'color': 'black', 'zeroline': False}}}



def get_fig_dirty_map(obs):
pass

@@ -930,6 +1031,6 @@ def get_fig_dirty_map(obs):
if __name__ == '__main__':
# app.run_server(host='0.0.0.0', debug=True)
# app.run_server(debug=True)
app.run_server(host='0.0.0.0', debug=True)
app.run_server(host='0.0.0.0')



+ 8
- 0
assets/style.css View File

@@ -3709,6 +3709,14 @@ img[alt=equation2] {
}


.card-no-left-border {
border-left: 0px;
border: 1px solid #CCCCCC !important;
}

.tab-for-card > .active {
border: 1px solid #CCCCCC !important;
}

.card.card-antenna {
background-color: white !important;


+ 1
- 1
tests/test.py View File

@@ -31,7 +31,7 @@ obs.channels = 32
obs.polarizations = 2
obs.inttime = 2

all_stations = stations.Stations.get_stations_from_configfile(f"data/stations_catalog.inp")
all_stations = stations.Stations.get_stations_from_configfile()

def get_selected_antennas(list_of_selected_antennas):
"""Given a list of antenna codenames, it returns a Stations object containing


+ 1
- 1
vlbiplanobs/graphical_elements.py View File

@@ -133,7 +133,7 @@ def summary_card_antennas(app, obs):
ant_text += [html.Span(", ")]

temp_msg += [html.P(className='text-danger', children=["Note that ", *ant_text[:-1],
" cannot observe the source."])]
" cannot observe the source during the planned observation."])]

longest_bl = obs.longest_baseline()
ant_l1, ant_l2 = longest_bl[0].split('-')


+ 94
- 0
vlbiplanobs/observation.py View File

@@ -1,4 +1,5 @@
import numpy as np
import datetime as dt
from astropy import units as u
from astropy import coordinates as coord
from astropy.time import Time, TimeDelta
@@ -488,6 +489,99 @@ class Observation(object):
return iv


@staticmethod
def guest_times_for_source(target: Source, stations: Stations, date: Time = None, min_stations : int = 3) \
-> tuple:
"""Use this function to discover when your target source can be observed by the given network
of stations. It will return the start and end time of the possible observation (both in UTC and GST).
Note that while this gives you specific dates, the main interest would likely be obtaining the
actual GST range, which can be retrieved by running '.sidereal_time('mean', 'greenwich')'
on each returned time.

Inputs
- target : Source
The target source to be observed.
- stations : Stations
The array of stations that will potentially observe the target source.
- date : astropy.time.Time [OPTIONAL]
In case you have a specific date which you want to use to compute the possible start time
for the observation (i.e. the start time will be determined within the given date).
If not provided, today will be used.
- min_stations : int
Minimum number of stations required to considered the time as useful in the observation.

Returns
- (t0, t1) : tuple with two astropy.time.Time objects.
The start and end (UTC) time of the period of time when the source is visible by enough stations.
- (gst0, gst1) : tuple with two astropy.time.Time objects.
The start and end (GST) time of the same period of time.

Exceptions
- It may raise the exception SourceNotVisible if the target source is not visible by
enough stations.
"""
if date is None:
dtdate = dt.datetime.today()
else:
dtdate = date.datetime

t0 = Time(dt.datetime(dtdate.year, dtdate.month, dtdate.day, 0, 0), format='datetime', scale='utc')
obstimes = t0 + np.arange(0, 24*60, 10)*u.min

mm = np.zeros((len(stations), len(obstimes)))
for s,a_station in enumerate(stations):
mm[s, a_station.is_visible(obstimes, target)] = 1

n_onsource = mm.sum(0)
# go through it storing the starting or ending times when more than min_stations can observe the source

vis_ranges = []
for i in range(1, len(obstimes)):
# Only record the positions where the source goes from being visible to not, or from no to yes.
if (n_onsource[i] >= 3 and n_onsource[i-1] < 3) or (n_onsource[i] < 3 and n_onsource[i-1] >= 3):
vis_ranges.append(i)

# Recovers the interval where most of the antennas are available to be reported
# Another option was to take the longest interval with >= antennas. But this one may be useless
# under some specific conditions
n_max = [None, None, -1] # t0, t1, n_ants
indexes = 0 if n_onsource[0] >= 3 else 1

# Special case: first element
if indexes == 0:
n_max = [-1, 0, np.max([n_onsource[vis_ranges[-1]:].max(), n_onsource[:vis_ranges[0]].max()])]

for i in range(1-indexes, len(vis_ranges)-1, 2):
print(vis_ranges)
print(n_onsource)
if n_onsource[vis_ranges[i-1+indexes]:vis_ranges[i+indexes]+1].max() > n_max[2]:
n_max = [i-1+indexes, i+indexes, n_onsource[vis_ranges[i-1+indexes]:vis_ranges[i+indexes]+1].max()]
elif (n_onsource[vis_ranges[i-1+indexes]:vis_ranges[i+indexes]+1].max() == n_max[2]) and \
(vis_ranges[i+indexes]-vis_ranges[i-1+indexes] > n_max[1]-n_max[0]):
n_max = [i-1+indexes, i+indexes, n_onsource[vis_ranges[i-1+indexes]:vis_ranges[i+indexes]+1].max()]

if None in (n_max[0], n_max[1]):
# Either the source is visible all the time or never
if n_onsource[0] >= 3:
best_utc = obstimes[0], obstimes[-1]
best_gtc = best_utc[0].sidereal_time('mean', 'greenwich'), \
best_utc[1].sidereal_time('mean', 'greenwich')
else:
raise SourceNotVisible
else:
if n_max[0] == -1:
best_utc = obstimes[vis_ranges[n_max[0]]], obstimes[vis_ranges[n_max[1]]] + 1*u.day
else:
best_utc = obstimes[vis_ranges[n_max[0]]], obstimes[vis_ranges[n_max[1]]]

best_gtc = best_utc[0].sidereal_time('mean', 'greenwich'), \
best_utc[1].sidereal_time('mean', 'greenwich')

return best_utc, best_gtc




def longest_baseline(self) -> tuple:
"""Returns the longest baseline in the observation.



Loading…
Cancel
Save