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):
class AbstractAcquisitionObjectIterator:
"""Iterate over an AcquisitionObject, yielding self.
"""
Iterate over an AcquisitionObject, yielding self.
"""
@property
......@@ -395,7 +396,8 @@ class AcquisitionObject:
self._do_add_counter(counter)
else:
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):
......@@ -406,7 +408,7 @@ class AcquisitionObject:
else:
return AcquisitionObjectIterator(self)
# --------------------------- ACQ. CHAIN METHODS ---------------------------------------------
# --------------------------- ACQ. CHAIN METHODS ------------------------------------------
def has_reading_task(self):
"""Returns True when the underlying device has a reading task.
......@@ -437,7 +439,7 @@ class AcquisitionObject:
finally:
gevent.killall(tasks, exception=StopTask)
# --------------------------- OVERLOAD ACQ. CHAIN METHODS ---------------------------------------------
# --------------------------- OVERLOAD ACQ. CHAIN METHODS ---------------------------------
def acq_prepare(self):
raise NotImplementedError
......@@ -1110,11 +1112,10 @@ class AcquisitionChain:
def add_preset(self, preset, master=None):
"""
Add a preset on a top-master.
If it None mean the first in the chain
Args:
preset should be inherited for class Preset
master if None take the first top-master from the chain
preset: a ChainPreset object
master: if None, take the first top-master of the chain
"""
if not isinstance(preset, ChainPreset):
raise ValueError("Expected ChainPreset instance")
......@@ -1236,22 +1237,27 @@ class ChainNode:
and self._parent_acq_params != parent_acq_params
):
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:
self._parent_acq_params = parent_acq_params
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 )
If the parameters have been set already, new parameters will be ignored (except if Force==True).
"""
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 )
If the parameters have been set already, new parameters will
be ignored (except if Force==True).
"""
if acq_params is not None:
if self._acq_obj_params is not None and self._acq_obj_params != acq_params:
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:
......@@ -1260,7 +1266,8 @@ class ChainNode:
if ctrl_params is not None:
if self._ctrl_params is not None and self._ctrl_params != ctrl_params:
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:
......@@ -1279,13 +1286,18 @@ class ChainNode:
self._counters.append(counter)
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)
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(
......@@ -1293,18 +1305,19 @@ class ChainNode:
)
def create_acquisition_object(self, force=False):
""" 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.
- 'force' (bool): if False, it won't instanciate the acquisition object if it already exists, else it will overwrite it.
"""
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.
- 'force' (bool): if False, it won't instanciate the acquisition
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:
return self._acquisition_obj
# --- Prepare parameters -----------------------------------------------------------------------------------------
# --- Prepare parameters --------------------------------------------------------------
if self._acq_obj_params is None:
acq_params = {}
else:
......@@ -1324,7 +1337,7 @@ class ChainNode:
self._parent_acq_params.copy()
) # <= 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_params, ctrl_params=ctrl_params, parent_acq_params=parent_acq_params
)
......
......@@ -428,6 +428,13 @@ class ScanPreset:
return self.__new_data_callback(counter, sender.fullname, data)
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)
for i, node in enumerate(nodes):
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
* detector cover removing/replacing
......@@ -7,14 +17,113 @@
* equipment protection (via data channels hook, see below)
* ...
A preset is a *hook* in a scan iteration. This hook can be set at different
levels depending on the need.
!!! Note
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
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
* `start()`: called before all devices starting
......@@ -34,9 +143,11 @@ class Preset(ScanPreset):
print(f"{scan.name} scan is stopped")
print(f"Closing the shutter")
```
and it's usage:
```python
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 [6]: s.run()
......@@ -55,20 +166,35 @@ loopscan scan is stopped
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.
It is useful in order to protect some equipments, for example: if the value
measured by a diode exceeds some threshold, the scan can stop or some
It is useful to protect some equipments. For example, if the value
measured by a diode exceeds some threshold, the scan can be stopped or some
attenuators can be activated.
The basic usage is to call the `.connect_data_channels()` method, from the
`.prepare()` of `ScanPreset`. Arguments are:
The basic usage is to call the `.connect_data_channels()` method from the
`.prepare()` of a `ScanPreset`.
The callback will receive the arguments: `counter`, `channel_name` and `data`.
```python
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
"""
...
* the list of counters to connect data channels to
* a callback function
```
Example:
......@@ -79,11 +205,9 @@ class MyScanPreset(ScanPreset):
self.diode = diode
def prepare(self, scan):
self.connect_data_channels([self.diode, ...], self.protect_my_detector)
def protect_my_detector(self, counter, channel_name, data):
if counter == diode:
# 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.
## ChainPreset
This hook is linked to a *top-master* of the acquisition chain. So the
callback method will be called during `prepare`, `start` and `stop`
phases of this top-master. It has exactly the same behaviour than
the `ScanPreset` if the chain has **only one** top-master.
ChainPreset hook is linked to a *top-master* of the acquisition chain. So the
callback method will be called during `prepare`, `start` and `stop` phases of
this top-master. It has exactly the same behaviour than the `ScanPreset` if the
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
from bliss.scanning.chain import ChainPreset
......@@ -138,15 +272,17 @@ Took 0:00:36.189189
## ChainIterationPreset
Use this object when you want to set a hook on each *iteration* of a
top-master. `ChainIterationPreset` is **yield** from `ChainPreset`
instance by *get_iterator* method. i.e here is an example where you want to
open/close the shutter for each point.
Use ChainIterationPreset to set a hook on each *iteration* of a
top-master. `ChainIterationPreset` is **yield** from `ChainPreset` instance by
the `get_iterator` method.
Example: to open and close the shutter at each iteration of the scan.
```python
class Preset(ChainPreset):
class Iterator(ChainIterationPreset):
def __init__(self,iteration_nb):
def __init__(self, iteration_nb):
self.iteration = iteration_nb
def prepare(self):
print(f"Preparing iteration {self.iteration}")
......@@ -154,12 +290,15 @@ class Preset(ChainPreset):
print(f"Starting, Opening the shutter iter {self.iteration}")
def stop(self):
print(f"Stopped, closing the shutter, iter {self.iteration}")
def get_iterator(self,acq_chain):
def get_iterator(self, acq_chain):
iteration_nb = 0
while True:
yield Preset.Iterator(iteration_nb)
iteration_nb += 1
```
```python
DEMO [2]: p = Preset()
DEMO [3]: s = loopscan(2,0.1,diode,run=False)
......@@ -183,28 +322,33 @@ Took 0:00:16.677241
```
!!! warning
In this example, you can see that *data display* and the *chain
iteration* are executed by two separated greenlets which are not
synchronised. This is not a problem but can be confusing if you think
that it's sequencial.
In this example, *data display* and the *chain iteration* are
executed by two separated greenlets which are not synchronised. This is not
a problem but can be confusing for the user.
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
As `Preset` callbacks are executed synchronously, they can easily pause
a scan, just by not returning imediatly from *prepare*, *start* or *stop*
callback methods.
As `Preset` callbacks are executed synchronously, they can easily pause a scan,
just by not returning imediatly from `prepare()`, `start()` or `stop()` callback
methods.
For example, to pause a scan in case of beam loss, the condition has to be
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
class Preset(ScanPreset):
def __init__(self,diode,beam_trigger_value):
def __init__(self, diode, beam_trigger_value):
self._diode = diode
self._beam_trigger_value
def prepare(self,scan):
def prepare(self, scan):
beam_value = self._diode.read()
while beam_value < self._beam_trigger_value:
print("Waiting for beam")
......
Markdown is supported
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