GUI

The gui package provides support for establishing graphical user interfaces (GUIs) by means of a scriptable GUI and a DOOCS property server. The contents of this package is shown below:

import doocspie


help(doocspie.gui)
Help on package doocspie.gui in doocspie:

NAME
    doocspie.gui - Support modules for graphical user interfaces (GUIs).

PACKAGE CONTENTS
    datalyzer (package)
    property_server

FILE
    /home/cbehrens/Home/Repositories/gitlab/doocspie/doocspie/gui/__init__.py


In the following, we show how to configure the scriptable GUI, which is called datalyzer and is very similar to the TrainAbo discussed in an earlier section of this documentation. Additionally, we explain how the PropertyServer class can be utilized in conjunction with a dedicated DOOCS property server for data visualization with the Java DOOCS Data Display (JDDD).

Datalyzer

The scriptable GUI for data visualization is realized by the Datalyzer class of the datalyzer sub-package, and its documentation, which is very similar to the TrainAbo, is presented here:

help(doocspie.gui.datalyzer.Datalyzer)
Help on class Datalyzer in module doocspie.gui.datalyzer:

class Datalyzer(builtins.object)
 |  Datalyzer(properties=None, timeout_seconds=10, buffer_size=32, allow_zero_events=False)
 |  
 |  Datalyzer class for providing a scriptable graphical user interface (GUI).
 |  
 |  This class provides the methods for providing a scriptable GUI for data visualization.
 |  
 |  Methods defined here:
 |  
 |  __init__(self, properties=None, timeout_seconds=10, buffer_size=32, allow_zero_events=False)
 |      Constructor of the datalyzer class.
 |      
 |      This constructor initializes the instance with optional parameters (see 'Args' below).
 |      
 |      Args:
 |          properties (tuple or dict, optional): The optional properties to get train events with readouts for.
 |          timeout_seconds (int): The optional timeout seconds for unsuccessful readout.
 |          buffer_size (int): The optional number of buffers to be used for storing previous readouts.
 |          allow_zero_events (bool, optional): The optional state for allow zero train events.
 |  
 |  __str__(self)
 |      Special method to return a properly formatted string representation of the datalyzer.
 |  
 |  add(self, address, label=None, offset=None, timestamp_event=None, meta_event=None, start=None, elements=None)
 |      Add address with optional parameters to the properties for synchronous readout.
 |      
 |      Args:
 |          address (str): The DOOCS address to add to the properties for synchronous readout.
 |          label (str, optional): Optional label for DOOCS address as a means for alternative property access.
 |          offset (int, optional): Optional offset to be applied on event numbers for synchronization.
 |          timestamp_event (bool, optional): Optional state to determine using the timestamp as an alternative event.
 |          meta_event (str, optional): Optional Meta property to replace the default event with.
 |          start (int, optional): Optional start position for reading out array-like types.
 |          elements (int, optional): Optional number of elements to read out for array-like types.
 |      
 |      Returns:
 |          None
 |      
 |      Raises:
 |          DoocspieException: Doocspie related exception for address duplication.
 |  
 |  add_axis_labels(self, source, x_label=None, y_label=None)
 |      Add axis labels to the given data source (either a property added or a registered function).
 |      
 |      Args:
 |          source (str): The data source (property or registered function) to add the axis labels to.
 |          x_label (str, optional): Optional x-label for the given source.
 |          y_label (str, optional): Optional y-label for the given source.
 |      
 |      Returns:
 |          None
 |  
 |  register(self, arg)
 |      Decorator with optional label supplied via argument for registration of a function.
 |      
 |      Args:
 |          arg (str or function): Either a label (str) attached to the function or the function itself.
 |      
 |      Returns:
 |          None
 |      
 |      Raises:
 |          DoocspieException: Doocspie related exception for an already registered function label.
 |  
 |  startup(self)
 |      Start up the datalyzer GUI application.
 |      
 |      Returns:
 |          None
 |  
 |  update(self)
 |      Update the newly added properties to for synchronous readout.
 |      
 |      Returns:
 |          None
 |  
 |  ----------------------------------------------------------------------
 |  Readonly properties defined here:
 |  
 |  actual_properties
 |      tuple: The properties that can actually be readout in the datalyzer.
 |  
 |  registered_functions
 |      tuple: The registered functions that can be used for synchronized readout in the datalyzer.
 |  
 |  unusable_properties
 |      tuple: The properties that cannot be used for synchronized readout in the datalyzer.
 |  
 |  usable_properties
 |      tuple: The properties that can be used for synchronized readout in the datalyzer.
 |  
 |  zero_event_properties
 |      tuple: The properties that have an event number of 'zero'.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

The Datalyzer class can simply be imported directly from the doocspie.gui subpackage as is presented below. In the first code block, we show again how the TrainAbo can be configured for synchronized readout of DOOCS properties and looping callback functions. Subsequent analysis and plotting must be provide by third-party tools. The second code block then demonstrates the same for the Datalyzer, which however starts up a full-blown GUI for data visualization.

from doocspie.abo import TrainAbo


train_abo = TrainAbo(train_events=42)
train_abo.add("FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD", label="charge 1")
train_abo.add("FLASH.DIAG/TOROID/2FL0DBC2/CHARGE.TD", label="charge 2")

ratios = []
def ratio(train_event):
    charge_1 = train_event.get("charge 1").data[0]
    charge_2 = train_event.get("charge 2").data[0]
    ratios.append(charge_1 / charge_2)

train_abo.start_loop(ratio)
# now 'ratios' can be used for plotting purposes or further analysis
from doocspie.gui import Datalyzer


datalyzer = Datalyzer()
datalyzer.add("FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD", label="charge 1")
datalyzer.add("FLASH.DIAG/TOROID/2FL0DBC2/CHARGE.TD", label="charge 2")

@datalyzer.register
def ratio(train_event):
    charge_1 = train_event.get("charge 1").data[0]
    charge_2 = train_event.get("charge 2").data[0]
    return charge_1 / charge_2

datalyzer.startup()

When comparing the both code blocks, there is a noticeable similarity between the two interfaces. In the latter case, we instantiate a Datalyzer instance and add two DOOCS properties with its address and a label, respectively. Furthermore, we register a custom callback function, which will be available in the GUI for data visualization as well. Finally, a PyQt based GUI gets started by invoking the startup method, and its appearance (including the e-log printer dialog) is presented here:

../_images/datalyzer.png ../_images/elog.png

The Datalyzer, just like the TrainAbo, can also be instantiated directly with DOOCS properties as is demonstrated below. The custom callback functions still must be registered though, utilizing the Python decorator called register, which is also part of the Datalyzer interface. Once registered, the function will be available as another data source for visualization. Such a custom data source, i.e. a registered function, will either be named after its function definition name or after an optional label, which can be passed to the register decorator. Both is presented in the example below, where the registered functions also show up in the printout of the Datalyzer instance:

addresses = {"FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD": {"label": "charge 1"}}

datalyzer = Datalyzer(addresses)

@datalyzer.register
def charge_first_bunch(train_event):
    return train_event.get("charge 1").data[0]

@datalyzer.register("first bunch")
def charge_first_bunch(train_event):
    return train_event.get("FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD").data[0]

print(datalyzer)
Datalyzer(actual_properties=('FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD', 'charge 1'), registered_functions=('charge_first_bunch', 'first bunch'))

Here, we show again the actual properties and registered functions by means of Python properties:

print("actual properties of datalyzer:", datalyzer.actual_properties)
print("registered functions of datalyzer:", datalyzer.registered_functions)
actual properties of datalyzer: ('FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD', 'charge 1')
registered functions of datalyzer: ('charge_first_bunch', 'first bunch')

Duplications in registered function labels are indicated with a corresponding custom exception:

try:
    @datalyzer.register("first bunch")
    def charge_second_bunch(train_event):
        return train_event.get("charge").data[1]
except doocspie.DoocspieException as exc:
    print("exception message:", exc.message)
exception message: registered function label 'first bunch' already exists

Just like the TrainAbo, the Datalyzer does internal feasibility checks on the added properties, and thus the update method must be called in order to see effects on the added properties. However, this update method is also called implicitly when the Datalyzer is being started up, hence it can usually be omitted. The following code example shows the effect of the update method:

addresses = ("FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD",)

datalyzer = Datalyzer(addresses)

datalyzer.add("FLASH.DIAG/TOROID/2FL0DBC2/CHARGE.TD", label="charge 2")
datalyzer.add("TEST.DOOCS/UNIT_TEST_SUPPORT/PY_DOOCS/FLOAT")

print("actual properties before update:", datalyzer.actual_properties)

datalyzer.update()

print("actual properties after update:", datalyzer.actual_properties)
print("unusable properties:", datalyzer.unusable_properties)
actual properties before update: ('FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD',)
actual properties after update: ('FLASH.DIAG/TOROID/1FL0UBC2/CHARGE.TD', 'FLASH.DIAG/TOROID/2FL0DBC2/CHARGE.TD', 'charge 2')
unusable properties: (Unusable(address='TEST.DOOCS/UNIT_TEST_SUPPORT/PY_DOOCS/FLOAT', label=None, reason='zero event number'),)

Finally, we show a comprehensive example of the Datalyzer application with all its features:

properties = {"FLASH.DIAG/TOROID.ML/1FL0UBC2/CHARGE.TD": {"label": "charge 1"},
              "FLASH.DIAG/TOROID.ML/2FL0DBC2/CHARGE.TD": {"label": "charge 2"},
              "FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD":
                  {"label": "encoder"},
              "FLASH.FEL/FL21.MOTOR/MOTOR7/FODOMETER": {},
              "FLASH.DIAG/CAMERA/OTR9FL2XTDS/IMAGE_EXT": None}


datalyzer = Datalyzer(properties, allow_zero_events=True)

datalyzer.add_axis_labels("charge 1", "Bins", "Charge (nC)")
datalyzer.add_axis_labels("encoder", y_label="Encoder (mm)")
datalyzer.add_axis_labels("FLASH.DIAG/CAMERA/OTR9FL2XTDS/IMAGE_EXT",
                          x_label="horizontal", y_label="vertical")

@datalyzer.register
def charge_1_first_bunch(train_event):
    return train_event.get("FLASH.DIAG/TOROID.ML/1FL0UBC2/CHARGE.TD").data[0]

@datalyzer.register("first bunch of 2. charge monitor")
def charge_2_first_bunch(train_event):
    return train_event.get("charge 2").data[0]

datalyzer.add_axis_labels("charge_1_first_bunch", y_label="Charge (nC)")
datalyzer.add_axis_labels("first bunch of 2. charge monitor", y_label="Charge (nC)")

import time
start = time.time()
@datalyzer.register
def update_rate(_):
    global start
    stop = time.time()
    rate = stop - start
    start = stop
    return 1 / rate

datalyzer.startup()

The application with its three sub-tools, Viewer, Logger and Correlator is presented below:

../_images/datalyzer-viewer.png ../_images/datalyzer-logger.png ../_images/datalyzer-correlator.png

The Viewer tool is able to visualize images and array-like data types, and also provides averaging and background correction. For scalar-like data types, the Logger tool allows plotting of time series with optional averaging and a movable line marker. The Correlator tool can correlate scalar-like data types, and additionally provides plot symbol customization. Axis labels can optionally be added.

PropertyServer

The default method for visualization of DOOCS property data is realized by the Java DOOCS Data Display (JDDD). Typically, recurrent and daily tasks are handled via DOOCS servers, and those come along with corresponding JDDD panels, i.e. server and visualization are bundled together. In situations where dedicated servers are not available or where one might want to visualize custom data on the fly via JDDD, the PropertyServer class comes handy. This class provides simplified access to property servers at FLASH, and its documentation is presented in the following. Refer to https://confluence.desy.de/display/FLASHUSER/Generic+User+Properties for more details on the DOOCS beamline servers for generic properties at FLASH.

help(doocspie.gui.property_server.PropertyServer)
Help on class PropertyServer in module doocspie.gui.property_server:

class PropertyServer(builtins.object)
 |  PropertyServer class for simplified access to the property servers.
 |  
 |  This class provides the property server class for simplified access to property servers at FLASH.
 |  
 |  Methods defined here:
 |  
 |  get(self, location, property)
 |      Get the readout from the property server for the given location and property.
 |      
 |      Args:
 |          location (str): The property server location to request the readout for the property from.
 |          property (str): The actual property to request the readout from.
 |      
 |      Returns:
 |          Readout: An instance of the readout object from the property server for the given location and property.
 |      
 |      Raises:
 |          ValueError: Exception for non-existing location and/or property or not supported full address input.
 |          DoocspieException: Doocspie related exception for unexpected events.
 |  
 |  get_locations(self, full_address=False)
 |      Get all available property server locations with the option to return the full addresses.
 |      
 |      Args:
 |          full_address (bool, optional): The optional state for returning the full addresses.
 |      
 |      Returns:
 |          tuple: All available property server locations.
 |  
 |  get_properties_of_location(self, location, full_address=False)
 |      Get all available property server properties with the option to return the full addresses.
 |      
 |      Args:
 |          location (str): The property server location to request the properties from.
 |          full_address (bool, optional): The optional state for returning the full addresses.
 |      
 |      Returns:
 |          tuple: All available property server properties.
 |      
 |      Raises:
 |          ValueError: Exception for non-existing location.
 |  
 |  set(self, location, property, value, allow_resizing=False)
 |      Set the property server content for the given location and property with the given value.
 |      
 |      Args:
 |          location (str): The property server location of the property to set the value to.
 |          property (str): The actual property to set the value to.
 |          value (property dependent): The value to set the property to.
 |          allow_resizing(bool, optional): The optional state for allowing resizing of array-like DOOCS types.
 |      
 |      Returns:
 |          None
 |      
 |      Raises:
 |          ValueError: Exception non-existing location and/or property or for not supported full address input.
 |          DoocspieException: Doocspie related exception for unexpected events.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

The PropertyServer class can simply be imported directly from the doocspie.abo subpackage as is shown in the code example below. Here, we instantiate an instance and return the server locations:

from doocspie.gui import PropertyServer


property_server = PropertyServer()

property_server.get_locations()
('FL21', 'FL23', 'FL24', 'FL26', 'BL1', 'BL3', 'PG1', 'PG2')

The default get_locations method returns just the location without its facility and device part, which is also the most relevant information in practice. However, the method also excepts the boolean full_address parameter in order to optionally return the full location as shown here:

property_server.get_locations(full_address=True)
('FLASH.EXP/USER.STORE.FL21/FL21',
 'FLASH.EXP/USER.STORE.FL23/FL23',
 'FLASH.EXP/USER.STORE.FL24/FL24',
 'FLASH.EXP/USER.STORE.FL26/FL26',
 'FLASH.EXP/USER.STORE.BL1/BL1',
 'FLASH.EXP/USER.STORE.BL3/BL3',
 'FLASH.EXP/USER.STORE.PG1/PG1',
 'FLASH.EXP/USER.STORE.PG2/PG2')

Once decided for a location, all properties of the particular location can be returned by means of the get_properties_of_location method. The following code example presents all the properties available, which consist of scalar type (DOUBLE) and 1D/2D array data (A_DOUBLE/A_XY):

properties = property_server.get_properties_of_location("FL24")
filtered_properties = tuple(p for p in properties if p[-1].isdigit())  # to reduce output

filtered_properties
('VAL.01',
 'VAL.02',
 'VAL.03',
 'VAL.04',
 'VAL.05',
 'VAL.06',
 'VAL.07',
 'VAL.08',
 'VAL.09',
 'VAL.10',
 'VAL.11',
 'VAL.12',
 'VAL.13',
 'VAL.14',
 'VAL.15',
 'VAL.16',
 'ARRAY1D.01',
 'ARRAY1D.02',
 'ARRAY1D.03',
 'ARRAY1D.04',
 'ARRAY2D.01',
 'ARRAY2D.02',
 'ARRAY2D.03',
 'ARRAY2D.04')

The following two images show example JDDD panel plots for 1D and 2D array data, respectively:

../_images/sine.png ../_images/heart.png

Similar to get_locations mentioned earlier, the get_properties_of_location method also allows to return full addresses via its boolean full_address parameter as is demonstrated in the following:

property_server.get_properties_of_location("FL24", full_address=True)[0]
'FLASH.EXP/USER.STORE.FL24/FL24/VAL.01'

Proper ValueError exceptions get raised for properties of not existing locations being requested:

try:
    property_server.get_properties_of_location("not existing")
except ValueError as err:
    print(err)
location 'not existing' does not exist

Once decided for a location and property, the get method can simply be used to retrieve data as is demonstrated in the following example. The get method requires a location and property string, and it always return an instance of Readout, which has been covered in great detail before.

value_readout = property_server.get("FL24", "VAL.01")
array_1d_readout = property_server.get("FL24", "ARRAY1D.01")
array_2d_readout = property_server.get("FL24", "ARRAY2D.01")

print("PropertyServer's 'get' method always returns a 'Readout':",
all([isinstance(value_readout, doocspie.doocspie.io.Readout),
     isinstance(array_1d_readout, doocspie.doocspie.io.Readout),
     isinstance(array_2d_readout, doocspie.doocspie.io.Readout)]))

print()
print("readout.type of 'VAL' property:", value_readout.type)
print("readout.type of 'ARRAY1D' property:", array_1d_readout.type)
print("readout.type of 'ARRAY2D' property:", array_2d_readout.type)
PropertyServer's 'get' method always returns a 'Readout': True

readout.type of 'VAL' property: DOUBLE
readout.type of 'ARRAY1D' property: A_DOUBLE
readout.type of 'ARRAY2D' property: A_XY

Trying to get data for non existing locations or properties results in ValueError exceptions:

try:
    property_server.get("not existing", "ARRAY2D.01")
except ValueError as err:
    print(err)

try:
    property_server.get("FL24", "not existing")
except ValueError as err:
    print(err)
location 'not existing' does not exist
property 'not existing' does not exist

The get method does not except full addresses for the location and/or property part, respectively:

try:
    property_server.get("FLASH.EXP/USER.STORE.FL24/FL24", "ARRAY2D.01")
except ValueError as err:
    print(err)
full address of location and/or property is not supported

Besides retrieving data, the PropertyServer class also permits to send data to the property servers via its set method, similar the get method but with an additional value parameter to pass. The following example demonstrates how to get/set data of/to a particular property server:

readout_before = property_server.get("FL24", "VAL.01")
property_server.set("FL24", "VAL.01", 42)  # the actual 'set' command
readout_after = property_server.get("FL24", "VAL.01")

print("readout data of 'VAL.01' at 'FL24' before 'set' command:", readout_before.data)
print("readout data of 'VAL.01' at 'FL24' after 'set' command:", readout_after.data)
readout data of 'VAL.01' at 'FL24' before 'set' command: 23.0
readout data of 'VAL.01' at 'FL24' after 'set' command: 42.0

The set provides an optional allow_resizing parameter of boolean type, which renders resizing of array-like DOOCS data types possible, in case resizing is implemented in the particular DOOCS server of interest. The following code example presents how such a resizing can be accomplished, and it also shows what happens otherwise, i.e. without resizing being allowed:

readout_before = property_server.get("FL24", "ARRAY2D.02")
print("readout data before resizing:", readout_before.data.tolist())

new_data = list(readout_before.data) + [[23, 42]]
try:
    property_server.set("FL24", "ARRAY2D.02", new_data)
except doocspie.DoocspieException as exc:
    print("without resizing -> exception message:", exc.message)
property_server.set("FL24", "ARRAY2D.02", new_data, allow_resizing=True)

readout_after = property_server.get("FL24", "ARRAY2D.02")
print("readout data after resizing:", readout_after.data.tolist())
readout data before resizing: [[0.0, 0.0]]
without resizing -> exception message: wrong input data size
readout data after resizing: [[0.0, 0.0], [23.0, 42.0]]