Commit b8c8128a authored by Cyril Guilloud's avatar Cyril Guilloud
Browse files

Merge branch '2857-update-doc-about-scan-preset' into 'master'

Resolve "Update doc about scan preset"

Closes #2857

See merge request !3861
parents 4cf467c1 3a75f49b
Pipeline #50266 failed with stages
in 104 minutes and 58 seconds
...@@ -57,7 +57,8 @@ def join_tasks(greenlets, **kw): ...@@ -57,7 +57,8 @@ def join_tasks(greenlets, **kw):
class AbstractAcquisitionObjectIterator: class AbstractAcquisitionObjectIterator:
"""Iterate over an AcquisitionObject, yielding self. """
Iterate over an AcquisitionObject, yielding self.
""" """
@property @property
...@@ -395,7 +396,8 @@ class AcquisitionObject: ...@@ -395,7 +396,8 @@ class AcquisitionObject:
self._do_add_counter(counter) self._do_add_counter(counter)
else: else:
raise RuntimeError( raise RuntimeError(
f"Cannot add counter {counter.name}: acquisition controller mismatch {counter._counter_controller} != {self.device}" f"Cannot add counter {counter.name}: acquisition controller"
f" mismatch {counter._counter_controller} != {self.device}"
) )
def get_iterator(self): def get_iterator(self):
...@@ -406,7 +408,7 @@ class AcquisitionObject: ...@@ -406,7 +408,7 @@ class AcquisitionObject:
else: else:
return AcquisitionObjectIterator(self) return AcquisitionObjectIterator(self)
# --------------------------- ACQ. CHAIN METHODS --------------------------------------------- # --------------------------- ACQ. CHAIN METHODS ------------------------------------------
def has_reading_task(self): def has_reading_task(self):
"""Returns True when the underlying device has a reading task. """Returns True when the underlying device has a reading task.
...@@ -437,7 +439,7 @@ class AcquisitionObject: ...@@ -437,7 +439,7 @@ class AcquisitionObject:
finally: finally:
gevent.killall(tasks, exception=StopTask) gevent.killall(tasks, exception=StopTask)
# --------------------------- OVERLOAD ACQ. CHAIN METHODS --------------------------------------------- # --------------------------- OVERLOAD ACQ. CHAIN METHODS ---------------------------------
def acq_prepare(self): def acq_prepare(self):
raise NotImplementedError raise NotImplementedError
...@@ -1110,11 +1112,10 @@ class AcquisitionChain: ...@@ -1110,11 +1112,10 @@ class AcquisitionChain:
def add_preset(self, preset, master=None): def add_preset(self, preset, master=None):
""" """
Add a preset on a top-master. Add a preset on a top-master.
If it None mean the first in the chain
Args: Args:
preset should be inherited for class Preset preset: a ChainPreset object
master if None take the first top-master from the chain master: if None, take the first top-master of the chain
""" """
if not isinstance(preset, ChainPreset): if not isinstance(preset, ChainPreset):
raise ValueError("Expected ChainPreset instance") raise ValueError("Expected ChainPreset instance")
...@@ -1236,22 +1237,27 @@ class ChainNode: ...@@ -1236,22 +1237,27 @@ class ChainNode:
and self._parent_acq_params != parent_acq_params and self._parent_acq_params != parent_acq_params
): ):
print( print(
f"=== ChainNode WARNING: try to set PARENT_ACQ_PARAMS again: \n Current {self._parent_acq_params} \n New {parent_acq_params} " f"=== ChainNode WARNING: try to set PARENT_ACQ_PARAMS again: \n"
f"Current {self._parent_acq_params} \n New {parent_acq_params} "
) )
if force or self._parent_acq_params is None: if force or self._parent_acq_params is None:
self._parent_acq_params = parent_acq_params self._parent_acq_params = parent_acq_params
def set_parameters(self, acq_params=None, ctrl_params=None, force=False): def set_parameters(self, acq_params=None, ctrl_params=None, force=False):
""" Store the scan and/or acquisition parameters into the node. """
These parameters will be used when the acquisition object is instantiated (see self.create_acquisition_object ) Store the scan and/or acquisition parameters into the node.
If the parameters have been set already, new parameters will be ignored (except if Force==True). These parameters will be used when the acquisition object
is instantiated (see self.create_acquisition_object )
If the parameters have been set already, new parameters will
be ignored (except if Force==True).
""" """
if acq_params is not None: if acq_params is not None:
if self._acq_obj_params is not None and self._acq_obj_params != acq_params: if self._acq_obj_params is not None and self._acq_obj_params != acq_params:
print( print(
f"=== ChainNode WARNING: try to set ACQ_PARAMS again: \n Current {self._acq_obj_params} \n New {acq_params} " f"=== ChainNode WARNING: try to set ACQ_PARAMS again: \n"
f"Current {self._acq_obj_params} \n New {acq_params} "
) )
if force or self._acq_obj_params is None: if force or self._acq_obj_params is None:
...@@ -1260,7 +1266,8 @@ class ChainNode: ...@@ -1260,7 +1266,8 @@ class ChainNode:
if ctrl_params is not None: if ctrl_params is not None:
if self._ctrl_params is not None and self._ctrl_params != ctrl_params: if self._ctrl_params is not None and self._ctrl_params != ctrl_params:
print( print(
f"=== ChainNode WARNING: try to set CTRL_PARAMS again: \n Current {self._ctrl_params} \n New {ctrl_params} " f"=== ChainNode WARNING: try to set CTRL_PARAMS again: \n"
f"Current {self._ctrl_params} \n New {ctrl_params} "
) )
if force or self._ctrl_params is None: if force or self._ctrl_params is None:
...@@ -1279,13 +1286,18 @@ class ChainNode: ...@@ -1279,13 +1286,18 @@ class ChainNode:
self._counters.append(counter) self._counters.append(counter)
def _get_default_chain_parameters(self, scan_params, acq_params): def _get_default_chain_parameters(self, scan_params, acq_params):
""" Obtain the full acquisition parameters set from scan_params in the context of the default chain """ """
Obtain the full acquisition parameters set from scan_params
in the context of the default chain
"""
return self.controller.get_default_chain_parameters(scan_params, acq_params) return self.controller.get_default_chain_parameters(scan_params, acq_params)
def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params): def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params):
""" Return the acquisition object associated to this node """
acq_params, ctrl_params and parent_acq_params have to be dicts (None not supported) Return the acquisition object associated to this node
acq_params, ctrl_params and parent_acq_params have to be
dicts (None not supported)
""" """
return self.controller.get_acquisition_object( return self.controller.get_acquisition_object(
...@@ -1293,18 +1305,19 @@ class ChainNode: ...@@ -1293,18 +1305,19 @@ class ChainNode:
) )
def create_acquisition_object(self, force=False): def create_acquisition_object(self, force=False):
""" Create the acquisition object using the current parameters (stored in 'self._acq_obj_params'). """
Create the acquisition object using the current
parameters (stored in 'self._acq_obj_params').
Create the children acquisition objects if any are attached to this node. Create the children acquisition objects if any are attached to this node.
- 'force' (bool): if False, it won't instanciate the acquisition
- 'force' (bool): if False, it won't instanciate the acquisition object if it already exists, else it will overwrite it. object if it already exists, else it will overwrite it.
""" """
# --- Return acquisition object if it already exist and Force is False ---------------------------- # --- Return acquisition object if it already exist and Force is False ----------------
if not force and self._acquisition_obj is not None: if not force and self._acquisition_obj is not None:
return self._acquisition_obj return self._acquisition_obj
# --- Prepare parameters ----------------------------------------------------------------------------------------- # --- Prepare parameters --------------------------------------------------------------
if self._acq_obj_params is None: if self._acq_obj_params is None:
acq_params = {} acq_params = {}
else: else:
...@@ -1324,7 +1337,7 @@ class ChainNode: ...@@ -1324,7 +1337,7 @@ class ChainNode:
self._parent_acq_params.copy() self._parent_acq_params.copy()
) # <= IMPORTANT: pass a copy because the acq obj may pop on that dict! ) # <= IMPORTANT: pass a copy because the acq obj may pop on that dict!
# --- Create the acquisition object ------------------------------------------------------- # --- Create the acquisition object ---------------------------------------------------
acq_obj = self.get_acquisition_object( acq_obj = self.get_acquisition_object(
acq_params, ctrl_params=ctrl_params, parent_acq_params=parent_acq_params acq_params, ctrl_params=ctrl_params, parent_acq_params=parent_acq_params
) )
......
...@@ -428,6 +428,13 @@ class ScanPreset: ...@@ -428,6 +428,13 @@ class ScanPreset:
return self.__new_data_callback(counter, sender.fullname, data) return self.__new_data_callback(counter, sender.fullname, data)
def connect_data_channels(self, counters_list, callback): def connect_data_channels(self, counters_list, callback):
"""
Associate a callback to the data emission by the channels of a list of counters.
Args:
* counters_list: the list of counters to connect data channels to
* callback: a callback function
"""
nodes = self.acq_chain.get_node_from_devices(*counters_list) nodes = self.acq_chain.get_node_from_devices(*counters_list)
for i, node in enumerate(nodes): for i, node in enumerate(nodes):
try: try:
......
*Presets* are used to set environnement for a scan. Typically to control:
# Preset
*Presets* are used to customize a scan by performing extra actions
(eg. open/close a shutter) at special events like `prepare()`, `start()` and
`stop()`.
BLISS standard presets are: `ScanPreset`, `ChainPreset` and
`ChainIterationPreset`.
Typically they allow to control:
* opening/closing of a shutter * opening/closing of a shutter
* detector cover removing/replacing * detector cover removing/replacing
...@@ -7,14 +17,113 @@ ...@@ -7,14 +17,113 @@
* equipment protection (via data channels hook, see below) * equipment protection (via data channels hook, see below)
* ... * ...
A preset is a *hook* in a scan iteration. This hook can be set at different !!! Note
levels depending on the need. In the general case, DO NOT use a `Preset` to modify the acquisition
parameters (eg. acquisition time) of a device.
To modify the default acquisition parameters used by a device in
standard scans, use `DEFAULT_CHAIN.set_settings`, see [Default
chain](scan_default.md#default-chain).
## Quick overview
A preset is a *hook* introduced in the [acquisition
chain](scan_writing.md#the-acquisition-chain) of a scan thanks to the
`add_preset()` method.
A preset has three methods (`prepare()`, `start()`, `stop()`) that user can
customize.
Bliss provides three kind of presets that act at different levels of the scan
chain: `ScanPreset`, `ChainPreset` and `ChainIterationPreset`.
**ScanPreset:** (hook a whole scan)
* `.prepare`: called at scan prepare
* `.start` : called at scan start
* `.stop` : called at scan stop
```python
DEMO [1]: s = loopscan(2, 0.1, diode, run=False)
DEMO [2]: p = MyScanPreset() # a ScanPreset object
DEMO [3]: s.add_preset(p) # using the scan object method
DEMO [4]: s.run()
```
**ChainPreset:** (hook a top-master)
* `.prepare`: called at TopMaster.prepare
* `.start` : called at TopMaster.start
* `.stop` : called at TopMaster.stop
Similar to `ScanPreset` if the chain has only one top-master (i.e. single
branch acq_chain)
```python
DEMO [1]: s = loopscan(2, 0.1, diode, run=False)
DEMO [2]: p = MyChainPreset() # a ChainPreset object
DEMO [3]: s.acq_chain.add_preset(p) # using the chain object method
DEMO [4]: s.run()
```
**ChainIterationPreset:** (hook each iteration of a top-master)
* `.prepare`: called at prepare of step i (iteration i of a top-master)
* `.start` : called at start of step i (iteration i of a top-master)
* `.stop` : called at stop of step i (iteration i of a top-master)
```python
DEMO [1]: s = loopscan(2, 0.1, diode, run=False)
DEMO [2]: p = MyIteratingChainPreset() # a ChainPreset object with
# a get_iterator method
DEMO [3]: s.acq_chain.add_preset(p) # using the chain object method
DEMO [4]: s.run()
```
!!! note
Most of the time the acquisition chain has only one top-master (only
one acquisition chain branch) and in that case using a `ScanPreset` or a
`ChainPreset` will produce the same result.
## Add presets to the default chain
It is possible to add a preset to all standard scans of Bliss (i.e: `ct`,
`loopscan`, `ascan`, etc.).
```python
DEMO [1]: p = MyChainPreset()
DEMO [2]: DEFAULT_CHAIN.add_preset(p) # method of the default chain object
...
DEMO [9]: DEFAULT_CHAIN.remove_preset(p)
```
## Multiple top-masters case
Keep in mind that the [acquisition chain](scan_writing.md#the-acquisition-chain)
can have multiple **branches** (one per top-master).
The `ChainPreset` and `ChainIterationPreset` are associated to one top-master
(i.e: one acquisition chain branch).
That is why the `add_preset()` method of the acquisition chain takes an optional
argument `master`.
```python
def add_preset(self, preset, master=None):
"""
Add a preset on a top-master.
Args:
preset: a ChainPreset object
master: if None, take the first top-master of the chain
"""
```
* to hook a whole scan, it will inherit from `ScanPreset`
* to hook a part of the acquisition chain it will inherit from `ChainPreset`
## ScanPreset ## ScanPreset
This is the simplest one. This one has 3 callback methods: This is the simplest one. It has 3 callback methods:
* `prepare()`: called before all devices preparation * `prepare()`: called before all devices preparation
* `start()`: called before all devices starting * `start()`: called before all devices starting
...@@ -34,9 +143,11 @@ class Preset(ScanPreset): ...@@ -34,9 +143,11 @@ class Preset(ScanPreset):
print(f"{scan.name} scan is stopped") print(f"{scan.name} scan is stopped")
print(f"Closing the shutter") print(f"Closing the shutter")
``` ```
and it's usage:
```python ```python
DEMO [3]: p = Preset() DEMO [3]: p = Preset()
DEMO [4]: s = loopscan(2,0.1,diode,run=False) DEMO [4]: s = loopscan(2, 0.1, diode, run=False)
DEMO [5]: s.add_preset(p) DEMO [5]: s.add_preset(p)
DEMO [6]: s.run() DEMO [6]: s.run()
...@@ -55,20 +166,35 @@ loopscan scan is stopped ...@@ -55,20 +166,35 @@ loopscan scan is stopped
Closing the shutter Closing the shutter
``` ```
### Data channels hook ### Data channels hook
The `ScanPreset` has a `connect_data_channels` method, to execute a callback The `ScanPreset` has a `connect_data_channels()` method to execute a callback
when data is emitted from channels. when data is emitted from channels.
It is useful in order to protect some equipments, for example: if the value It is useful to protect some equipments. For example, if the value
measured by a diode exceeds some threshold, the scan can stop or some measured by a diode exceeds some threshold, the scan can be stopped or some
attenuators can be activated. attenuators can be activated.
The basic usage is to call the `.connect_data_channels()` method, from the The basic usage is to call the `.connect_data_channels()` method from the
`.prepare()` of `ScanPreset`. Arguments are: `.prepare()` of a `ScanPreset`.
The callback will receive the arguments: `counter`, `channel_name` and `data`.
* the list of counters to connect data channels to ```python
* a callback function class ScanPreset:
...
def connect_data_channels(self, counters_list, callback):
"""
Associate a callback to the data emission by the channels
of a list of counters.
Args:
* counters_list: the list of counters to connect data channels to
* callback: a callback function
"""
...
```
Example: Example:
...@@ -79,11 +205,9 @@ class MyScanPreset(ScanPreset): ...@@ -79,11 +205,9 @@ class MyScanPreset(ScanPreset):
self.diode = diode self.diode = diode
def prepare(self, scan): def prepare(self, scan):
self.connect_data_channels([self.diode, ...], self.protect_my_detector) self.connect_data_channels([self.diode, ...], self.protect_my_detector)
def protect_my_detector(self, counter, channel_name, data): def protect_my_detector(self, counter, channel_name, data):
if counter == diode: if counter == diode:
# assuming the counter has only 1 channel, no need to # assuming the counter has only 1 channel, no need to
...@@ -98,14 +222,24 @@ If an exception is raised in the callback function, the scan will stop. ...@@ -98,14 +222,24 @@ If an exception is raised in the callback function, the scan will stop.
## ChainPreset ## ChainPreset
This hook is linked to a *top-master* of the acquisition chain. So the ChainPreset hook is linked to a *top-master* of the acquisition chain. So the
callback method will be called during `prepare`, `start` and `stop` callback method will be called during `prepare`, `start` and `stop` phases of
phases of this top-master. It has exactly the same behaviour than this top-master. It has exactly the same behaviour than the `ScanPreset` if the
the `ScanPreset` if the chain has **only one** top-master. chain has **only one** top-master.
Example: In a loopscan, the only top master is a timer. So here it is the same
example as shown above with the ScanPreset, where a shutter is opened at the
beginning of the scan and closed at the end.
The only difference is that the `add_preset()` method is called on the
acquisition chain object instead of the scan object (`s.acq_chain.add_preset`
instead of `s.add_preset`).
In the multiple top-masters case, the `acq_chain.add_preset` method takes an
optional `master` argument to specify the top-master that should be associated
to this preset (see [Multiple top-masters
case](scan_engine_preset.md#multiple-top-masters-case))
i.e: In a loopscan, the sole top master is a timer, so here is the same simple
example where the need is to open a shutter at the beginning of the scan and to
close it at the end.
```python ```python
from bliss.scanning.chain import ChainPreset from bliss.scanning.chain import ChainPreset
...@@ -138,15 +272,17 @@ Took 0:00:36.189189 ...@@ -138,15 +272,17 @@ Took 0:00:36.189189
## ChainIterationPreset ## ChainIterationPreset
Use this object when you want to set a hook on each *iteration* of a Use ChainIterationPreset to set a hook on each *iteration* of a
top-master. `ChainIterationPreset` is **yield** from `ChainPreset` top-master. `ChainIterationPreset` is **yield** from `ChainPreset` instance by
instance by *get_iterator* method. i.e here is an example where you want to the `get_iterator` method.
open/close the shutter for each point.
Example: to open and close the shutter at each iteration of the scan.
```python ```python
class Preset(ChainPreset): class Preset(ChainPreset):
class Iterator(ChainIterationPreset): class Iterator(ChainIterationPreset):
def __init__(self,iteration_nb): def __init__(self, iteration_nb):
self.iteration = iteration_nb self.iteration = iteration_nb
def prepare(self): def prepare(self):
print(f"Preparing iteration {self.iteration}") print(f"Preparing iteration {self.iteration}")
...@@ -154,12 +290,15 @@ class Preset(ChainPreset): ...@@ -154,12 +290,15 @@ class Preset(ChainPreset):
print(f"Starting, Opening the shutter iter {self.iteration}") print(f"Starting, Opening the shutter iter {self.iteration}")
def stop(self): def stop(self):
print(f"Stopped, closing the shutter, iter {self.iteration}") print(f"Stopped, closing the shutter, iter {self.iteration}")
def get_iterator(self,acq_chain):
def get_iterator(self, acq_chain):
iteration_nb = 0 iteration_nb = 0
while True: while True:
yield Preset.Iterator(iteration_nb) yield Preset.Iterator(iteration_nb)
iteration_nb += 1 iteration_nb += 1
``` ```
```python ```python
DEMO [2]: p = Preset() DEMO [2]: p = Preset()
DEMO [3]: s = loopscan(2,0.1,diode,run=False) DEMO [3]: s = loopscan(2,0.1,diode,run=False)
...@@ -183,28 +322,33 @@ Took 0:00:16.677241 ...@@ -183,28 +322,33 @@ Took 0:00:16.677241
``` ```
!!! warning !!! warning
In this example, you can see that *data display* and the *chain In this example, *data display* and the *chain iteration* are
iteration* are executed by two separated greenlets which are not executed by two separated greenlets which are not synchronised. This is not
synchronised. This is not a problem but can be confusing if you think a problem but can be confusing for the user.
that it's sequencial.
In the multiple top-masters case, the `acq_chain.add_preset` method takes an
optional `master` argument to specify the top-master that should be associated
to this preset (see [Multiple top-masters
case](scan_engine_preset.md#multiple-top-masters-case))
## To pause a scan ## To pause a scan
As `Preset` callbacks are executed synchronously, they can easily pause As `Preset` callbacks are executed synchronously, they can easily pause a scan,
a scan, just by not returning imediatly from *prepare*, *start* or *stop* just by not returning imediatly from `prepare()`, `start()` or `stop()` callback
callback methods. methods.
For example, to pause a scan in case of beam loss, the condition has to be For example, to pause a scan in case of beam loss, the condition has to be
checked in a loop. checked in a loop.
As an example, wait to start a scan if the beam is not present. Example to delay starting a scan until the beam is present.
```python ```python
class Preset(ScanPreset): class Preset(ScanPreset):
def __init__(self,diode,beam_trigger_value): def __init__(self, diode, beam_trigger_value):
self._diode = diode self._diode = diode
self._beam_trigger_value self._beam_trigger_value
def prepare(self,scan): def prepare(self, scan):
beam_value = self._diode.read() beam_value = self._diode.read()
while beam_value < self._beam_trigger_value: while beam_value < self._beam_trigger_value:
print("Waiting for beam") print("Waiting for beam")
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment