vidavis 0.0.12__py3-none-any.whl → 0.0.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vidavis/__version__.py +1 -1
- vidavis/apps/_ms_raster.py +183 -290
- vidavis/plot/ms_plot/_check_raster_inputs.py +107 -0
- vidavis/plot/ms_plot/_locate_points.py +126 -12
- vidavis/plot/ms_plot/_ms_plot.py +182 -51
- vidavis/plot/ms_plot/_plot_inputs.py +19 -0
- vidavis/plot/ms_plot/_raster_plot_gui.py +50 -41
- vidavis/plot/ms_plot/_raster_plot_inputs.py +111 -104
- {vidavis-0.0.12.dist-info → vidavis-0.0.14.dist-info}/METADATA +1 -1
- {vidavis-0.0.12.dist-info → vidavis-0.0.14.dist-info}/RECORD +12 -10
- {vidavis-0.0.12.dist-info → vidavis-0.0.14.dist-info}/WHEEL +0 -0
- {vidavis-0.0.12.dist-info → vidavis-0.0.14.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Check inputs to MsRaster plot() or its GUI
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
from vidavis.plot.ms_plot._ms_plot_constants import VIS_AXIS_OPTIONS, AGGREGATOR_OPTIONS
|
|
6
|
+
|
|
7
|
+
def check_inputs(inputs):
|
|
8
|
+
''' Check plot input types, and axis (plot, agg, iter) input values. '''
|
|
9
|
+
_set_baseline_antenna_axis(inputs)
|
|
10
|
+
_check_axis_inputs(inputs)
|
|
11
|
+
_check_agg_inputs(inputs)
|
|
12
|
+
_check_color_inputs(inputs)
|
|
13
|
+
_check_other_inputs(inputs)
|
|
14
|
+
|
|
15
|
+
def _set_baseline_antenna_axis(inputs):
|
|
16
|
+
''' Set baseline axis to dimension in data_dims '''
|
|
17
|
+
if 'data_dims' not in inputs:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
data_dims = inputs['data_dims']
|
|
21
|
+
baseline_dim = 'antenna_name' if 'antenna_name' in data_dims else 'baseline'
|
|
22
|
+
|
|
23
|
+
# Convert baseline axis to existing baseline dimension
|
|
24
|
+
baseline_dims = ['baseline', 'antenna_name']
|
|
25
|
+
for axis in ['x_axis', 'y_axis', 'iter_axis', 'agg_axis']:
|
|
26
|
+
if inputs[axis] in baseline_dims:
|
|
27
|
+
inputs[axis] = baseline_dim
|
|
28
|
+
|
|
29
|
+
def _check_axis_inputs(inputs):
|
|
30
|
+
''' Check x_axis, y_axis, vis_axis, and iter_axis inputs. '''
|
|
31
|
+
x_axis = inputs['x_axis']
|
|
32
|
+
y_axis = inputs['y_axis']
|
|
33
|
+
if x_axis == y_axis:
|
|
34
|
+
raise ValueError(f"Invalid parameter values: x_axis {x_axis} cannot be same as y_axis {y_axis}.")
|
|
35
|
+
|
|
36
|
+
iter_axis = inputs['iter_axis']
|
|
37
|
+
if iter_axis and iter_axis in (x_axis, y_axis):
|
|
38
|
+
raise ValueError(f"Invalid parameter value: iter_axis {iter_axis} cannot be x_axis ({x_axis}) or y_axis ({y_axis}).")
|
|
39
|
+
|
|
40
|
+
data_dims = inputs['data_dims'] if 'data_dims' in inputs else None
|
|
41
|
+
if data_dims:
|
|
42
|
+
if x_axis not in data_dims or y_axis not in data_dims:
|
|
43
|
+
raise ValueError(f"Invalid parameter value: x and y axis must be a data dimension in {data_dims}.")
|
|
44
|
+
if iter_axis and iter_axis not in data_dims:
|
|
45
|
+
raise ValueError(f"Invalid parameter value: iter_axis {iter_axis} must be a data dimension in {data_dims}.")
|
|
46
|
+
|
|
47
|
+
vis_axis = inputs['vis_axis']
|
|
48
|
+
if vis_axis not in VIS_AXIS_OPTIONS:
|
|
49
|
+
raise ValueError(f"Invalid parameter value: vis_axis {vis_axis} must be one of {VIS_AXIS_OPTIONS}")
|
|
50
|
+
|
|
51
|
+
def _check_agg_inputs(inputs):
|
|
52
|
+
''' Check aggregator and agg_axis. Set agg_axis if not set. '''
|
|
53
|
+
aggregator = inputs['aggregator']
|
|
54
|
+
agg_axis = inputs['agg_axis']
|
|
55
|
+
|
|
56
|
+
x_axis = inputs['x_axis']
|
|
57
|
+
y_axis = inputs['y_axis']
|
|
58
|
+
data_dims = inputs['data_dims'] if 'data_dims' in inputs else None
|
|
59
|
+
|
|
60
|
+
if aggregator and aggregator not in AGGREGATOR_OPTIONS:
|
|
61
|
+
raise ValueError(f"Invalid parameter value: aggregator {aggregator} must be None or one of {AGGREGATOR_OPTIONS}.")
|
|
62
|
+
|
|
63
|
+
if agg_axis:
|
|
64
|
+
if not isinstance(agg_axis, str) and not isinstance(agg_axis, list):
|
|
65
|
+
raise TypeError(f"Invalid parameter type: agg_axis {agg_axis} must be str or list.")
|
|
66
|
+
|
|
67
|
+
# Make agg_axis a list
|
|
68
|
+
if isinstance(agg_axis, str):
|
|
69
|
+
agg_axis = [agg_axis]
|
|
70
|
+
|
|
71
|
+
for axis in agg_axis:
|
|
72
|
+
if axis in (x_axis, y_axis):
|
|
73
|
+
raise ValueError(f"Invalid parameter value: agg_axis {agg_axis} cannot be x_axis ({x_axis}) or y_axis ({y_axis}).")
|
|
74
|
+
if data_dims and axis not in data_dims:
|
|
75
|
+
raise ValueError(f"Invalid parameter value: agg_axis {axis} must be a data dimension in {data_dims}.")
|
|
76
|
+
elif aggregator and data_dims:
|
|
77
|
+
# Set agg_axis to non-plotted dim axes
|
|
78
|
+
agg_axis = data_dims.copy()
|
|
79
|
+
agg_axis.remove(x_axis)
|
|
80
|
+
agg_axis.remove(y_axis)
|
|
81
|
+
if 'iter_axis' in inputs and inputs['iter_axis']:
|
|
82
|
+
agg_axis.remove(inputs['iter_axis'])
|
|
83
|
+
inputs['agg_axis'] = agg_axis
|
|
84
|
+
|
|
85
|
+
def _check_color_inputs(inputs):
|
|
86
|
+
if inputs['color_mode']:
|
|
87
|
+
color_mode = inputs['color_mode'].lower()
|
|
88
|
+
valid_color_modes = ['auto', 'manual']
|
|
89
|
+
if color_mode not in valid_color_modes:
|
|
90
|
+
raise ValueError(f"Invalid parameter value: color_mode {color_mode} must be None or one of {valid_color_modes}.")
|
|
91
|
+
inputs['color_mode'] = color_mode
|
|
92
|
+
|
|
93
|
+
if inputs['color_range']:
|
|
94
|
+
if not (isinstance(inputs['color_range'], tuple) and len(inputs['color_range']) == 2):
|
|
95
|
+
raise ValueError("Invalid parameter type: color_range must be None or a tuple of (min, max).")
|
|
96
|
+
|
|
97
|
+
def _check_other_inputs(inputs):
|
|
98
|
+
if inputs['iter_range']:
|
|
99
|
+
if not (isinstance(inputs['iter_range'], tuple) and len(inputs['iter_range']) == 2):
|
|
100
|
+
raise ValueError("Invalid parameter type: iter_range must be None or a tuple of (start, end).")
|
|
101
|
+
|
|
102
|
+
if inputs['subplots']:
|
|
103
|
+
if not (isinstance(inputs['subplots'], tuple) and len(inputs['subplots']) == 2):
|
|
104
|
+
raise ValueError("Invalid parameter type: subplots must be None or a tuple of (rows, columns).")
|
|
105
|
+
|
|
106
|
+
if inputs['title'] and not isinstance(inputs['title'], str):
|
|
107
|
+
raise TypeError("Invalid parameter type: title must be None or a string.")
|
|
@@ -10,7 +10,94 @@ import panel as pn
|
|
|
10
10
|
|
|
11
11
|
from vidavis.plot.ms_plot._ms_plot_constants import TIME_FORMAT
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def cursor_changed(cursor, last_cursor):
|
|
14
|
+
''' Check whether cursor position changed '''
|
|
15
|
+
x, y = cursor
|
|
16
|
+
if not x and not y:
|
|
17
|
+
return False # not cursor callback
|
|
18
|
+
if last_cursor and last_cursor == (x, y):
|
|
19
|
+
return False # same cursor
|
|
20
|
+
return True # new cursor or cursor changed
|
|
21
|
+
|
|
22
|
+
def points_changed(data, last_points):
|
|
23
|
+
''' Check whether point positions changed '''
|
|
24
|
+
# No data = {'x': [], 'y': []}
|
|
25
|
+
if not data or (len(data['x']) == 0 and len(data['y']) == 0):
|
|
26
|
+
return False # not points callback
|
|
27
|
+
if last_points and last_points == data:
|
|
28
|
+
return False # same points
|
|
29
|
+
return True # new points, points changed, or points deleted
|
|
30
|
+
|
|
31
|
+
def box_changed(bounds, last_box):
|
|
32
|
+
''' Check whether box position changed '''
|
|
33
|
+
# No bounds = None
|
|
34
|
+
if not bounds:
|
|
35
|
+
return False # no data, not box select callback
|
|
36
|
+
if last_box and last_box == bounds:
|
|
37
|
+
return False # same box
|
|
38
|
+
return True # new box, box changed, or box deleted
|
|
39
|
+
|
|
40
|
+
def update_cursor_location(cursor, plot_axes, xds, cursor_locate_box):
|
|
41
|
+
''' Show data values for cursor x,y position in cursor location box (pn.WidgetBox) '''
|
|
42
|
+
# Convert plot values to selection values to select plot data
|
|
43
|
+
cursor_locate_box.clear()
|
|
44
|
+
|
|
45
|
+
x, y = cursor
|
|
46
|
+
x_axis, y_axis, vis_axis = plot_axes
|
|
47
|
+
cursor_position = {x_axis: x, y_axis: y}
|
|
48
|
+
cursor_location = _locate_point(xds, cursor_position, vis_axis)
|
|
49
|
+
|
|
50
|
+
location_column = pn.Column(pn.widgets.StaticText(name="Cursor Location"))
|
|
51
|
+
# Add row of columns to column layout
|
|
52
|
+
location_row = _layout_point_location(cursor_location)
|
|
53
|
+
location_column.append(location_row)
|
|
54
|
+
# Add location column to widget box
|
|
55
|
+
cursor_locate_box.append(location_column)
|
|
56
|
+
|
|
57
|
+
def update_points_location(data, plot_axes, xds, points_tab_column, logger):
|
|
58
|
+
''' Show data values for points in point_draw in tab and log '''
|
|
59
|
+
points_tab_column.clear()
|
|
60
|
+
if data:
|
|
61
|
+
x_axis, y_axis, vis_axis = plot_axes
|
|
62
|
+
logger.info("Locate selected points:")
|
|
63
|
+
|
|
64
|
+
for point in list(zip(data['x'], data['y'])):
|
|
65
|
+
# Locate point
|
|
66
|
+
point_position = {x_axis: point[0], y_axis: point[1]}
|
|
67
|
+
point_location = _locate_point(xds, point_position, vis_axis)
|
|
68
|
+
# Format location and add to points locate column
|
|
69
|
+
location_layout = _layout_point_location(point_location)
|
|
70
|
+
points_tab_column.append(location_layout)
|
|
71
|
+
points_tab_column.append(pn.layout.Divider())
|
|
72
|
+
|
|
73
|
+
# Format and add to log
|
|
74
|
+
location_list = [f"{static_text.name}={static_text.value}" for static_text in point_location]
|
|
75
|
+
logger.info(", ".join(location_list))
|
|
76
|
+
|
|
77
|
+
def update_box_location(bounds, plot_axes, xds, box_tab_column, logger):
|
|
78
|
+
''' Show data values for points in box_select in tab and log '''
|
|
79
|
+
box_tab_column.clear()
|
|
80
|
+
if bounds:
|
|
81
|
+
x_axis, y_axis, vis_axis = plot_axes
|
|
82
|
+
box_bounds = {x_axis: (bounds[0], bounds[2]), y_axis: (bounds[1], bounds[3])}
|
|
83
|
+
npoints, point_locations = _locate_box(xds, box_bounds, vis_axis)
|
|
84
|
+
|
|
85
|
+
message = f"Locate {npoints} points"
|
|
86
|
+
message += " (only first 100 shown):" if npoints > 100 else ":"
|
|
87
|
+
logger.info(message)
|
|
88
|
+
box_tab_column.append(pn.pane.Str(message))
|
|
89
|
+
|
|
90
|
+
for point in point_locations:
|
|
91
|
+
# Format and add to box locate column
|
|
92
|
+
location_layout = _layout_point_location(point)
|
|
93
|
+
box_tab_column.append(location_layout)
|
|
94
|
+
box_tab_column.append(pn.layout.Divider())
|
|
95
|
+
|
|
96
|
+
# Format and add to log
|
|
97
|
+
location_list = [f"{static_text.name}={static_text.value}" for static_text in point]
|
|
98
|
+
logger.info(", ".join(location_list))
|
|
99
|
+
|
|
100
|
+
def _locate_point(xds, position, vis_axis):
|
|
14
101
|
'''
|
|
15
102
|
Get cursor location as values of coordinates and data vars.
|
|
16
103
|
xds (Xarray Dataset): data for plot
|
|
@@ -22,15 +109,18 @@ def locate_point(xds, position, vis_axis):
|
|
|
22
109
|
static_text_list = []
|
|
23
110
|
values, units = _get_point_location(xds, position, vis_axis)
|
|
24
111
|
|
|
25
|
-
#
|
|
26
|
-
|
|
112
|
+
# List indexed coordinate int value with with str value
|
|
113
|
+
index_coords = {'baseline': 'baseline_name', 'antenna_name': 'antenna', 'polarization': 'polarization_name'}
|
|
27
114
|
for name, value in values.items():
|
|
28
|
-
|
|
115
|
+
if name in index_coords.values():
|
|
116
|
+
continue
|
|
117
|
+
if name in index_coords and isinstance(value, int):
|
|
118
|
+
value = f"{values[index_coords[name]]} ({value})" # append name to index
|
|
29
119
|
static_text = _get_location_text(name, value, units)
|
|
30
120
|
static_text_list.append(static_text)
|
|
31
121
|
return static_text_list
|
|
32
122
|
|
|
33
|
-
def
|
|
123
|
+
def _locate_box(xds, bounds, vis_axis):
|
|
34
124
|
'''
|
|
35
125
|
Get location of each point in box bounds as values of coordinate and data vars.
|
|
36
126
|
xds (Xarray Dataset): data for plot
|
|
@@ -47,7 +137,7 @@ def locate_box(xds, bounds, vis_axis):
|
|
|
47
137
|
selection = {}
|
|
48
138
|
for coord, val in bounds.items():
|
|
49
139
|
# Round index values to int for selection
|
|
50
|
-
selection[coord] = slice(
|
|
140
|
+
selection[coord] = slice(_get_selection_value(coord, val[0]), _get_selection_value(coord, val[1]))
|
|
51
141
|
sel_xds = xds.sel(indexers=None, method=None, tolerance=None, drop=False, **selection)
|
|
52
142
|
|
|
53
143
|
x_coord, y_coord = bounds.keys()
|
|
@@ -57,7 +147,7 @@ def locate_box(xds, bounds, vis_axis):
|
|
|
57
147
|
for y in sel_xds[y_coord].values:
|
|
58
148
|
for x in sel_xds[x_coord].values:
|
|
59
149
|
position = {x_coord: x, y_coord: y}
|
|
60
|
-
points.append(
|
|
150
|
+
points.append(_locate_point(sel_xds, position, vis_axis))
|
|
61
151
|
counter += 1
|
|
62
152
|
if counter == 100:
|
|
63
153
|
break
|
|
@@ -81,9 +171,9 @@ def _get_point_location(xds, position, vis_axis):
|
|
|
81
171
|
|
|
82
172
|
if xds:
|
|
83
173
|
try:
|
|
84
|
-
# Round index coordinates to int for selection
|
|
85
174
|
for coord, value in position.items():
|
|
86
|
-
|
|
175
|
+
# Round index coordinates to int and convert time to datetime if float for selection
|
|
176
|
+
position[coord] = _get_selection_value(coord, value)
|
|
87
177
|
|
|
88
178
|
sel_xds = xds.sel(indexers=None, method='nearest', tolerance=None, drop=False, **position)
|
|
89
179
|
for coord in sel_xds.coords:
|
|
@@ -112,9 +202,15 @@ def _get_point_location(xds, position, vis_axis):
|
|
|
112
202
|
values[vis_axis.upper()] = values.pop('VISIBILITY')
|
|
113
203
|
return values, units
|
|
114
204
|
|
|
115
|
-
def
|
|
116
|
-
'''
|
|
117
|
-
|
|
205
|
+
def _get_selection_value(coord, value):
|
|
206
|
+
''' Convert index coordinates to int and float time coordinate to datetime '''
|
|
207
|
+
if coord in ['baseline', 'antenna_name', 'polarization']:
|
|
208
|
+
# Round index coordinates to int for selecction
|
|
209
|
+
value = round(value)
|
|
210
|
+
elif coord == 'time' and isinstance(value, float):
|
|
211
|
+
# Bokeh datetime values are floating-point numbers: milliseconds since the Unix epoch
|
|
212
|
+
value = to_datetime(value, unit='ms', origin='unix')
|
|
213
|
+
return value
|
|
118
214
|
|
|
119
215
|
def _get_xda_val_unit(xda):
|
|
120
216
|
''' Return value and unit of xda (selected so only one value) '''
|
|
@@ -151,3 +247,21 @@ def _get_location_text(name, value, units):
|
|
|
151
247
|
units.pop(name) # no unit for datetime string
|
|
152
248
|
unit = units[name] if name in units else ""
|
|
153
249
|
return pn.widgets.StaticText(name=name, value=f"{value} {unit}")
|
|
250
|
+
|
|
251
|
+
def _layout_point_location(text_list):
|
|
252
|
+
''' Layout list of StaticText in row of columns containing 3 rows '''
|
|
253
|
+
location_row = pn.Row()
|
|
254
|
+
location_col = pn.Column()
|
|
255
|
+
|
|
256
|
+
for static_text in text_list:
|
|
257
|
+
# 3 entries per column; append to row and start new column
|
|
258
|
+
if len(location_col.objects) == 3:
|
|
259
|
+
location_row.append(location_col)
|
|
260
|
+
location_col = pn.Column()
|
|
261
|
+
|
|
262
|
+
static_text.margin = (0, 10) # default (5, 10)
|
|
263
|
+
location_col.append(static_text)
|
|
264
|
+
|
|
265
|
+
# Add last column
|
|
266
|
+
location_row.append(location_col)
|
|
267
|
+
return location_row
|
vidavis/plot/ms_plot/_ms_plot.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
|
|
8
8
|
from bokeh.io import export_png, export_svg
|
|
9
|
-
from bokeh.plotting import save
|
|
9
|
+
from bokeh.plotting import save
|
|
10
10
|
import hvplot
|
|
11
11
|
import holoviews as hv
|
|
12
12
|
import numpy as np
|
|
@@ -20,6 +20,7 @@ except ImportError:
|
|
|
20
20
|
_HAVE_TOOLVIPER = False
|
|
21
21
|
|
|
22
22
|
from vidavis.data.measurement_set._ms_data import MsData
|
|
23
|
+
from vidavis.plot.ms_plot._locate_points import cursor_changed, points_changed, box_changed, update_cursor_location, update_points_location, update_box_location
|
|
23
24
|
from vidavis.toolbox import AppContext, get_logger
|
|
24
25
|
|
|
25
26
|
class MsPlot:
|
|
@@ -45,28 +46,42 @@ class MsPlot:
|
|
|
45
46
|
# Set up temp dir for output html files
|
|
46
47
|
self._app_context = AppContext(app_name)
|
|
47
48
|
|
|
48
|
-
if show_gui:
|
|
49
|
-
# Enable "toast" notifications
|
|
50
|
-
pn.config.notifications = True
|
|
51
|
-
self._toast = None # for destroy() with new plot or new notification
|
|
52
|
-
|
|
53
49
|
# Initialize plot inputs and params
|
|
54
|
-
self._plot_inputs =
|
|
55
|
-
self._plot_params = None
|
|
50
|
+
self._plot_inputs = None # object to manage plot inputs
|
|
56
51
|
|
|
57
52
|
# Initialize plots
|
|
58
53
|
self._plot_init = False
|
|
59
|
-
self._plots_locked = False
|
|
60
54
|
self._plots = []
|
|
55
|
+
self._last_plot = None
|
|
56
|
+
self._plot_params = [] # for plot inputs tab
|
|
61
57
|
|
|
62
|
-
# Initialize gui
|
|
63
58
|
if show_gui:
|
|
59
|
+
# Enable "toast" notifications
|
|
60
|
+
pn.config.notifications = True
|
|
61
|
+
self._toast = None # for destroy() with new plot or new notification
|
|
62
|
+
|
|
63
|
+
# Initialize gui panel for callbacks
|
|
64
|
+
self._gui_plot_data = None
|
|
65
|
+
self._gui_selection = {}
|
|
64
66
|
self._gui_layout = None
|
|
65
67
|
self._first_gui_plot = True
|
|
66
|
-
|
|
68
|
+
|
|
69
|
+
# For _update_plot callback: check if inputs changed
|
|
70
|
+
self._last_plot_inputs = None
|
|
71
|
+
self._last_style_inputs = None
|
|
72
|
+
|
|
73
|
+
# For locate callback: check if points changed
|
|
74
|
+
self._plot_axes = None
|
|
75
|
+
self._last_cursor = None
|
|
76
|
+
self._last_points = None
|
|
77
|
+
self._last_box = None
|
|
78
|
+
|
|
79
|
+
# Initialize non-gui show() panel for callbacks
|
|
80
|
+
self._show_layout = None
|
|
81
|
+
self._plot_data = None
|
|
67
82
|
|
|
68
83
|
# Set data (if ms)
|
|
69
|
-
self.
|
|
84
|
+
self._ms_data = None
|
|
70
85
|
self._ms_info = {}
|
|
71
86
|
self._set_ms(ms)
|
|
72
87
|
# pylint: enable=too-many-arguments, too-many-positional-arguments
|
|
@@ -83,15 +98,15 @@ class MsPlot:
|
|
|
83
98
|
'field_name', 'source_name', 'field_coords', 'start_frequency', 'end_frequency'
|
|
84
99
|
Returns: list of unique values when single column is requested, else None
|
|
85
100
|
'''
|
|
86
|
-
if self.
|
|
87
|
-
self.
|
|
101
|
+
if self._ms_data:
|
|
102
|
+
self._ms_data.summary(data_group, columns)
|
|
88
103
|
else:
|
|
89
104
|
self._logger.error("Error: MS path has not been set")
|
|
90
105
|
|
|
91
106
|
def data_groups(self):
|
|
92
107
|
''' Returns set of data groups from all ProcessingSet ms_xds. '''
|
|
93
|
-
if self.
|
|
94
|
-
return self.
|
|
108
|
+
if self._ms_data:
|
|
109
|
+
return self._ms_data.data_groups()
|
|
95
110
|
self._logger.error("Error: MS path has not been set")
|
|
96
111
|
return None
|
|
97
112
|
|
|
@@ -100,8 +115,8 @@ class MsPlot:
|
|
|
100
115
|
Dimension options include 'time', 'baseline' (for visibility data), 'antenna' (for spectrum data), 'antenna1',
|
|
101
116
|
'antenna2', 'frequency', 'polarization'.
|
|
102
117
|
'''
|
|
103
|
-
if self.
|
|
104
|
-
return self.
|
|
118
|
+
if self._ms_data:
|
|
119
|
+
return self._ms_data.get_dimension_values(dimension)
|
|
105
120
|
self._logger.error("Error: MS path has not been set")
|
|
106
121
|
return None
|
|
107
122
|
|
|
@@ -109,8 +124,8 @@ class MsPlot:
|
|
|
109
124
|
''' Plot antenna positions.
|
|
110
125
|
label_antennas (bool): label positions with antenna names.
|
|
111
126
|
'''
|
|
112
|
-
if self.
|
|
113
|
-
self.
|
|
127
|
+
if self._ms_data:
|
|
128
|
+
self._ms_data.plot_antennas(label_antennas)
|
|
114
129
|
else:
|
|
115
130
|
self._logger.error("Error: MS path has not been set")
|
|
116
131
|
|
|
@@ -119,23 +134,33 @@ class MsPlot:
|
|
|
119
134
|
data_group (str): data group to use for field and source xds.
|
|
120
135
|
label_fields (bool): label all fields on the plot if True, else label central field only
|
|
121
136
|
'''
|
|
122
|
-
if self.
|
|
123
|
-
self.
|
|
137
|
+
if self._ms_data:
|
|
138
|
+
self._ms_data.plot_phase_centers(data_group, label_fields)
|
|
124
139
|
else:
|
|
125
140
|
self._logger.error("Error: MS path has not been set")
|
|
126
141
|
|
|
127
142
|
def clear_plots(self):
|
|
128
143
|
''' Clear plot list '''
|
|
129
|
-
while self._plots_locked:
|
|
130
|
-
time.sleep(1)
|
|
131
144
|
self._plots.clear()
|
|
145
|
+
self._plot_params.clear()
|
|
146
|
+
self._plot_axes = None
|
|
147
|
+
|
|
148
|
+
def unlink_plot_streams(self):
|
|
149
|
+
''' Disconnect streams when plot data is going to be replaced '''
|
|
150
|
+
if self._show_layout and len(self._show_layout.objects) == 4:
|
|
151
|
+
# Remove dmap (streams with callback) from previous plot
|
|
152
|
+
self._show_layout[0][0] = self._last_plot.opts(tools=['hover'])
|
|
153
|
+
# Remove locate widgets
|
|
154
|
+
self._show_layout[0].pop(1) # cursor locate box
|
|
155
|
+
self._show_layout.pop(3) # box locate tab
|
|
156
|
+
self._show_layout.pop(2) # points locate tab
|
|
132
157
|
|
|
133
158
|
def clear_selection(self):
|
|
134
159
|
''' Clear data selection and restore original ProcessingSet '''
|
|
135
|
-
if self.
|
|
136
|
-
self.
|
|
160
|
+
if self._ms_data:
|
|
161
|
+
self._ms_data.clear_selection()
|
|
137
162
|
|
|
138
|
-
self._plot_inputs
|
|
163
|
+
self._plot_inputs.remove_input('selection')
|
|
139
164
|
|
|
140
165
|
def show(self):
|
|
141
166
|
'''
|
|
@@ -144,25 +169,62 @@ class MsPlot:
|
|
|
144
169
|
if not self._plots:
|
|
145
170
|
raise RuntimeError("No plots to show. Run plot() to create plot.")
|
|
146
171
|
|
|
147
|
-
# Do not delete plot list until rendered
|
|
148
|
-
self._plots_locked = True
|
|
149
|
-
|
|
150
172
|
# Single plot or combine plots into layout using subplots (rows, columns)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# Render plot as Bokeh Figure or GridPlot so can show() in script without tying up thread
|
|
154
|
-
bokeh_fig = hv.render(layout_plot)
|
|
173
|
+
subplots = self._plot_inputs.get_input('subplots')
|
|
174
|
+
layout_plot = self._layout_plots(subplots)
|
|
155
175
|
|
|
156
|
-
|
|
176
|
+
# Add plot inputs column tab
|
|
177
|
+
inputs_column = None
|
|
157
178
|
if self._plot_params:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
inputs_column = pn.Column()
|
|
180
|
+
self._fill_inputs_column(inputs_column)
|
|
181
|
+
|
|
182
|
+
# Show plot and plot inputs in tabs
|
|
183
|
+
if self._plot_inputs.is_layout():
|
|
184
|
+
self._show_layout = pn.Tabs(('Plot', layout_plot))
|
|
185
|
+
if inputs_column:
|
|
186
|
+
self._show_layout.append(('Plot Inputs', inputs_column))
|
|
164
187
|
else:
|
|
165
|
-
|
|
188
|
+
plot = layout_plot.opts(
|
|
189
|
+
hv.opts.QuadMesh(
|
|
190
|
+
tools=['hover', 'box_select'],
|
|
191
|
+
selection_fill_alpha=0.2, # dim selected areas of plot
|
|
192
|
+
nonselection_fill_alpha=1.0, # do not dim unselected areas of plot
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
# Add DynamicMap for streams for single plot
|
|
196
|
+
points = hv.Points([]).opts(
|
|
197
|
+
size=5,
|
|
198
|
+
fill_color='white'
|
|
199
|
+
)
|
|
200
|
+
dmap = hv.DynamicMap(
|
|
201
|
+
self._locate,
|
|
202
|
+
streams=[
|
|
203
|
+
hv.streams.PointerXY(), # cursor location (x, y)
|
|
204
|
+
hv.streams.PointDraw(source=points), # fixed points location (data)
|
|
205
|
+
hv.streams.BoundsXY() # box location (bounds)
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Create panel layout
|
|
210
|
+
self._show_layout = pn.Tabs(
|
|
211
|
+
('Plot', pn.Column(
|
|
212
|
+
plot * dmap * points,
|
|
213
|
+
pn.WidgetBox(), # cursor info
|
|
214
|
+
)
|
|
215
|
+
),
|
|
216
|
+
sizing_mode='stretch_width',
|
|
217
|
+
)
|
|
218
|
+
if inputs_column:
|
|
219
|
+
self._show_layout.append(('Plot Inputs', inputs_column))
|
|
220
|
+
self._show_layout.append(('Locate Selected Points', pn.Column()))
|
|
221
|
+
self._show_layout.append(('Locate Selected Box', pn.Column()))
|
|
222
|
+
|
|
223
|
+
# return value for locate callback
|
|
224
|
+
self._last_plot = plot
|
|
225
|
+
|
|
226
|
+
# Show panel layout
|
|
227
|
+
self._show_layout.show(title=self._app_name, threaded=True)
|
|
166
228
|
|
|
167
229
|
def save(self, filename='ms_plot.png', fmt='auto', width=900, height=600):
|
|
168
230
|
'''
|
|
@@ -182,11 +244,14 @@ class MsPlot:
|
|
|
182
244
|
|
|
183
245
|
# Combine plots into layout using subplots (rows, columns) if not single plot.
|
|
184
246
|
# Set fixed size for export.
|
|
185
|
-
|
|
247
|
+
subplots = self._plot_inputs.get_input('subplots')
|
|
248
|
+
layout_plot = self._layout_plots(subplots, (width, height))
|
|
186
249
|
|
|
187
|
-
|
|
250
|
+
iter_axis = self._plot_inputs.get_input('iter_axis')
|
|
251
|
+
if not isinstance(layout_plot, hv.Layout) and iter_axis:
|
|
188
252
|
# Save iterated plots individually, with index appended to filename
|
|
189
|
-
|
|
253
|
+
iter_range = self._plot_inputs.get_input('iter_range')
|
|
254
|
+
plot_idx = 0 if iter_range is None else iter_range[0]
|
|
190
255
|
for plot in self._plots:
|
|
191
256
|
exportname = f"{name}_{plot_idx}{ext}"
|
|
192
257
|
self._save_plot(plot, exportname, fmt)
|
|
@@ -255,22 +320,22 @@ class MsPlot:
|
|
|
255
320
|
Return whether ms changed (false if ms_path is None, not set yet), even if error. '''
|
|
256
321
|
self._ms_info['ms'] = ms_path
|
|
257
322
|
ms_error = ""
|
|
258
|
-
if not ms_path or (self.
|
|
323
|
+
if not ms_path or (self._ms_data and self._ms_data.is_ms_path(ms_path)):
|
|
259
324
|
return False
|
|
260
325
|
|
|
261
326
|
try:
|
|
262
327
|
# Set new MS data
|
|
263
|
-
self.
|
|
264
|
-
data_path = self.
|
|
328
|
+
self._ms_data = MsData(ms_path, self._logger)
|
|
329
|
+
data_path = self._ms_data.get_path()
|
|
265
330
|
self._ms_info['ms'] = data_path
|
|
266
331
|
root, ext = os.path.splitext(os.path.basename(data_path))
|
|
267
332
|
while ext != '':
|
|
268
333
|
root, ext = os.path.splitext(root)
|
|
269
334
|
self._ms_info['basename'] = root
|
|
270
|
-
self._ms_info['data_dims'] = self.
|
|
335
|
+
self._ms_info['data_dims'] = self._ms_data.get_data_dimensions()
|
|
271
336
|
except RuntimeError as e:
|
|
272
337
|
ms_error = str(e)
|
|
273
|
-
self.
|
|
338
|
+
self._ms_data = None
|
|
274
339
|
if ms_error:
|
|
275
340
|
self._notify(ms_error, 'error', 0)
|
|
276
341
|
return True
|
|
@@ -309,4 +374,70 @@ class MsPlot:
|
|
|
309
374
|
del plot_inputs[key]
|
|
310
375
|
except KeyError:
|
|
311
376
|
pass
|
|
312
|
-
self._plot_params
|
|
377
|
+
if not self._plot_params:
|
|
378
|
+
self._plot_params = plot_inputs
|
|
379
|
+
else:
|
|
380
|
+
for param, value in self._plot_params.items():
|
|
381
|
+
if plot_inputs[param] != value:
|
|
382
|
+
if isinstance(value, list):
|
|
383
|
+
# append new value to existing list if not repeat
|
|
384
|
+
if plot_inputs[param] != value[-1]:
|
|
385
|
+
value.append(plot_inputs[param])
|
|
386
|
+
else:
|
|
387
|
+
# make list to include new value
|
|
388
|
+
value = [value, plot_inputs[param]]
|
|
389
|
+
self._plot_params[param] = value
|
|
390
|
+
|
|
391
|
+
def _fill_inputs_column(self, inputs_tab_column):
|
|
392
|
+
''' Format plot inputs and list in Panel column '''
|
|
393
|
+
if self._plot_params:
|
|
394
|
+
inputs_tab_column.clear()
|
|
395
|
+
plot_params = sorted([f"{key}={value}" for key, value in self._plot_params.items()])
|
|
396
|
+
for param in plot_params:
|
|
397
|
+
str_pane = pn.pane.Str(param)
|
|
398
|
+
str_pane.margin = (0, 10)
|
|
399
|
+
inputs_tab_column.append(str_pane)
|
|
400
|
+
|
|
401
|
+
def _get_plot_axes(self):
|
|
402
|
+
''' Return x, y, vis axes '''
|
|
403
|
+
if not self._plot_axes:
|
|
404
|
+
x_axis = self._plot_inputs.get_input('x_axis')
|
|
405
|
+
y_axis = self._plot_inputs.get_input('y_axis')
|
|
406
|
+
vis_axis = self._plot_inputs.get_input('vis_axis')
|
|
407
|
+
self._plot_axes = (x_axis, y_axis, vis_axis)
|
|
408
|
+
return self._plot_axes
|
|
409
|
+
|
|
410
|
+
def _locate(self, x, y, data, bounds):
|
|
411
|
+
''' Callback for all show plot streams '''
|
|
412
|
+
self._locate_cursor(x, y, self._plot_data, self._show_layout)
|
|
413
|
+
self._locate_points(data, self._plot_data, self._show_layout)
|
|
414
|
+
self._locate_box(bounds, self._plot_data, self._show_layout)
|
|
415
|
+
return self._last_plot
|
|
416
|
+
|
|
417
|
+
def _locate_cursor(self, x, y, plot_data, tabs):
|
|
418
|
+
''' Show location from cursor position in cursor locate box '''
|
|
419
|
+
cursor = (x, y)
|
|
420
|
+
if cursor_changed(cursor, self._last_cursor):
|
|
421
|
+
# new cursor position - update cursor location box
|
|
422
|
+
plot_axes = self._get_plot_axes()
|
|
423
|
+
cursor_box = tabs[0][1]
|
|
424
|
+
update_cursor_location(cursor, plot_axes, plot_data, cursor_box)
|
|
425
|
+
self._last_cursor = cursor
|
|
426
|
+
|
|
427
|
+
def _locate_points(self, point_data, plot_data, tabs):
|
|
428
|
+
''' Show points locations from point_draw tool '''
|
|
429
|
+
if points_changed(point_data, self._last_points):
|
|
430
|
+
# new points position - update selected points location tab
|
|
431
|
+
plot_axes = self._get_plot_axes()
|
|
432
|
+
points_tab = tabs[2]
|
|
433
|
+
update_points_location(point_data, plot_axes, plot_data, points_tab, self._logger)
|
|
434
|
+
self._last_points = point_data
|
|
435
|
+
|
|
436
|
+
def _locate_box(self, box_bounds, plot_data, tabs):
|
|
437
|
+
''' Show points locations in box from box_select tool '''
|
|
438
|
+
if box_changed(box_bounds, self._last_box):
|
|
439
|
+
# new box_select position - update selected box location tab
|
|
440
|
+
plot_axes = self._get_plot_axes()
|
|
441
|
+
box_tab = tabs[3]
|
|
442
|
+
update_box_location(box_bounds, plot_axes, plot_data, box_tab, self._logger)
|
|
443
|
+
self._last_box = box_bounds
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
''' Utilities for inputs to MeasurementSet plots '''
|
|
2
|
+
|
|
3
|
+
def inputs_changed(plot_inputs, last_plot_inputs):
|
|
4
|
+
''' Check if inputs changed and need new plot '''
|
|
5
|
+
if not last_plot_inputs:
|
|
6
|
+
return True
|
|
7
|
+
|
|
8
|
+
for key, val in plot_inputs.items():
|
|
9
|
+
if not _values_equal(val, last_plot_inputs[key]):
|
|
10
|
+
return True
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
def _values_equal(val1, val2):
|
|
14
|
+
''' Test if values are set and equal, or not set (cannot compare value with None) '''
|
|
15
|
+
if val1 is not None and val2 is not None: # both set
|
|
16
|
+
return val1 == val2
|
|
17
|
+
if val1 is None and val2 is None: # both None
|
|
18
|
+
return True
|
|
19
|
+
return False # one set and other is None
|