Skip to content

Latest commit

 

History

History
279 lines (196 loc) · 10.3 KB

experiments.rst

File metadata and controls

279 lines (196 loc) · 10.3 KB

Experiments

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.

Acquisition

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 .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.

Advanced

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()

Imaging

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

Synchrotron and X-Ray tube experiments

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.

Radiography

concert.experiments.synchrotron.Radiography

concert.experiments.xraytube.Radiography

SteppedTomography

concert.experiments.synchrotron.SteppedTomography

concert.experiments.xraytube.SteppedTomography

ContinuousTomography

concert.experiments.synchrotron.ContinuousTomography

concert.experiments.xraytube.ContinuousTomography

SteppedSpiralTomography

concert.experiments.synchrotron.SteppedSpiralTomography

concert.experiments.xraytube.SteppedSpiralTomography

ContinuousSpiralTomography

concert.experiments.synchrotron.ContinuousSpiralTomography

concert.experiments.xraytube.ContinuousSpiralTomography

GratingInterferometryStepping

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

Control

concert.experiments.control

Addons

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

Running an experiment

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