calculator.py 20.6 KB
Newer Older
sarrvesh's avatar
sarrvesh committed
1 2
"""Main file for LUCI"""

sarrvesh's avatar
sarrvesh committed
3
__author__ = "Sarrvesh S. Sridhar"
sarrvesh's avatar
sarrvesh committed
4
__email__ = "sarrvesh@astron.nl"
sarrvesh's avatar
sarrvesh committed
5

sarrvesh's avatar
sarrvesh committed
6 7
from random import randint
import os
sarrvesh's avatar
sarrvesh committed
8 9
import dash
import dash_bootstrap_components as dbc
sarrvesh's avatar
sarrvesh committed
10
from dash.dependencies import Input, Output, State
sarrvesh's avatar
sarrvesh committed
11
import flask
12
from gui import layout
sarrvesh's avatar
sarrvesh committed
13
import backend as bk
14
import targetvis as tv
15
import generatepdf as g
sarrvesh's avatar
sarrvesh committed
16 17

# Initialize the dash app
sarrvesh's avatar
sarrvesh committed
18 19
server = flask.Flask(__name__)
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUMEN], \
20 21 22
                server=server, url_base_pathname='/luci/')
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True
sarrvesh's avatar
sarrvesh committed
23 24 25 26

#######################################
# Setup the layout of the web interface
#######################################
27
app.layout = layout
sarrvesh's avatar
sarrvesh committed
28
app.title = 'LUCI - LOFAR Unified Calculator for Imaging'
sarrvesh's avatar
sarrvesh committed
29

30 31 32 33 34
##############################################
# TODO: Move all callbacks to a separate file
# See https://community.plot.ly/t/dash-callback-in-a-separate-file/14122/16
##############################################

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
##############################################
# Show observational setup fields based on 
# obsMode dropdown value
##############################################
@app.callback(
    [Output('tabModeForm', 'style'),
     Output('tabModeRowL', 'style'),
     Output('tabModeRow', 'style'),
     Output('tabModeRow', 'value'),
     
     Output('stokesForm', 'style'),
     Output('stokesRowL', 'style'),
     Output('stokesRow', 'style'),
     
     Output('nRingsForm', 'style'),
     Output('nRingsRow', 'style'),
     Output('nRingsRowL', 'style'),
     
     Output('pipeTypeRow', 'options'),
54 55 56 57
     Output('pipeTypeRow', 'value'),
     
     Output('imNoiseRowL', 'style'),
     Output('imNoiseRow', 'style')
58 59 60
    ],
    [Input('obsModeRow', 'value')]
)
61
def toggle_obs_mode(obs_value):
62 63 64 65 66 67
    """Function to show relevant observational setup fields
       depending on the user's choice"""
    all_pipelines = {
        'Interferometric': ['none', 'preprocessing'],
        'Beamformed': ['none', 'pulp']
    }
68 69 70
    valid_pipes = [{'label':i, 'value':i} for i in all_pipelines[obs_value]]
    if obs_value == 'Interferometric':
        return {'display':'none'}, {'display':'none'}, {'display':'none'}, 'Incoherent', \
71 72
               {'display':'none'}, {'display':'none'}, {'display':'none'}, \
               {'display':'none'}, {'display':'none'}, {'display':'none'}, \
73 74
               valid_pipes, 'none', \
               {'display':'block'}, {'display':'block'}
75
    else:
76
        return {}, {'display':'block'}, {'display':'block'}, 'Incoherent', \
77 78
               {}, {'display':'block'}, {'display':'block'}, \
               {}, {'display':'block'}, {'display':'block'}, \
79 80
               valid_pipes, 'none', \
               {'display':'none'}, {'display':'none'}
81 82 83 84 85 86

################################################
# Show TAb stokes fields based on dropdown value
################################################
@app.callback(
    [Output('stokesRow', 'options'), 
87 88 89 90 91 92 93 94 95
     Output('stokesRow', 'value'),
     
     Output('nRemoteForm', 'style'),
     Output('nRemoteRow', 'style'),
     Output('nRemoteRowL', 'style'),
     
     Output('nIntForm', 'style'),
     Output('nIntRow', 'style'),
     Output('nIntRowL', 'style'),
96 97 98 99 100 101 102 103 104 105 106 107 108
    ],
    [Input('tabModeRow', 'value')]
)
def toggle_stokes(value):
    """Function to show relevant Stokes products depending
       on the user's TAB choice"""
    if value == '':
        value = 'Coherent'
    all_stokes = {
        'Coherent': ['I', 'IQUV', 'XXYY'],
        'Incoherent': ['I', 'IQUV']
    }
    valid_stokes = [{'label':i, 'value':i} for i in all_stokes[value]]
109 110 111 112 113 114 115 116
    if value == 'Incoherent':
        return valid_stokes, 'I', \
               {}, {'display':'block'}, {'display':'block'}, \
               {}, {'display':'block'}, {'display':'block'}
    else:
        return valid_stokes, 'I', \
               {'display':'none'}, {'display':'none'}, {'display':'none'}, \
               {'display':'none'}, {'display':'none'}, {'display':'none'}, 
117 118


119 120 121 122
##############################################
# Show pipeline fields based on dropdown value
##############################################
@app.callback(
sarrvesh's avatar
sarrvesh committed
123 124 125 126 127 128 129 130 131 132
    [Output('tAvgRowL', 'style'),
     Output('tAvgRow', 'style'),
     Output('fAvgRowL', 'style'),
     Output('fAvgRow', 'style'),
     Output('dyCompressRowL', 'style'),
     Output('dyCompressRow', 'style'),
     Output('pipeSizeRowL', 'style'),
     Output('pipeSizeRow', 'style'),
     Output('pipeProcTimeRow', 'style'),
     Output('pipeProcTimeRowL', 'style')
133
    ],
sarrvesh's avatar
sarrvesh committed
134
    [Input('pipeTypeRow', 'value')]
135 136 137 138 139 140
)
def toggle_pipeline(value):
    """Function to show relevant pipeline fields depending on
       the user's pipeline choice."""
    if value == 'none':
        return {'display':'none'}, {'display':'none'}, \
141 142
               {'display':'none'}, {'display':'none'}, \
               {'display':'none'}, {'display':'none'}, \
143 144
               {'display':'none'}, {'display':'none'}, \
               {'display':'none'}, {'display':'none'}
145
    elif value == 'preprocessing':
146
        return {'display':'block'}, {'display':'block'}, \
147 148
               {'display':'block'}, {'display':'block'}, \
               {'display':'block'}, {'display':'block'}, \
149 150 151
               {'display':'block'}, {'display':'block'}, \
               {'display':'block'}, {'display':'block'}

152 153 154 155
#######################################
# Validate time averaging factor
#######################################
@app.callback(
sarrvesh's avatar
sarrvesh committed
156 157 158 159 160 161 162
    Output('msgboxTAvg', 'is_open'),
    [Input('tAvgRow', 'n_blur'),
     Input('mbtAvgClose', 'n_clicks')
    ],
    [State('tAvgRow', 'value'),
     State('msgboxTAvg', 'is_open')
    ]
163
)
sarrvesh's avatar
sarrvesh committed
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
def validate_t_avg(n_blur, n_clicks, value, is_open):
    """Validate time averaging factor and display error message if needed"""
    if is_open is True and n_clicks is not None:
        # The message box is open and the user has clicked the close
        # button. Close the alert message
        return False
    if n_blur is None:
        # The page is loading. Do not validate anything
        return False
    else:
        # Text box has lost focus.
        # Go ahead and validate the text in it.
        try:
            int(str(value))
        except ValueError:
            return True
        return False
181 182 183 184 185

#######################################
# Validate freq averaging factor
#######################################
@app.callback(
sarrvesh's avatar
sarrvesh committed
186 187 188 189 190 191 192
    Output('msgboxFAvg', 'is_open'),
    [Input('fAvgRow', 'n_blur'),
     Input('mbfAvgClose', 'n_clicks')
    ],
    [State('fAvgRow', 'value'),
     State('msgboxFAvg', 'is_open')
    ]
193
)
sarrvesh's avatar
sarrvesh committed
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
def validate_t_avg(n_blur, n_clicks, value, is_open):
    """Validate frequency averaging factor and display error message if needed"""
    if is_open is True and n_clicks is not None:
        # The message box is open and the user has clicked the close
        # button. Close the alert message
        return False
    if n_blur is None:
        # The page is loading. Do not validate anything
        return False
    else:
        # Text box has lost focus.
        # Go ahead and validate the text in it.
        try:
            int(str(value))
        except ValueError:
            return True
        return False
211

212 213 214 215
#######################################
# What should the resolve button do?
#######################################
@app.callback(
sarrvesh's avatar
sarrvesh committed
216 217 218 219 220 221 222 223 224
    [Output('coordRow', 'value'),
     Output('msgboxResolve', 'is_open')
    ],
    [Input('resolve', 'n_clicks'),
     Input('mbResolveClose', 'n_clicks')
    ],
    [State('targetNameRow', 'value'),
     State('msgboxResolve', 'is_open')
    ]
225
)
sarrvesh's avatar
sarrvesh committed
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
def on_resolve_click(n, close_msg_box, target_name, is_open):
    """Function defines what to do when the resolve button is clicked"""
    if is_open is True and close_msg_box is not None:
        # The message box is open and the user has clicked the close
        # button. Close the alert message
        return '', False
    if n is None:
        # The page has just loaded.
        return '', False
    else:
        # Resole button has been clicked
        coord_str = tv.resolve_source(target_name)
        if coord_str is None:
            # Display error message.
            return '', True
        else:
            return coord_str, False
243

sarrvesh's avatar
sarrvesh committed
244 245 246 247
#######################################
# What should the export button do?
#######################################
@app.callback(
sarrvesh's avatar
sarrvesh committed
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    [Output('download-link', 'style'),
     Output('download-link', 'href'),
     Output('msgboxGenPdf', 'is_open')
    ],
    [Input('genpdf', 'n_clicks'),
     Input('mbGenPdfClose', 'n_clicks')
    ],
    [State('obsTimeRow', 'value'),
     State('nCoreRow', 'value'),
     State('nRemoteRow', 'value'),
     State('nIntRow', 'value'),
     State('nChanRow', 'value'),
     State('nSbRow', 'value'),
     State('intTimeRow', 'value'),
     State('hbaDualRow', 'value'),

     State('pipeTypeRow', 'value'),
     State('tAvgRow', 'value'),
     State('fAvgRow', 'value'),
     State('dyCompressRow', 'value'),

     State('imNoiseRow', 'value'),
     State('rawSizeRow', 'value'),
     State('pipeSizeRow', 'value'),
     State('pipeProcTimeRow', 'value'),

     State('msgboxGenPdf', 'is_open'),

     State('elevation-plot', 'figure'),
sarrvesh's avatar
sarrvesh committed
277
     State('distance-table', 'figure'),
sarrvesh's avatar
sarrvesh committed
278

sarrvesh's avatar
sarrvesh committed
279 280 281 282 283
     State('dateRow', 'date'),
     
     State('obsModeRow', 'value'),
     State('tabModeRow', 'value'),
     State('stokesRow', 'value')
sarrvesh's avatar
sarrvesh committed
284
    ]
sarrvesh's avatar
sarrvesh committed
285
)
sarrvesh's avatar
sarrvesh committed
286 287 288
def on_genpdf_click(n_clicks, close_msg_box, obs_t, n_core, n_remote, n_int, n_chan,
                    n_sb, integ_t, ant_set, pipe_type, t_avg, f_avg, is_dysco,
                    im_noise_val, raw_size, proc_size, pipe_time, is_msg_box_open,
sarrvesh's avatar
sarrvesh committed
289
                    elevation_fig, distance_table, obs_date, obs_mode, tab_mode, stokes):
sarrvesh's avatar
sarrvesh committed
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
    """Function defines what to do when the generate pdf button is clicked"""
    if is_msg_box_open is True and close_msg_box is not None:
        # The message box is open and the user has clicked the close
        # button. Close the alert message.
        return {'display':'none'}, '', False
    if n_clicks is None:
        # Generate button has not been clicked. Hide the download link
        return {'display':'none'}, '', False
    else:
        if im_noise_val is '':
            # User has clicked generate PDF button before calculate
            return {'display':'none'}, '', True
        else:
            # Generate a random number so that this user's pdf can be stored here
            randnum = '{:05d}'.format(randint(0, 10000))
            rel_path = 'static/'
            # Generate a relative and absolute filenames to the pdf file
            rel_path = os.path.join(rel_path, 'summary_{}.pdf'.format(randnum))
            abs_path = os.path.join(os.getcwd(), rel_path)
            g.generate_pdf(rel_path, obs_t, n_core, n_remote, n_int, n_chan,
                           n_sb, integ_t, ant_set, pipe_type, t_avg, f_avg,
                           is_dysco, im_noise_val, raw_size, proc_size, pipe_time,
sarrvesh's avatar
sarrvesh committed
312 313
                           elevation_fig, distance_table, obs_date, 
                           obs_mode, tab_mode, stokes)
sarrvesh's avatar
sarrvesh committed
314
            return {'display':'block'}, '/luci/{}'.format(rel_path), False
sarrvesh's avatar
sarrvesh committed
315

sarrvesh's avatar
sarrvesh committed
316 317
@app.server.route('/luci/static/<resource>')
def serve_static(resource):
sarrvesh's avatar
sarrvesh committed
318
    path = os.path.join(os.getcwd(), 'static')
sarrvesh's avatar
sarrvesh committed
319
    return flask.send_from_directory(path, resource)
sarrvesh's avatar
sarrvesh committed
320

sarrvesh's avatar
sarrvesh committed
321 322 323 324
#######################################
# What should the submit button do?
#######################################
@app.callback(
sarrvesh's avatar
sarrvesh committed
325 326 327 328 329 330 331 332 333 334 335 336
    [Output('imNoiseRow', 'value'),
     Output('rawSizeRow', 'value'),
     Output('pipeSizeRow', 'value'),
     Output('pipeProcTimeRow', 'value'),
     Output('msgBoxBody', 'children'),
     Output('msgbox', 'is_open'),
     Output('elevation-plot', 'style'),
     Output('elevation-plot', 'figure'),
     Output('beam-plot', 'style'),
     Output('beam-plot', 'figure'),
     Output('distance-table', 'style'),
     Output('distance-table', 'figure')
337
    ],
sarrvesh's avatar
sarrvesh committed
338 339
    [Input('calculate', 'n_clicks'),
     Input('msgBoxClose', 'n_clicks'),
sarrvesh's avatar
sarrvesh committed
340
    ],
sarrvesh's avatar
sarrvesh committed
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
    [State('obsTimeRow', 'value'),
     State('nCoreRow', 'value'),
     State('nRemoteRow', 'value'),
     State('nIntRow', 'value'),
     State('nChanRow', 'value'),
     State('nSbRow', 'value'),
     State('intTimeRow', 'value'),
     State('hbaDualRow', 'value'),
     State('pipeTypeRow', 'value'),
     State('tAvgRow', 'value'),
     State('fAvgRow', 'value'),
     State('dyCompressRow', 'value'),
     State('msgbox', 'is_open'),
     State('targetNameRow', 'value'),
     State('coordRow', 'value'),
     State('dateRow', 'date'),
     State('calListRow', 'value'),
sarrvesh's avatar
sarrvesh committed
358 359 360 361
     State('demixListRow', 'value'),
     State('obsModeRow', 'value'),
     State('tabModeRow', 'value'),
     State('stokesRow', 'value')
sarrvesh's avatar
sarrvesh committed
362 363
    ]
)
sarrvesh's avatar
sarrvesh committed
364 365 366
def on_calculate_click(n, n_clicks, obs_t, n_core, n_remote, n_int, n_chan, n_sb,
                       integ_t, hba_mode, pipe_type, t_avg, f_avg, dy_compress,
                       is_open, src_name, coord, obs_date, calib_names,
sarrvesh's avatar
sarrvesh committed
367
                       ateam_names, obs_mode, tab_mode, stokes):
sarrvesh's avatar
sarrvesh committed
368
    """Function defines what to do when the calculate button is clicked"""
369
    if is_open is True:
sarrvesh's avatar
sarrvesh committed
370 371 372 373
        # User has closed the error message box
        return '', '', '', '', '', False, \
               {'display':'none'}, {}, {'display':'none'}, \
               {}, {'display':'none'}, {}
sarrvesh's avatar
sarrvesh committed
374 375 376
    if n is None:
        # Calculate button has not been clicked yet
        # So, do nothing and set default values to results field
sarrvesh's avatar
sarrvesh committed
377
        return '', '', '', '', '', False, \
sarrvesh's avatar
sarrvesh committed
378 379
               {'display':'none'}, {}, {'display':'none'}, {}, \
               {'display':'none'}, {}
sarrvesh's avatar
sarrvesh committed
380 381 382
    else:
        # Calculate button has been clicked.
        # First, validate all command line inputs
383

sarrvesh's avatar
sarrvesh committed
384 385
        # If the user sets n_core, n_remote, or n_int to 0, dash return None.
        # Why is this?
386
        # Correct this manually, for now.
sarrvesh's avatar
sarrvesh committed
387 388 389 390 391 392 393 394
        if n_core is None:
            n_core = '0'
        if n_remote is None:
            n_remote = '0'
        if n_int is None:
            n_int = '0'
        status, msg = bk.validate_inputs(obs_t, int(n_core), int(n_remote), \
                                         int(n_int), n_sb, integ_t, t_avg, f_avg, \
395 396
                                         src_name, coord, hba_mode, pipe_type, \
                                         ateam_names)
sarrvesh's avatar
sarrvesh committed
397
        if status is False:
sarrvesh's avatar
sarrvesh committed
398 399 400
            return '', '', '', '', msg, True, \
                   {'display':'none'}, {}, {'display':'none'}, {}, \
                   {'display':'none'}, {}
sarrvesh's avatar
sarrvesh committed
401
        else:
sarrvesh's avatar
sarrvesh committed
402 403 404 405 406 407
            # Estimate the raw data size
            n_baselines = bk.compute_baselines(int(n_core), int(n_remote),
                                               int(n_int), hba_mode)
            im_noise = bk.calculate_im_noise(int(n_core), int(n_remote),
                                             int(n_int), hba_mode, float(obs_t),
                                             int(n_sb))
sarrvesh's avatar
sarrvesh committed
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427

            if obs_mode == 'Interferometric':
                # Calculate interferometric raw size
                raw_size = bk.calculate_raw_size(float(obs_t), float(integ_t),
                                                 n_baselines, int(n_chan), int(n_sb))
            if obs_mode == 'Beamformed':
                # Calculate beamformed datasize
                if stokes == 'I':
                    n_pol = 1
                    n_value = 1
                if stokes == 'IQUV':
                    n_pol = 4
                    n_value = 1
                if stokes == 'XXYY':
                    n_pol = 2
                    n_value =2
                raw_size = bk.calculate_bf_size(int(n_sb), int(n_chan), \
                                                n_pol, n_value, float(integ_t), \
                                                float(obs_t))
                
sarrvesh's avatar
sarrvesh committed
428 429 430
            if pipe_type == 'none':
                # No pipeline
                pipe_time = None
sarrvesh's avatar
sarrvesh committed
431
                avg_size = 0
sarrvesh's avatar
sarrvesh committed
432 433 434 435
            else:
                pipe_time = bk.calculate_pipe_time(float(obs_t), int(n_sb),
                                                   hba_mode, ateam_names,
                                                   pipe_type)
sarrvesh's avatar
sarrvesh committed
436 437 438 439
                avg_size = bk.calculate_proc_size(float(obs_t), float(integ_t),
                                                  n_baselines, int(n_chan), int(n_sb),
                                                  pipe_type, int(t_avg), int(f_avg),
                                                  dy_compress)
sarrvesh's avatar
sarrvesh committed
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458

            # It is useful to have coord as a list from now on
            if coord is not '':
                coord_list = coord.split(',')
                coord_input_list = coord.split(',')

            # Add calibrator names to the target list so that they can be
            # plotted together. Before doing that, make a copy of the input
            # target list and its coordinates
            src_name_input = src_name
            coord_input = coord
            if calib_names is not None:
                for i in range(len(calib_names)):
                    if i == 0 and src_name is None:
                        src_name = '{}'.format(calib_names[i])
                        coord_list = [tv.CALIB_COORDINATES[calib_names[i]]]
                    else:
                        src_name += ', {}'.format(calib_names[i])
                        coord_list.append(tv.CALIB_COORDINATES[calib_names[i]])
sarrvesh's avatar
sarrvesh committed
459

sarrvesh's avatar
sarrvesh committed
460 461 462 463 464 465 466 467 468 469
            # Add A-team names to the target list so that they can be
            # plotted together
            if ateam_names is not None:
                for i in range(len(ateam_names)):
                    if i == 0 and src_name is None:
                        src_name = '{}'.format(ateam_names[i])
                        coord_list = [tv.ATEAM_COORDINATES[ateam_names[i]]]
                    else:
                        src_name += ', {}'.format(ateam_names[i])
                        coord_list.append(tv.ATEAM_COORDINATES[ateam_names[i]])
sarrvesh's avatar
sarrvesh committed
470

sarrvesh's avatar
sarrvesh committed
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
            if coord is '':
                # No source is specified under Target setup
                display_fig = {'display':'none'}
                elevation_fig = {}
                beam_fig = {}
                display_tab = {'display':'none'}
                distance_tab = {}
            else:
                # User has specified a coordinate and it has passed validation
                # in the validate_inputs function.
                # Check if the number of SAPs is less than 488
                n_point = len(coord_input_list)
                n_sap = n_point * int(n_sb)
                max_sap = 488
                if n_sap > max_sap:
                    msg = 'Number of targets times number of subbands cannot ' + \
                          'be greater than {}.'.format(max_sap)
                    return '', '', '', '', msg, True, \
                           {'display':'none'}, {}, {'display':'none'}, {}, \
                           {'display':'none'}, {}
                # Find target elevation across a 24-hour period
                data = tv.find_target_elevation(src_name, coord_list,
                                                obs_date, int(n_int))
                display_fig = {'display':'block', 'height':600}
                elevation_fig = {'data':data,
                                 'layout':{
                                     'xaxis':{'title':'Time (UTC)'},
                                     'yaxis':{'title':'Elevation'},
                                     'title':'Target visibility plot',
                                     'shapes':[]
                                 }
                                }
                elevation_fig = tv.add_sun_rise_and_set_times(obs_date,
                                                              int(n_int),
                                                              elevation_fig)
                # Find the position of the station and tile beam
                beam_fig = tv.find_beam_layout(src_name_input, coord_input, \
                                   int(n_core), int(n_remote), int(n_int), hba_mode)
                # Calculate distance between all the targets and offending sources
                display_tab = {'display':'block'}
                table_data = [tv.make_distance_table(src_name_input,
                                                     coord_input, obs_date)]
                table_title = 'Angular distances in degrees between specified ' +\
                             'targets and other bright sources'
                distance_tab = {'data':table_data,
                                'layout':{'title':table_title, 'autosize':True}
                               }
sarrvesh's avatar
sarrvesh committed
518

sarrvesh's avatar
sarrvesh committed
519
            return im_noise, raw_size, avg_size, pipe_time, '', \
sarrvesh's avatar
sarrvesh committed
520 521
                   False, display_fig, elevation_fig, display_fig, beam_fig, \
                   display_tab, distance_tab
sarrvesh's avatar
sarrvesh committed
522 523

if __name__ == '__main__':
524 525 526
    app.run_server(debug=True, host='0.0.0.0', port=8051)
    #app.run_server(debug=False, host='0.0.0.0', port=8051, \
    #              dev_tools_ui=False, dev_tools_props_check=False)