Skip to content
This repository has been archived by the owner on Nov 3, 2021. It is now read-only.

Latest commit

 

History

History
631 lines (457 loc) · 23.2 KB

howto_countertimercontroller.rst

File metadata and controls

631 lines (457 loc) · 23.2 KB

sardana.pool.controller

How to write a counter/timer controller

This chapter provides the necessary information to write a counter/timer controller in Sardana.

Table of contents

The basics

An example of a hypothetical Springfield counter/timer controller will be build incrementally from scratch to aid in the explanation.

By now you should have read the general controller basics <sardana-controller-api> chapter. You should be able to create a CounterTimerController with:

  • a proper constructor,
  • add and delete axis methods
  • get axis state
import springfieldlib

from sardana.pool.controller import CounterTimerController

from sardana import State

class SpringfieldCounterTimerController(CounterTimerController):

    def __init__(self, inst, props, *args, **kwargs):
        super(SpringfieldCounterTimerController, self).__init__(inst, props, *args, **kwargs)

        # initialize hardware communication
        self.springfield = springfieldlib.SpringfieldCounterHW()

        # do some initialization
        self._counters = {}

    def AddDevice(self, axis):
        self._counters[axis] = True 

    def DeleteDevice(self, axis):
        del self._counters[axis]

    StateMap = {
        1 : State.On,
        2 : State.Moving,
        3 : State.Fault,
    }

    def StateOne(self, axis):
        springfield = self.springfield
        state = self.StateMap[ springfield.getState(axis) ]
        status = springfield.getStatus(axis)
        return state, status

The examples use a springfieldlib module which emulates a counter/timer hardware access library.

The springfieldlib can be downloaded from here <springfieldlib.py>.

The Springfield counter/timer controller can be downloaded from here <sf_ct_ctrl.py>.

The following code describes a minimal Springfield base counter/timer controller which is able to return both the state and value of an individual counter as well as to start an acquisition:

sf_ct_ctrl.py

Get counter state

To get the state of a counter, sardana calls the ~sardana.pool.controller.Controller.StateOne method. This method receives an axis as parameter and should return either:

  • state (~sardana.sardanadefs.State) or
  • a sequence of two elements:
    • state (~sardana.sardanadefs.State)
    • status (str)

The state should be a member of ~sardana.sardanadefs.State (For backward compatibility reasons, it is also supported to return one of PyTango.DevState). The status could be any string.

Load a counter

To load a counter with either the integration time or the monitor counts, sardana calls the ~sardana.pool.controller.Loadable.LoadOne method. This method receives axis, value and repetitions parameters. For the moment let's focus on the first two of them.

Here is an example of the possible implementation of ~sardana.pool.controller.Loadable.LoadOne:

class SpringfieldCounterTimerController(CounterTimerController):

    def LoadOne(self, axis, value, repetitions, latency):
        self.springfield.LoadChannel(axis, value)

Get counter value

To get the counter value, sardana calls the ~sardana.pool.controller.Readable.ReadOne method. This method receives an axis as parameter and should return a valid counter value. Sardana notifies the pseudo counters about the new counter value so they can be updated (see sardana-pseudocounter-overview for more details).

Here is an example of the possible implementation of ~sardana.pool.controller.Readable.ReadOne:

class SpringfieldCounterTimerController(CounterTimerController):

    def ReadOne(self, axis):
        value = self.springfield.getValue(axis)
        return value

Start a counter

When an order comes for sardana to start a counter, sardana will call the ~sardana.pool.controller.Startable.StartOne method. This method receives an axis as parameter. The controller code should trigger the hardware acquisition.

Here is an example of the possible implementation of ~sardana.pool.controller.Startable.StartOne:

class SpringfieldCounterTimerController(CounterTimerController):

    def StartOne(self, axis, value):
        self.springfield.StartChannel(axis)

As soon as ~sardana.pool.controller.Startable.StartOne is invoked, sardana expects the counter to be acquiring. It enters a high frequency acquisition loop which asks for the counter state through calls to ~sardana.pool.controller.Controller.StateOne. It will keep the loop running as long as the controller responds with State.Moving. If ~sardana.pool.controller.Controller.StateOne raises an exception or returns something other than State.Moving, sardana will assume the counter is stopped and exit the acquisition loop.

For an acquisition to work properly, it is therefore, very important that ~sardana.pool.controller.Controller.StateOne responds correctly.

Stop a counter

It is possible to stop a counter when it is acquiring. When sardana is ordered to stop a counter acquisition, it invokes the ~sardana.pool.controller.Stopable.StopOne method. This method receives an axis parameter. The controller should make sure the desired counter is gracefully stopped.

Here is an example of the possible implementation of ~sardana.pool.controller.Stopable.StopOne:

class SpringfieldCounterTImerController(CounterTimerController):

    def StopOne(self, axis):
        self.springfield.StopChannel(axis)

Abort a counter

In an emergency situation, it is desirable to abort an acquisition as fast as possible. When sardana is ordered to abort a counter acquisition, it invokes the ~sardana.pool.controller.Stopable.AbortOne method. This method receives an axis parameter. The controller should make sure the desired counter is stopped as fast as it can be done.

Here is an example of the possible implementation of ~sardana.pool.controller.Stopable.AbortOne:

class SpringfieldCounterTimerController(CounterTimerController):

    def AbortOne(self, axis):
        self.springfield.AbortChannel(axis)

Advanced topics

Timer and monitor roles

Usually counters can work in either of two modes: timer or monitor. In both of them, one counter in a group is assigned a special role to control when the rest of them should stop counting. The stopping condition is based on the integration time in case of the timer or on the monitor counts in case of the monitor. The assignment of this special role is based on the measurement group sardana-measurementgroup-overview-configuration. The controller receives this configuration (axis number) via the controller parameter timer and monitor. The currently used acquisition mode is set via the controller parameter acquisition_mode.

Controller may announce its default timer axis with the ~sardana.pool.controller.Loadable.default_timer class attribute.

Timestamp a counter value

When you read the value of a counter from the hardware sometimes it is necessary to associate a timestamp with that value so you can track the value of a counter in time.

If sardana is executed as a Tango device server, reading the value attribute from the counter device triggers the execution of your controller's ~sardana.pool.controller.Readable.ReadOne method. Tango responds with the value your controller returns from the call to ~sardana.pool.controller.Readable.ReadOne and automatically assigns a timestamp. However this timestamp has a certain delay since the time the value was actually read from hardware and the time Tango generates the timestamp.

To avoid this, sardana supports returning in ~sardana.pool.controller.Readable.ReadOne an object that contains both the value and the timestamp instead of the usual numbers.Number. The object must be an instance of ~sardana.sardanavalue.SardanaValue.

Here is an example of associating a timestamp in ~sardana.pool.controller.Readable.ReadOne:

import time
from sardana.pool.controller import SardanaValue

class SpringfieldCounterTimerController(CounterTimerController):

   def ReadOne(self, axis):
       return SardanaValue(value=self.springfield.getValue(axis),
                           timestamp=time.time())

If your controller communicates with a Tango device, Sardana also supports returning a ~PyTango.DeviceAttribute object. Sardana will use this object's value and timestamp. Example:

class TangoCounterTimerController(CounterTimerController):

   def ReadOne(self, axis):
       return self.device.read_attribute("value")

Multiple acquisition synchronization

This chapter describes an extended API that allows you to better synchronize acquisitions involving more than one counter, as well as optimize hardware communication (in case the hardware interface also supports this).

Often it is the case that the experiment/procedure the user runs requires to acquire more than one counter at the same time (see sardana-measurementgroup-overview). Imagine that the user requires counter at axis 1 and counter at axis 2 to be acquired. Your controller will receive two consecutive calls to ~sardana.pool.controller.Startable.StartOne:

StartOne(1)
StartOne(2)

and each StartOne will probably connect to the hardware (through serial line, socket, Tango or EPICS) and ask the counter to be started. This will do the job but, there will be a slight desynchronization between the two counters because hardware call of counter 1 will be done before hardware call to counter 2.

Sardana provides an extended start acquisition which gives you the possibility to improve the synchronization (and probably reduce communications) but your hardware controller must somehow support this feature as well.

The complete start acquisition API consists of four methods:

  • ~sardana.pool.controller.Startable.PreStartAll
  • ~sardana.pool.controller.Startable.PreStartOne
  • ~sardana.pool.controller.Startable.StartOne
  • ~sardana.pool.controller.Startable.StartAll

Except for ~sardana.pool.controller.Startable.StartOne, the implementation of all other start methods is optional and their default implementation does nothing (~sardana.pool.controller.Startable.PreStartOne actually returns True).

So, actually, the algorithm for counter acquisition start in sardana is:

/FOR/ Each controller(s) implied in the acquisition
    - Call PreStartAll()
/END FOR/

/FOR/ Each controller(s) implied in the acquisition
    /FOR/ Each counter(s) implied in the acquisition
        - ret = PreStartOne(counter to acquire, new position)
        - /IF/ ret is not true
            /RAISE/ Cannot start. Counter PreStartOne returns False
        - /END IF/
        - Call StartOne(counter to acquire, new position)
    /END FOR/
/END FOR/

/FOR/ Each controller(s) implied in the acquisition
    - Call StartAll()
/END FOR/

The controllers over which we iterate in the above pseudo code are organized so the master timer/monitor controller is the last one to be called. Similar order of iteration applies to the counters of a given controller, so the timer/monitor is the last one to be called.

You can assign the master controller role with the order of the controllers in the measurement group. There is one master per each of the following synchronization modes: ~sardana.pool.pooldefs.AcqSynch.SoftwareTrigger and ~sardana.pool.pooldefs.AcqSynch.SoftwareStart. This order must be set within the measurement group sardana-measurementgroup-overview-configuration.

So, for the example above where we acquire two counters, the complete sequence of calls to the controller is:

PreStartAll()

if not PreStartOne(1):
    raise Exception("Cannot start. Counter(1) PreStartOne returns False")
if not PreStartOne(2):
    raise Exception("Cannot start. Counter(2) PreStartOne returns False")

StartOne(1)
StartOne(2)

StartAll()

Sardana assures that the above sequence is never interrupted by other calls, like a call from a different user to get counter state.

Suppose the springfield library tells us in the documentation that:

... to acquire multiple counters at the same time use:

startCounters(seq<axis>)

Example:

startCounters([1, 2])

We can modify our counter controller to take profit of this hardware feature:

class SpringfieldCounterTimerController(MotorController):

    def PreStartAll(self):
        # clear the local acquisition information dictionary
        self._counters_info = []

    def StartOne(self, axis):
        # store information about this axis motion
        self._counters_info.append(axis)

    def StartAll(self):
        self.springfield.startCounters(self._counters_info)

External (hardware) synchronization

The synchronization achieved in sardana-countertimercontroller-howto-mutliple-acquisition may not be enough when it comes to acquiring with multiple controllers at the same time or to executing multiple acquisitions in a row. Some of the controllers can be synchronized on an external hardware event and in this case several important aspects needs to be taken into account.

Synchronization type

First of all the controller needs to know which type of synchronization will be used. This is assigned on the measurement group sardana-measurementgroup-overview-configuration level. The controller receives one of the ~sardana.pool.pooldefs.AcqSynch values via the controller parameter synchronization.

The selected mode will change the behavior of the counter after the ~sardana.pool.controller.Startable.StartOne is invoked. In case one of the software modes was selected, the counter will immediately start acquiring. In case one of the hardware modes was selected, the counter will immediately get armed for the hardware events, and will wait with the acquisition until they occur.

Here is an example of the possible implementation of ~sardana.pool.controller.Controller.SetCtrlPar:

from sardana.pool import AcqSynch

class SpringfieldCounterTimerController(CounterTimerController):

    SynchMap = {
        AcqSynch.SoftwareTrigger : 1,
        AcqSynch.SoftwareGate : 2,
        AcqSynch.SoftwareStart : 3,
        AcqSynch.HardwareTrigger: 4,
        AcqSynch.HardwareGate: 5,
        AcqSynch.HardwareStart: 6
    }

    def SetCtrlPar(self, name, value):
        super(SpringfieldMotorController, self).SetCtrlPar(name, value)
        synchronization = SynchMap[value]
        if name == "synchronization":
            self.springfield.SetSynchronization(synchronization)

Multiple acquisitions

It is a very common scenario to execute multiple hardware synchronized acquisitions in a row. One example of this type of measurements are the sardana-users-scan-continuous. The controller receives the number of acquisitions via the repetitions argument of the ~sardana.pool.controller.Loadable.LoadOne method.

Here is an example of the possible implementation of ~sardana.pool.controller.Loadable.LoadOne:

class SpringfieldCounterTimerController(CounterTimerController):

    def LoadOne(self, axis, value, repetitions, latency):
        self.springfield.LoadChannel(axis, value)
        self.springfield.SetRepetitions(repetitions)
        return value

In order to make the acquisition flow smoothly the synchronizer and the counter/timer controllers needs to agree on the synchronization pace. The counter/timer controller manifest what is the maximum allowed pace for him by means of the latency_time controller parameter (in seconds). This parameter corresponds to the minimum time necessary by the hardware controller to re-arm for the next acquisition.

Here is an example of the possible implementation of ~sardana.pool.controller.Controller.GetCtrlPar:

class SpringfieldCounterTimerController(CounterTimerController):

    def GetCtrlPar(self, name):
        if name == "latency_time":
            return self.springfield.GetLatencyTime()

Warning

By default, the ~sardana.pool.controller.CounterTimerController base classes return zero latency time controller parameter. If in your controller you override the ~sardana.pool.controller.Controller.GetCtrlPar method remember to always call the super class method as fallback:

def GetCtrlPar(self, name):
    if name == "some_par":
        return "some_val"
    else:
        return super().GetCtrlPar(name)

In the case of the ~sardana.pool.pooldefs.AcqSynch.HardwareStart or ~sardana.pool.pooldefs.AcqSynch.SoftwareStart synchronizations the counter/timer hardware auto triggers itself during the measurement process. In order to fully configure the hardware and set the re-trigger pace you can use the latency argument (in seconds) of the ~sardana.pool.controller.Loadable.LoadOne method:

class SpringfieldCounterTimerController(CounterTimerController):

    def LoadOne(self, axis, value, repetitions, latency):
        self.springfield.LoadChannel(axis, value)
        self.springfield.SetRepetitions(repetitions)
        self.springfield.SetLatency(latency)
        return value

Get counter values

During the hardware synchronized acquisitions the counter values are usually stored in the hardware buffers. Sardana enters a high frequency acquisition loop after the ~sardana.pool.controller.Startable.StartOne is invoked which, apart of asking for the counter state through calls to the ~sardana.pool.controller.Controller.StateOne method, will try to retrieve the counter values using the ~sardana.pool.controller.Readable.ReadOne method. It will keep the loop running as long as the controller responds with State.Moving. Sardana executes one extra readout after the state has changed in order to retrieve the final counter values.

The ~sardana.pool.controller.Readable.ReadOne method is used indifferently of the selected synchronization but its return values should depend on it and can be:

  • a single counter value: either float or ~sardana.sardanavalue.SardanaValue in case of the ~sardana.pool.pooldefs.AcqSynch.SoftwareTrigger or ~sardana.pool.pooldefs.AcqSynch.SoftwareGate synchronization
  • a sequence of counter values: either float or ~sardana.sardanavalue.SardanaValue in case of the ~sardana.pool.pooldefs.AcqSynch.HardwareTrigger, ~sardana.pool.pooldefs.AcqSynch.HardwareGate, ~sardana.pool.pooldefs.AcqSynch.HardwareStart or ~sardana.pool.pooldefs.AcqSynch.SoftwareStart synchronization

Sardana assumes that the counter values are returned in the order of acquisition and that there are no gaps in between them.

Per measurement preparation

Since SEP18 counter/timer controllers may take a profit from the per measurement preparation and reserve resources for a sequence of ~sardana.pool.pooldefs.AcqSynch.SoftwareTrigger or ~sardana.pool.pooldefs.AcqSynch.SoftwareGate acquisitions already in the ~sardana.pool.controller.Loadable.PrepareOne method. This method is called only once at the beginning of the measurement e.g. Deterministic step scans <sardana-macros-scanframework-determscan> or sardana-users-scan-continuous. It enables an opportunity for significant dead time optimization thanks to the single per measurement configuration instead of the multiple per acquisition preparation using the ~sardana.pool.controller.Loadable.LoadOne.

Here is an example of the possible implementation of ~sardana.pool.controller.Loadable.PrepareOne:

class SpringfieldCounterTimerController(CounterTimerController):

    def PrepareOne(self, value, repetitions, latency, nb_starts):
        return self.springfield.SetNbStarts()