Integrating counters¶
This section describes the implementation of integrating counters and integrating counter controllers.
If you read this lines for the first time, please have a look to the counter basics first
The IntegratingCounterController
is designed for counters data not immediately available and buffered by the controller.
It means that during acquisition, a polling mechanism checks periodically if the controller has data. When data are available
it may contain multiple measurements that can be returned as a block of a variable size (see
get_values
).
Usually, it works in association with a master controller. A typical usage example could be counters associated to time integrated data or counters working on data such as ROI statistics in an image.
IntegratingCounter
class¶
Base class for all integrating counters. It inherits from the
Counter
base class.
It is compatible with the
HasMetadataForScan and
HasMetadataForDataset protocols.
Signature (from bliss.common.counter)
class IntegratingCounter(Counter):
def __init__(
self,
name,
controller,
conversion_function=None,
unit=None,
):
-
name
: a name for this counter (str) -
controller
: the counter controller that will manage this counter (mandatory) -
conversion_function
: a function to convert counter’s data on the fly (optional) -
unit
: the counter’s data unit (optional)
Inherited properties (see Counter class)
@property
def _counter_controller(self):
""" Return the CounterController owning this counter """
@property
def name(self):
""" Return the counter name """
@property
def fullname(self):
""" Return fullname as '<counter_controller_name>:<counter_name>' """
@property
def unit(self):
""" Return counter data unit (default is None) """
@unit.setter
def unit(self, unit):
@property
def conversion_function(self):
""" Return a data conversion function (default is `_identity`) """
@conversion_function.setter
def conversion_function(self, func):
Customizable properties and methods
@property
def dtype(self):
""" Return counter data type """
return float
@property
def shape(self):
""" Return counter data shape """
return ()
def dataset_metadata(self) -> dict:
""" Return metadata dictionary for ICAT """
return {"name": self.name}
def scan_metadata(self) -> dict:
""" Return scan metadata dictionary """
return dict()
def __info__(self):
""" Return a description of the counter as a string """
info_str = f"{self.__class__.__name__}:\n"
info_str += f" name = {self.name}\n"
info_str += f" dtype = {self.dtype.__name__}\n"
info_str += f" shape = {len(self.shape)}D\n"
info_str += f" unit = {self.unit}\n"
if self.conversion_function is not _identity:
info_str += f" conversion_function = {self.conversion_function}\n"
return info_str
IntegratingCounterController
class¶
The integrating counter controller object is a container for integrating counters associated to one equipment.
It inherits from the CounterController
base class.
Usually, this object declares a master counter controller but it can work without.
Signature (from bliss.controllers.counter)
class IntegratingCounterController(CounterController):
def __init__(self, name, master_controller=None, register_counters=True):
-
name
: a name for this counter controller (str) -
master_controller
: a counter controller instance acting as a master of this one -
register_counters
: a flag to tell if counters registration should be made by this object
Inherited properties and methods (see CounterController class)
@property
def name(self):
""" Return counter controller name """
@property
def fullname(self):
""" Return '<master_controller_fullname>:<name>'
If master controller is None, it just returns <name>
"""
@property
def _master_controller(self):
""" Return the master counter controller object (default is None) """
@property
def counters(self):
""" Return owned counters as a counter_namespace """
Customizable methods (optional)
def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params):
"""
Return a IntegratingCounterAcquisitionSlave.
args:
- `acq_params`: parameters for the acquisition object (dict)
- `ctrl_params`: parameters for the controller (dict)
- `parent_acq_params`: acquisition parameters of the master (if any)
"""
return IntegratingCounterAcquisitionSlave(
self, ctrl_params=ctrl_params, **acq_params
)
def get_default_chain_parameters(self, scan_params, acq_params):
"""
Return necessary acquisition parameters in the context of step by step scans.
args:
- `scan_params`: parameters of the scan (dict)
- `acq_params`: parameters for the acquisition (dict)
return: a dictionary of acquisition parameters
In the context of a step-by-step scan, `acq_params` is usually empty
and the returned dict must be deduced from `scan_params`.
However, in the case of a customized DEFAULT_CHAIN, `acq_params` may
be not empty and these parameters must override the default ones.
"""
try:
count_time = acq_params["count_time"]
except KeyError:
count_time = scan_params["count_time"]
params = {"count_time": count_time}
if self._master_controller is None:
try:
npoints = acq_params["npoints"]
except KeyError:
npoints = scan_params["npoints"]
params["npoints"] = npoints
return params
To be overridden methods (mandatory)
def get_values(self, from_index, *counters):
"""
Return the counter values for the list of given counters.
For each counter, data is a list of values (one per measurement).
All counters must retrieve the same number of data!
args:
- from_index: an integer corresponding to the index of the
measurement from which new data should be retrieved
- counters: the list of counters for which measurements should be retrieved.
example:
tmp = [self.get_available_measurements(cnt, from_index) for cnt in counters]
dmin = min([len(cnt_data) for cnt_data in tmp])
return [cnt_data[:dmin] for cnt_data in tmp]
"""
raise NotImplementedError
IntegratingCounterAcquisitionSlave
class¶
The default acquisition object associated to integrating counter controllers.
This class already implements the reading
method of its base class.
In that method it calls get_values(from_index, *counters)
(detailed above) to obtain the list
of available measurements for a given list of counters, since a given index.
Usually it is not necessary to override that class, except if the prepare_device
,
start_device
and stop_device
methods must be overridden.
If it has a master controller, the
npoints
, prepare_once
and start_once
attributes are automatically set to the values
of it is master’s acquisition object.
Signature (from bliss.scanning.acquisition.counter)
class IntegratingCounterAcquisitionSlave(BaseCounterAcquisitionSlave):
def __init__(self, controller, ctrl_params=None,
count_time=None, npoints=1):
-
controller
: the associated CounterController instance -
ctrl_params
: optional controller parameters -
count_time
: the scan step counting time (float) -
npoints
: the scan steps number (integer)
About npoints
, prepare_once
and start_once
This class sets npoints
, prepare_once
and start_once
to the same values as its
master counter controller
(whatever the values received at the initialization). If it has no master
prepare_once = start_once = False
. Note that the scan count_time
is
accessible (as a property) but not used in this base class implementation.
Inherited properties and methods (see BaseCounterAcquisitionSlave class)
@staticmethod
def get_param_validation_schema():
""" An acquisition parameters validation scheme"""
acq_params_schema = {
"count_time": {"type": "numeric"},
"npoints": {"type": "int"},
}
schema = {"acq_params": {"type": "dict", "schema": acq_params_schema}}
return schema
@property
def npoints(self):
return self.__npoints
@property
def count_time(self):
return self.__count_time
Customizable methods (optional)
def prepare_device(self):
pass
def start_device(self):
pass
def stop_device(self):
pass
Example¶
Implementation example of a MCA device with spectrum and ROIs counters
import time
import gevent
import numpy
from bliss.common.counter import IntegratingCounter
from bliss.controllers.counter import CounterController, IntegratingCounterController
from bliss.scanning.chain import AcquisitionMaster
from bliss.scanning.chain import TRIGGER_MODE_ENUM
class MyMcaDevice:
""" A mockup of an hardware device measuring data from multiple channels as a spectrum """
def __init__(self) -> None:
self.integration_time = 1
self.spectrum_size = 100
self.rois = {}
# attributes to simulate data acquisition
self._random_generator = numpy.random.default_rng()
self._acq_task = None
self._data_buffer = []
self._roi_data_buffer = {}
def add_roi(self, name, roi):
self.rois[name] = roi
self._roi_data_buffer[name] = []
def acquire_spectrum(self, num_of_acq=1):
for i in range(num_of_acq):
time.sleep(self.integration_time)
sdata = self._random_generator.random(self.spectrum_size)*self.integration_time
self._data_buffer.append(sdata)
for name, roi in self.rois.items():
self._roi_data_buffer[name].append(numpy.sum(sdata[roi[0]:roi[1]]))
def start_acq(self, num_of_acq=1):
if self._acq_task:
raise RuntimeError(f"an acquisition task is already running {self._acq_task}")
self._acq_task = gevent.spawn(self.acquire_spectrum, num_of_acq)
def get_data(self, from_index):
""" return spectrum data from a given index """
return self._data_buffer[from_index:]
def get_roi_data(self, from_index, *roi_names):
""" return ROIs data from a given index """
return [self._roi_data_buffer[name][from_index:] for name in roi_names]
def clean_buffer(self):
self._data_buffer = []
for name in self._roi_data_buffer.keys():
self._roi_data_buffer[name] = []
def stop_acq(self):
self._acq_task.kill()
class MyMcaMasterCounterController(CounterController):
""" A master counter controller providing an AcquisitionMaster for spectrum acquisition """
def __init__(self, hw_device, name, master_controller=None, register_counters=True):
super().__init__(name, master_controller, register_counters)
# a reference to the hardware device
self.hw_device = hw_device
def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params):
return MyMcaAcquisitionMaster(self, **acq_params)
def get_default_chain_parameters(self, scan_params, acq_params):
params = {}
# defaults guessed from scan_params
params["npoints"] = acq_params.get("npoints", scan_params.get("npoints", 1))
params["integration_time"] = acq_params.get("integration_time", scan_params["count_time"])
# hardcoded defaults
params["prepare_once"] = acq_params.get("prepare_once", True)
params["start_once"] = acq_params.get("start_once", False)
return params
class MyMcaAcquisitionMaster(AcquisitionMaster):
""" An AcquisitionMaster to drive spectrum acquisition """
def __init__(self,
controller, # the associated counter controller
name=None,
npoints=1,
integration_time=1,
trigger_type=TRIGGER_MODE_ENUM.SOFTWARE,
prepare_once=True,
start_once=False,
ctrl_params=None):
super().__init__(
controller,
name=name,
npoints=npoints,
trigger_type=trigger_type,
prepare_once=prepare_once,
start_once=start_once,
ctrl_params=ctrl_params
)
self.integration_time = integration_time
self.t0 = time.perf_counter()
def prepare(self):
self.device.hw_device.integration_time = self.integration_time
self.device.hw_device.clean_buffer()
def start(self):
pass
def stop(self):
self.device.hw_device.stop_acq()
def trigger(self):
self.trigger_slaves()
self.device.hw_device.start_acq()
def wait_ready(self):
if self.device.hw_device._acq_task:
self.device.hw_device._acq_task.join()
class MySpectrumCounter(IntegratingCounter):
""" A 1D counter for spectrum counter data """
def __init__(self, name, controller, conversion_function=None, unit=None):
super().__init__(name, controller, conversion_function, unit)
# a reference to the hw device
self.hw_device = self._counter_controller.hw_device
@property
def dtype(self):
return float
@property
def shape(self):
return (self.hw_device.spectrum_size, )
def dataset_metadata(self) -> dict:
""" Metadata for ICAT """
return {"name": self.name, "spectrum_size":self.shape[0]}
def scan_metadata(self) -> dict:
metadata = super().scan_metadata()
metadata.update({"spectrum_size": self.shape[0],})
return metadata
def __info__(self):
info_str = super().__info__()
info_str += f" spectrum size: {self.shape[0]}\n"
return info_str
class MySpectrumCounterController(IntegratingCounterController):
""" An IntegratingCounterController to handle spectrum counter data """
def __init__(self, hw_device, name, master_controller=None, register_counters=True):
super().__init__(name, master_controller, register_counters)
# a reference to the hardware device
self.hw_device = hw_device
def get_values(self, from_index, *counters):
data = self.hw_device.get_data(from_index)
return [data] # it has only one counter
class MyROICounter(IntegratingCounter):
""" A counter for ROI data """
def __init__(self, name, roi, controller, conversion_function=None, unit=None):
super().__init__(name, controller, conversion_function, unit)
# a reference to the hw device
self.hw_device = self._counter_controller.hw_device
if len(roi) != 2:
raise ValueError(f"ROI value should be a list of 2 integer values")
self.roi = roi
self.hw_device.add_roi(name, roi)
@property
def dtype(self):
return float
@property
def shape(self):
return ()
def dataset_metadata(self) -> dict:
""" Metadata for ICAT """
return {"name": self.name, "roi":self.roi}
def scan_metadata(self) -> dict:
metadata = super().scan_metadata()
metadata.update({"roi": self.roi,})
return metadata
def __info__(self):
info_str = super().__info__()
info_str += f" roi: {self.roi}\n"
return info_str
class MyROICounterController(IntegratingCounterController):
""" An IntegratingCounterController to handle ROI counters data """
def __init__(self, hw_device, name, master_controller=None, register_counters=True):
super().__init__(name, master_controller, register_counters)
# a reference to the hardware device
self.hw_device = hw_device
def get_values(self, from_index, *counters):
cnts_data = self.hw_device.get_roi_data(from_index, *[cnt.name for cnt in counters])
return cnts_data
In this example MySpectrumCounterController
and MyROICounterController
use the default IntegratingCounterAcquisitionSlave.
During the whole scan, these integrating acquisition slaves will just poll data asynchronously,
calling the get_values
method defined above, until they receive the expected number of measurements
(npoints
). The value for npoints
is automatically obtained from their master
(i.e. MyMcaAcquisitionMaster
). In the case of standard step by step scan,
the master obtains the npoints
value directly from the scan parameters
(see MyMcaMasterCounterController.get_default_chain_parameters
).
Usage
hw_device = MyMcaDevice()
McaMasterCC = MyMcaMasterCounterController(hw_device, "McaMaster")
SpectrumCC = MySpectrumCounterController(hw_device, "SpectrumCC", McaMasterCC)
RoisCC = MyROICounterController(hw_device, "RoisCC", McaMasterCC)
sp_cnt = MySpectrumCounter("spectrum", SpectrumCC, unit='counts')
roi_cnt1 = MyROICounter("r1", (0, 3), RoisCC, unit='counts')
roi_cnt2 = MyROICounter("r2", (3, 7), RoisCC, unit='counts')
roi_cnt2 = MyROICounter("r3", (7, 13), RoisCC, unit='counts')
loopscan(10, 0.1, sp_cnt, RoisCC) # count with spectrums and ROIs