Experiments connect data acquisition and processing. They can be run multiple times by the .base.Experiment.run
, they take care of proper file structure and logging output.
Experiments consist of .Acquisition
objects which encapsulate data generator and consumers for a particular experiment part (dark fields, radiographs, ...). This way the experiments can be broken up into smaller logical pieces. A single acquisition object needs to be reproducible in order to repeat an experiment more times, thus we specify its generator and consumers as callables which return the actual generator or consumer. We need to do this because generators cannot be "restarted".
It is very important that you enclose the executive part of the production and consumption code in try-finally to ensure proper clean up. E.g. if a producer starts rotating a motor, then in the finally clause there should be the call await motor.stop().
An example of an acquisition could look like this:
from concert.experiments.base import Acquisition
# This is a real generator, num_items is provided somewhere in our session
async def produce():
try:
for i in range(num_items):
yield i
finally:
# Clean up here
pass
# A simple coroutine sink which prints items
async def consume(producer):
try:
async for item in producer:
print(item)
finally:
# Clean up here
pass
acquisition = await Acquisition('foo', produce, consumers=[consume])
# Now we can run the acquisition
await acquisition()
concert.experiments.base.Acquisition
Base .base.Experiment
makes sure all acquisitions are executed. It also holds .addons.Addon
instances which provide some extra functionality, e.g. live preview, online reconstruction, etc. To make a simple experiment for running the acquisition above and storing log with concert.storage.Walker
:
import logging
from concert.experiments.base import Acquisition, Experiment
from concert.storage import DirectoryWalker
LOG = logging.getLogger(__name__)
walker = DirectoryWalker(log=LOG)
acquisitions = [await Acquisition('foo', produce)]
experiment = await Experiment(acquisitions, walker)
await experiment.run()
concert.experiments.base.Experiment
Experiments also have a :py.base.Experiment.log
attribute, which gets a new handler on every experiment run and this handler stores the output in the current experiment working directory defined by it's concert.storage.Walker
.
Sometimes we need finer control over when exactly is the data acquired and worry about the download later. We can use the acquire argument to ~.base.Acquisition
. This means that the data acquisition can be invoked before data download. ~.base.Acquisition
calls its acquire first and only when it is finished connects producer with consumers.
The Experiment class has the attribute :py.base.Experiment.ready_to_prepare_next_sample
which is an instance of an asyncio.Event
. This can be used to tell that most of the experiment is finished and a new iteration of this experiment can be prepared (e.g. by the concert.directors.base.Director
. In the .base.Experiment.run
the :py.base.Experiment.ready_to_prepare_next_sample
will be set that at the end of an experiment is is always set. In the beginning of the .base.Experiment.run
it will be cleared. This is an example implementation making use of this:
from concert.experiments.base import Experiment, Acquisition
class MyExperiment(Experiment):
async def __ainit__(self, walker, camera):
acq = Acquisition("acquisition", self._produce_frames)
self._camera = camera
await super().__ainit__([acq], walker)
async def _produce_frame(self):
num_frames = 100
async with self._camera.recording():
# Do the acquisition of the frames in camera memory
# Only the readout and nothing else will happen after this point.
self.ready_to_prepare_next_sample.set()
async with self._camera.readout():
for i in range(num_frames):
yield await self._camera.grab()
A basic frame acquisition generator which triggers the camera itself is provided by .frames
concert.experiments.imaging.frames
There are tomography helper functions which make it easier to define the proper settings for conducting a tomographic experiment.
concert.experiments.imaging.tomo_angular_step
concert.experiments.imaging.tomo_projections_number
concert.experiments.imaging.tomo_max_speed
In :pyconcert.experiments.synchrotron
and :pyconcert.experiments.xraytube
implementations of Radiography, SteppedTomography, ContinuousTomography and SteppedSpiralTomography, ContinuousSpiralTomography and GratingInterferometryStepping are implemented for the two different source types.
For detailed information how they are implemented, you can have a look at the base classes concert.experiments.imaging.Radiography
, concert.experiments.imaging.Tomography
, concert.experiments.imaging.SteppedTomography
, concert.experiments.imaging.ContinuousTomography
, concert.experiments.imaging.SteppedSpiralTomography
, concert.experiments.imaging.ContinuousSpiralTomography
and concert.experiments.imaging.GratingInterferometryStepping
.
In the standard configuration, all tomography and radiography experiments first acquire the dark images, then the flat images and the projection images of the sample at the end. This order can be adjusted by the ~concert.experiments.base.Experiment.swap
command.
concert.experiments.synchrotron.Radiography
concert.experiments.xraytube.Radiography
concert.experiments.synchrotron.SteppedTomography
concert.experiments.xraytube.SteppedTomography
concert.experiments.synchrotron.ContinuousTomography
concert.experiments.xraytube.ContinuousTomography
concert.experiments.synchrotron.SteppedSpiralTomography
concert.experiments.xraytube.SteppedSpiralTomography
concert.experiments.synchrotron.ContinuousSpiralTomography
concert.experiments.xraytube.ContinuousSpiralTomography
In this grating based phase contrast imaging implementation a single projection is generated. The grating is stepped with and without the sample while images are recorded. Dark images are also recorded. If the concert.experiments.addons.PhaseGratingSteppingFourierProcessing
addon is attached, directly the intensity, visibility and differential phase are reconstructed.
concert.experiments.synchrotron.GratingInterferometryStepping
concert.experiments.xraytube.GratingInterferometryStepping
concert.experiments.control
Addons are special features which are attached to experiments and operate on their data acquisition. For example, to save images on disk:
from concert.experiments.addons import ImageWriter
# Let's assume an experiment is already defined
writer = ImageWriter(experiment.acquisitions, experiment.walker)
writer.attach()
# Now images are written on disk
await experiment.run()
# To remove the writing addon
writer.detach()
concert.experiments.addons
To demonstrate how a typical experiment can be run in an empty session with dummy devices:
from concert.storage import DirectoryWalker
from concert.ext.viewers import PyplotImageViewer
from concert.experiments.addons import Consumer, ImageWriter
from concert.devices.motors.dummy import LinearMotor, ContinuousRotationMotor
from concert.devices.camera.dummy import Camera
from concert.devices.shutters.dummy import Shutter
# Import experiment
from concert.experiments.synchrotron import ContinuousTomography
# Devices
camera = await Camera()
shutter = await Shutter()
flat_motor = await LinearMotor()
tomo_motor = await ContinuousRotationMotor()
viewer = await PyplotImageViewer()
walker = DirectoryWalker(root="folder to write data")
exp = await ContinuousTomography(walker=walker,
flat_motor=flat_motor,
tomography_motor=tomo_motor,
radio_position=0*q.mm,
flat_position=10*q.mm,
camera=camera,
shutter=shutter)
# Attach live_view to the experiment
live_view = Consumer(exp.acquisitions, viewer)
# Attach image writer to experiment
writer = ImageWriter(exp.acquisitions, walker)
# check all parameters by typing 'exp'
# Run the experiment
f = exp.run()
# Wait until the experiment is done
await f