#########################
DAMARIS for Experimenters
#########################

**DAMARIS** :cite:`Gadke:2006fk` is the software, established by Achim Gädke, used to control the PFG spectrometer. There are two separate parts: the back end, which controls the hardware and executes the pulse program, and a front end where the pulse program is defined and results are displayed. The programming language used for the scripts in the front end is Python, because it is easy to learn and the programs are easy to maintain.

The DArmstadt MAgnetic Resonance Instrument Software is an in-house developed open-source spectrometer control software which gives every scientist a very versatile and highly flexible instrumentation tool to operate and design novel and/or complex NMR experiments. By using standard pulse programmers and data acquisition PCI and PCIe computer cards this allows any research group a way to build NMR spectrometers in a very cost effective way.
Using open-source computer software to operate the hardware enables the user to get a very detailed understanding and a high level of control over the measurement data acquisition procedure. Unlike many home-built spectrometers and control software which are used specifically and tied to one spectrometer, DAMARIS was designed from the ground up for a flexible use in many different types of spectrometers. Another major design goal was to make the control hardware characteristics transparent to the spectrometer developer. In our group in Darmstadt DAMARIS is used on 9 different spectrometers ranging from Mechanical and Fast Electronic Field Cycling, Static Field Gradient (SFG) NMR as well as Pulsed Field Gradient (PFG) and 2H spectrometers. It is used successfully to measure multidimensional 2H NMR exchange spectra as well as for using shaped pulses (radio frequency and PFG) in other experiments. Beside the use in Darmstadt, DAMARIS is also in use at the Universities of Dortmund and Düsseldorf for time-domain, SFG and PFG spectrometers.
The front end of DAMARIS is user friendly and the usage is not difficult to learn. There is  introductory documentation available [1]. The pulse programs are defined and written in Python which is, again, a very user friendly interpreted programming language. Using a full-fledged programming language with a huge amount of freely available libraries and a very active scientific community is a key advantage of using DAMARIS. The use of the scipy and numpy libraries (similar to Matlab) for numerical analysis and data treatment makes it possible, for example, to pre- or post-process data at single-shot or accumulation level using custom filters. Leveraging Python modules for serial communication permits DAMARIS to control auxiliary hardware like temperature controllers of different make and models (Lakeshore, Cryovac, Oxford Instruments, other RS232, Modbus, and GPIB controlled devices), oscilloscopes, magnet drift compensation controller, microprocessors (Arduino, etc.), step motor/piezo positioning systems, and it is even possible to react to and modify the experiments according to feedback from these devices.



******
Basics
******
The basic idea in DAMARIS is the separation of an experiment in three steps:

- Experiment description where the experiment sequence is described
- Back end executes the sequences obtained from the experiment description
- Results from the back end are processed in the result script

.. figure:: pics/damaris_description.png
   :alt: The DAMARIS front end
   :width: 600px

   The DAMARIS front end

The front end is composed of five tabs (see figure above): Experiment, Result, Display, Log and Configuration. This introduction into DAMARIS will begin with a NMR "Hello World" program, i.e. recording a FID. More elaborated techniques like phase cycling and PFG will be discussed in the :ref:`advanced` section.

Configuration
=============

.. figure:: pics/config1.png
   :alt: Configuration tab
   :width: 600px

   Configuration tab

The Configuration tab (see figure above) offers a convenient way to setup DAMARIS.
Following is a short description of the options:

1. Toggle the start of the back end
2. Path to the back end executable, here **PFGcore.exe**
3. Path to the spool directory, where the job files will be written or picked up
4. Delete the job files after execution
5. Filename of the logfile
6. Number of logfiles to keep
7. Toggle the start of the experiment script
8. Toggle the start of the result script
9. Toggle the deletion of results after they have been processed by the result script
10. Filename of the data pool file
11. Write interval after which the data pool is rewritten
12. Compression settings
13. Authors, License and version numbers of used components

Experiment Script
=================
.. code-block:: python
   :caption: Experiment script for recording a FID
   :linenos:

   from damaris import Experiment

   def fid_experiment():
       e = Experiment()
       e.ttl_pulse(length=1e-6, channel=1)
       e.wait(time=1e-6)
       e.record(samples=8192, frequency=2e6)
       return e.get_state_sequence()

   def experiment():
       return fid_experiment()

On the first line, the basic Experiment module is imported which loads the methods and commands to control an experiment. Next, a function **fid_experiment** is defined which will contain everything necessary to run a single scan.
The first line inside the function creates an object **e** in which the states will be written by subsequent application of methods to this experiment object. This sequence of states is then passed to the caller.
When the "Execute" button is pressed, the experiment script is scanned for a function called **experiment**.
Thus, to run **fid_experiment**, the **experiment** function has to contain a call to the **fid_experiment**.

Behind the scenes is the experiment script that writes the state sequence from **fid_experiment**, an XML^1^ file, into the spool directory and the back end is then executed. The back end is simply reads these files, translates the contents to pulse programmer code and stores the generated code into the memory of the pulse card. Finally, the pulse program is executed. Then the results are written by the back end into the spool directory, where they are provided to the result script. In the result script, the function **result** is then executed.

Result Script
=============
The function **result** in the result tab defines what should be done with the results. Here is a short example how to display the recorded signal:

.. code-block:: python
   :caption: Result script which is displaying the result
   :linenos:

   from damaris import get_result

   def result(results):
       # get the first result
       timesignal = results[0]
       # display the signal in the graph
       data["Timesignal"] = timesignal

After pressing the "Execute" button to start the experiment, the Display tab is opened. With the drop down menu right to "Source" under the graph window one can choose which data should be displayed. This list is built dynamically by the data dictionary in the result script, where the key denotes the name to be shown and the value contains the data. In this example we overwrite the data in the key **Timesignal** on every scan with the new result.
There are now two possibilities to export the figure:

1. Saving the plot as a figure (EPS, PNG, JPEG and more) by pressing the save button in the toolbar below the plot area
2. Saving the data of the plot as CSV file (comma separated values) by pressing the button on the right side of the toolbar under the plot


Accumulation
============

Sometimes the detected signals intensity is low compared to the noise, and can therefore not be seen. A measure for the quality of the recorded signal is the signal-to-noise ratio (SNR) which is defined as the ratio of signal versus the standard deviation of the noise, i.e. noise strength.
The SNR is improved by measuring the signal many times and averaging the recorded signals, also called accumulation.
In DAMARIS this can be achieved by creating a loop in the experiment script and accumulating the results in the result script.

.. code-block:: python
   :caption: Experiment script showing how to create a loop
   :linenos:

   from damaris import Experiment

   def accumulate_experiment():
       e = Experiment()
       # define how many scans you want
       scans = 10
       for i in range(scans):
           e.ttl_pulse(length=1e-6, channel=1)
           e.wait(time=1e-6)
           e.record(samples=8192, frequency=2e6)
       return e.get_state_sequence()

   def experiment():
       return accumulate_experiment()

.. code-block:: python
   :caption: Result script accumulating the data
   :linenos:

   from damaris import get_result, Accumulation

   def result(results):
       akku = Accumulation(error=True)
       for timesignal in results:
           akku += timesignal
       data["Accumulated"] = akku


Phase Cycling
=============

Artifacts due to channel imbalance in the receiver, inhomogeneity in the RF field and unwanted signals, for example primary echos, can be suppressed or cancelled out by cycling the phases :cite:`Berger:2004vn` of the receiver and the RF pulses and then accumulating the signal.

.. code-block:: python
   :caption: Experiment script for phase cycling
   :linenos:

   from damaris import Experiment

   def phase_cycling_experiment():
       e = Experiment()
       phases = [0, 90, 180, 270]
       for phase in phases:
           e.set_phase(phase=phase)
           e.ttl_pulse(length=1e-6, channel=1)
           e.wait(time=1e-6)
           e.record(samples=8192, frequency=2e6)
       return e.get_state_sequence()

   def experiment():
       return phase_cycling_experiment()

New in this script is the **set_description** method and a parameter *run* that is passed to the experiment. The **set_description** method is used to set descriptions and values important to the experimenter or to pass values to the result script. These descriptions are written into the XML job file and stored by the back end into the result file. In this way it is possible to pass values to the result script so the latter can act upon these. In this case the result is either added or subtracted to the accumulation variable.


Changing Parameters
===================

Now, this script is modified to measure the longitudinal relaxation time T1 by a so called inversion recovery experiment. Therefore the π/2-pulse is preceded by π-pulse both separated by the variable time τ. The FID after the second pulse is recorded. In order to get a good signal, accumulation and phase cycling are implemented.
In the result script, the magnetization will be obtained and, together with the corresponding variable τ, stored in an ASCII file for determination of T1.

.. code-block:: python
   :caption: Experiment script illustrating the use of variables
   :linenos:

   from damaris import Experiment, lin_range

   def t1_experiment():
       e = Experiment()
       for tau in lin_range(start=1e-3, stop=5e-2, step=1e-3):
           e.set_description("tau", tau)
           e.ttl_pulse(length=1e-6, channel=1)  # 180 degree pulse
           e.wait(time=tau)
           e.ttl_pulse(length=1e-6, channel=1)  # 90 degree pulse
           e.wait(time=1e-6)
           e.record(samples=8192, frequency=2e6)
       return e.get_state_sequence()

   def experiment():
       return t1_experiment()

.. code-block:: python
   :caption: Result script writing the amplitude and corresponding delay time τ between the pulses to a file
   :linenos:

   from damaris import get_result

   def result(results):
       datafile = open("t1_data.dat", "w")
       for timesignal in results:
           tau = timesignal.get_description('tau')
           # calculate magnetization from timesignal
           magnetization = calculate_magnetization(timesignal)
           datafile.write("%f %f\n" % (tau, magnetization))
       datafile.close()



Data Handling
=============

To store the result for further data analysis one has several built-in possibilities in DAMARIS. One can store the results as ASCII file or as HDF5 file. Writing an ASCII file is usable for smaller data-sets and a small number of experiments.
The speed-up in post-processing and analysis is already 24 fold in small data-sets with 32768 points per channel and 25 files on a Athlon XP 2600+ CPU with 512 MB RAM running Debian Sarge Linux. Thus, writing HDF5 with compression enabled is highly recommended. Additionally the HDF files are much much smaller than the ASCII files. Nethertheless we can save data in simple text files:


.. code-block:: python
   :caption: Part of a result script for saving data into an ASCII file
   :linenos:

   def result(results):
       f = open('testfile.dat', 'w')
       timesignal = results[0]
       timesignal.write_to_csv(destination=f)
       f.close()



For larger experiments it is advantageous to store data in a HDF5 file. This is the Hierarchical Data Format developed by the NCSA (National Center for Supercomputer Applications) for storing complex tables and big amounts of data in an effective way. It supports grouping of data, annotation and compression. A very convenient way to access HDF files in Python is the pyTables module, which is also used by DAMARIS internally.


.. container:: image-container

   .. image:: pics/datapool_root.png
      :width: 45%
      :alt: First image

   .. image:: pics/datapool_root.png
      :width: 45%
      :alt: Second image

   .. image:: pics/datapool_data.png
      :width: 45%

   .. image:: pics/datapool_table.png
      :width: 45%



   A sample data pool file, opened with HDFCompass. The data is stored as a table (lower right).


After an experiment in DAMARIS has been finished, the results in the data dictionary as well as the experiment and result scripts themselves are written automatically to a HDF file, the so called **data pool**.
In the configuration tab (figure above, 10-12), filename, compression ratio, compression library and write interval can be set up.
The write interval is the time after which the data pool file should be periodically rewritten and is useful in case of hardware or experiment failure.
The results from are stored as 64-bit floating point values in tables which are compressed/decompressed on-the-fly when writing/reading.

.. note::
    You can select from ultiple compression libraries, but for compatibility reasons *zlib* is highly recommended. Compression levels higher than 3 are typically not beneficial.

A typical method to already extract data like amplitudes etc. is possible by using the MeasurementResult class.

.. code-block:: python
   :linenos:

   from damaris import MeasurementResult

   def fid():
       # create a measurement object
       measurement = MeasurementResult("Magnetization(tau)")
       for timesignal in results:
           # extract "tau" from the description dictionary
           tau = timesignal.get_description('tau')
           ...
           # get the magnetization from the timesignal
           magnetization = sqrt(timesignal.y[0][80:100].mean()**2
                               + timesignal.y[1][80:100].mean()**2)
           measurement[tau] += magnetization
           # provide the magnetization curve
           data["Magnetization(tau)"] = measurement
           ...

Essentially, a dictionary "measurement" is created with *tau* as the key. For each new key the current magnetization is added. This dictionary is then added to the data pool. The advantage over the previous listing is that the magnetization vs. τ curve can be directly displayed in order to gain an overview of the running experiment, because the "measurement" dictionary is added to the data pool. In this example, the value also stores statistics from the accumulation. The data dictionary is stored automatically as described before, including the result and experiment scripts, information about the used back end and a timeline of the experiment.

Note that we can also create a HDF5 file from scratch! In this case experiment and result script are not saved in the resulting file. The advantage is that one can create arbitrary complex structured files at the cost of having a more complicated result script.

.. code-block:: python
   :caption: Result script for saving data in a HDF5 file
   :linenos:

   import tables
   from damaris import get_result

   def result(results):
       hdf = tables.openFile("myexperiment.h5", "w")
       # create a group
       group = hdf.createGroup("/", "experiment")
       # save each result
       for i, timesignal in enumerate(results):
           hdf.createArray(group, "result_%d" % i, timesignal.get_ydata())
       hdf.close()


Fourier Transform Module
========================

A module was written for an easy calculation of spectra from time domain data. The Fourier transformation module "DamarisFFT" is based on the **fft** module of **numpy**, an array module for Python. There are no interactive capabilities such as phase correction incorporated yet. The FFT method can be applied directly to the object. The order in which these methods are applied *does* matter. Here are the available methods and a short explanation of their effects (values in italic are default values).
The change is *in-place*, which means if the timesignal is needed later on, a copy of it should be made.

- **clip(start=**\ *None* **, stop=**\ *None* **)**
   Only the data between start and stop is returned.
   The start and stop parameters can be either in time or frequency domain.

- **baseline(lastpart=**\ *0.1* **)**
   This method should be applied first as it corrects the baseline of the signal, taking the last part of the signal for determining the correction.

- **autophase()**
   This will try to automatically phase the spectrum to maximize the real signal. Works only reliably at sufficient high SNR (>20)

- **fft(samples=**\ *None* **)**
   This method returns the real and the imaginary part of an FFT of the timesignal. Zero-filling of the timesignal can be done via the **points** keyword. If **points** is bigger than the number of data points, the rest will be filled with zeros (zero-filling/zero-padding).

- **abs_fft(points=**\ *None* **)**
   This method returns an absolute FFT of the timesignal. Zero-filling of the timesignal can be done via the **points** keyword. If **points** is larger than the number of data points, the rest will be filled with zeros (zero-padding). The **zoom** keyword accepts either a tuple with centre frequency and width in Hz. Auto zooming the highest peak can be achieved by setting the centre frequency to *auto*.

In order to suppress side-lobes, enhance resolution or improve SNR it is common practice in data processing to apply windowing functions to the data.
Several windowing functions are available in the "DaFFT" module. The first two functions, i. e. the exponential and gaussian window function are SNR enhancing windows, while the double exponential window function is resolution enhancing.
The TARF window is both, SNR and resolution enhancing :cite:`Traficante:1987fk`.
All four are commonly used in NMR spectroscopy. The line broadening factor LB is the rate (in Hz) in which the window function decays. The higher the value, the faster the function decays, hence the resulting spectrum will be more "smooth" but the peaks will become wider.

- **exp_window(line_broadening=**\ *10* **)**
   This sets an exponential window over the data, the beginning of the signal is weighted more, where as the tail of the signal gets suppressed. Best results are obtained with line broadening factor set to the peak width:

   .. math::
      f(x)=exp(-tp \cdot LB)

- **gauss_window(line_broadening=**\ *10* **)**
   This sets a gaussian window over the data:

   .. math::
      f(x)=exp(-(tp \cdot LB)^2)

- **dexp_window(line_broadening=**\ *-10* **, gaussian_multiplicator=**\ *0.3* **, show=**\ *0* **)**
   This sets a double-exponential window over the data, which enhances the tail of the FID thus prolonging it and increases the resolution. Note that the line broadening should have a negative value here. The reason is that weighting is supposed to increase from the beginning (exponential part) and then later to decrease again (gaussian part). Another point to mention is that this window is in fact transforming a lorentzian peak in a gaussian peak which has the property of a narrower base:

   .. math::
      f(x)=exp(-tp \cdot LB - GM \cdot tp^2)

- :meth:`damaris.data.DamarisFFT.DamarisFFT.traf_window`(line_broadening=10 )
   This sets a TRAF window :cite:p:`Traficante:1987fk` over the data:

   .. math::
      f(x)=\frac{exp(-tp \cdot LB))^2}{[exp(-tp \cdot LB))^3 + (exp(-aq\_time \cdot LB)]^3}

Following standard windowing functions :cite:p:`Butz:1998fr` are available additionally. Note that the whole data will be windowed, not only a subset. Furthermore, these windows are not suitable for FID's or similar signals, because they are symmetric around the middle of the data set and zero at the beginning and the end.

- :meth:`damaris.data.DamarisFFT.DamarisFFT.hanning_window`
- :meth:`damaris.data.DamarisFFT.DamarisFFT.hamming_window`
- :meth:`damaris.data.DamarisFFT.DamarisFFT.blackman_window`
- :meth:`damaris.data.DamarisFFT.DamarisFFT.bartlett_window`
- :meth:`damaris.data.DamarisFFT.DamarisFFT.kaiser_window`

To use this methods one has to apply consecutively the methods to an *timesignal* or *accumulation* object:

.. code-block:: python
   :caption: FFT example
   :linenos:

   data["FFT"] = timesignal.baseline(0.4).exp_window(line_broadening=100).abs_fft().clip(-1e3, 1e3)

This applies baseline correction using the last 40% of the timesignal for determination of the signal. Then, an exponential window is applied. From this timesignal an FFT is calculated which is further clipped to view to the data between -1 kHz with 1 kHz width.


Good Practices
==============

- Save often. It can become slow to open a HDF file in append mode and close it again for *each* result, but in case of an error, the file is not completely lost.
- Keep the experiment function simple. Everything necessary for a scan should be contained in a function, the parameters should stay in the **experiment** function. This makes it possible to loop over several parameters easier and, in case of complex experiments, all variables are changed in one place: the call to the function.
- Display if necessary. If the repetition time of subsequent scans is short (several ms) it can slow down the result script notably. The result script will take care of this by not displaying every single shot, but displaying nothing would further speed up the processing of the results.
- Save disk space. If the options "delete results" and "delete jobs" are switched off, the spool directory can get clobbered by several thousand files and in the worst case, one can run out of *inodes*. If it is not possible to delete the files in the directory anymore (rm gives an error "too much arguments") the following command can help deleting the files anyway:

   .. code-block:: bash

      find . -name job\* -exec rm {} \;

   which will delete all files starting with *job* in the current directory.

- It is a good idea to put a *synchronize()* statement in the experiment script, for example after the 100th accumulation. This will write job files only until the synchronize is requested. Then, the front end will wait writing new job files, until the result script has processed all current results. In big experiments, several ten thousands job of files would be written without this, which can lead to problems with the file system and performance.

   .. code-block:: python

      if accu % 100 == 0:
          synchronize()

- Keep an eye on the logs. In case of errors it is highly recommended to check the log file in the log path (as defined in the configuration tab) *as well* as the "Log" tab for error messages.


Tuning and Development
======================

When new result scripts are developed, it is an advantage if the actual experiment has not to be repeated for every minor change or tweak in the result script. This can become a very tedious task when the sample has a long T1 relaxation, like water T1 ≈ 3s :cite:`PhysRev.111.1201`.
In order to use this technique, the experiment has to be run once with the option "delete results after processing" turned off. This leads to the result files in the spool directory not being deleted. Starting the experiment again with the option "run back end" switched off, the front end will read the results again like if it would be a new experiment, hence one can test new result scripts quite conveniently.
Developing experiment scripts can be done in a similar manner: the dummy back end (to be set in the configuration tab) can be used to test new experiment scripts without the need to have a spectrometer at hand.


Data Processing Outside DAMARIS
===============================

For post-processing of the data, several options are available, depending on the format of the data. Most programs can handle ASCII files (gnuplot, Origin, fitsh, etc.), so this is the more flexible format. HDF can be read by pyTables, Matlab, Octave, R, Mathematica and more.


Experiment Script Commands
==========================

Following is a list of available commands in an experiment script.

- **ttl_pulse(length, channel=**\ *None* **, value=**\ *None* **)**
   Gives out *either* a TTL pulse with duration **length** on **channel** (counting from zero), *or* multiple channels given by the binary representation of **value**.

   .. figure:: pics/pins_ttl_bit.png
      :alt: Line driver with a decimal value 17 set
      :width: 400px

      Line driver with a decimal value 17 set (or 0x11 hexadecimal). In binary, channel 0 and channel 3 are on

   Example:

   .. code-block:: python

      e.ttl_pulse(length=5e-6, channel=1)
      e.ttl_pulse(length=3e-6, value=3)

   This example is used to create a RF pulse with a pulse length of 3 μs. The first statement sets the gate pulse (channel 0) of the RF amplifier for 5 μs, while the second statement combines the gate and RF pulse on channels 0 and 1 (2^0 + 2^1 = 3). Hexadecimal representation (number starts with *0x*) is convenient as the numbers are shorter: To set all channels, **value** would be in decimal 16777215 and in hexadecimal 0xffffff. One letter in hexadecimal represents four bits.

- **ttls(length=None, value=None)**
   Same as **ttl_pulse(length, value)**

- **wait(time)**
   Wait specified **time** in seconds. Minimum time is 90 ns, maximum time is essentially unlimited (years). The limit imposed from the pulse programmer is circumvented by the driver.

   Example:

   .. code-block:: python

      e.wait(length=2e-3)

- **record(samples, frequency, timelength=**\ *None* **, sensitivity=**\ *None* **)**
   Records data with given number of **samples**, sampling-frequency **frequency** and **sensitivity** in Volts.
   If **timelength** is given, this state will stay the given time. The maximum sampling-frequency is 20 MHz and the onboard memory can hold 8M samples shared by both channels. Sensitivity can be one of 0.2, 0.5, 1, 2, 5 and 10 V.

   Note that multiple record statements can be in a single scan (gated sampling) or in a loop.

   Example:

   .. code-block:: python

      e.record(samples=1024, frequency=2e6, sensitivity=2)

   Records a signal with 1024 data points and 2 MHz sampling-frequency. The sensitivity will be ± 2 V. With a 14bit ADC card this gives a resolution of 0.2 mV.

- **loop_start(iterations)/loop_end()**
   This creates a loop of given number of **iterations** and has to be closed by loop_end().
   Commands inside the loop can not change, i.e. the parameters are the same for each loop run. This loop is created on the pulse programmer, thus saving commands.

   Example:

   .. code-block:: python

      e.loop_start(iterations=10)
      e.rf_pulse(channel=3, length=2e-6)
      e.wait(time=2e-6)
      e.loop_end()

   This loop will create 10 pulses with 2 μs length and 2 μs apart.

- **set_frequency(frequency, phase, ttls=**\ *0* **)**
   Sets the **frequency** and **phase** of the frequency source and optionally further channels. The time needed to set the frequency is 2 μs.

   Example:

   .. code-block:: python

      e.set_frequency(frequency=300.01e6, phase=0)

   The frequency generator will deliver 300.01 MHz.

   .. code-block:: python

      e.set_frequency(frequency=300.01e6, phase=0, ttls=3)

   Same as above, but also sets channel 0 and 1.

- **set_phase(phase, ttls=**\ *0* **)**
   This command can be used to change the phase of the frequency. The *phase* is given in degree. The *ttls* keyword has the same functionality like in the set_frequency command. Setting the phase is done in 0.5 μs.

   Example:

   .. code-block:: python

      e.set_phase(phase=90)
      e.record(samples=1024, frequency=2e6, sensitivity=2)

   This would set the receiver phase to 90 degree.

- **set_pts_local()**
   If the frequency source is set to remote control mode, this can be used to set to local mode, thus the frequency of the PTS synthesizer can be changed manually.

- **set_pfg(length, dac_value, shape=('rec',0), trigger=4, is_seq=**\ *0* **)**
   With this command, pulsed field gradients are generated. In order to create arbitrary ramps or shapes, **is_seq** can be set to 1, leading the DAC to keep the current state until a new strength is applied.
   The trigger keyword allows to set a trigger line with the start of the gradient pulse.
   Certain gradient shapes are predefined:

   - rectangular ('rec')
   - sin ('sin')
   - sin² ('sin2')

   You can select them by giving the shape keyword a tuple with shape and resolution in s

   Example:

   .. code-block:: python

      e.set_pfg(length=1e-3, dac_value=15040, is_seq=0)

   This would create a 1 ms long PFG pulse with 1 Tm⁻¹

   .. code-block:: python

      for val in lin_range(start=0, stop=pi, step=pi/50):
          gradient = int(sin(val)**2 * 15040)
          e.set_pfg(length=1e-3/50, dac_value=gradient, is_seq=1)

   This would create a 1 ms long PFG pulse shaped like half period of a sin² with an amplitude of 1 Tm⁻¹.

   .. code-block:: python

      e.set_pfg(length=1e-3, dac_value=15040, shape=('sin2', 1e-5))

   This would create a sin² shaped gradient with 1ms length and 100 interpolation points.

- **set_description(key, value)**
   Setting a description is accomplished by this command. It creates an entry **key** with value **value** in the description dictionary. In case of the data being stored in a HDF5 file this dictionary is stored as well.

   Example:

   .. code-block:: python

      e.set_description(key="tau", value=2e-3)

Following are convenient functions for use in loops.

- **lin_range(start, stop, step)** This is used for loops. It increases **start** with **step** until **stop** is reached.

   Example:

   .. code-block:: python

      def experiment():
          for tp in lin_range(start=5e-7, stop=10e-6, step=5e-7):
              yield pi_pulse(pulselength=tp)

   Starting from 0.5 μs, increase pulse length with 0.5 μs until 10 μs.

- **log_range(start, stop, stepno)** Divide the range **start** to **stop** logarithmically in **stepno** steps.

   Example:

   .. code-block:: python

      def experiment():
          for t in log_range(start=5e-3, stop=10, stepno=10):
              yield inversion_recovery(tau=t)

   Variate the distance τ of the two pulses in the inversion recovery experiment logarithmically from 5 ms to 10 s. The values would be:
   ['0.0050', '0.0116', '0.0271', '0.0630', '0.1466', '0.3411',
   '0.7937', '1.8469', '4.2975', '10.0000']

- **staggered_range(some_range, size=**\ *1* **)** Creates a staggered range from another range, leaving out **size** values in a row and appending the left out later.

   Example:

   .. code-block:: python

      def experiment():
          myrange = log_range(start=5e-3, stop=10, stepno=10)
          for tp in staggered_range(myrange, size=2):
              yield inversion_recovery(tau=t)

   This is the same as before, but the values are now staggered: 12345678 will become 12563478. Another possibility for achieving something similar is the *shuffle* function.

- **interleaved_range(some_range, size=**\ *1* **)** Creates an interleaved range from another range, appending every n-th point.

   Example:

   .. code-block:: python

      def experiment():
          myrange = log_range(start=5e-3, stop=10, stepno=10)
          for tp in interleaved_range(myrange, size=3):
              yield inversion_recovery(tau=t)

   The values are now interleaved: 12345678 will become 14725836.

- **combine_ranges(*ranges)** With this function it is possible to combine several ranges

   Example:

   .. code-block:: python

      def experiment():
          mylinrange = lin_range(start=1e-3, stop=4.9e-3, step=1e-4)
          mylogrange = log_range(start=5e-3, stop=10, stepno=10)
          for tp in combine_ranges(mylinrange, mylogrange):
              yield inversion_recovery(tau=t)

Result Script Commands
======================

Following is a list of available commands in a result script. These are methods to be applied to a ADC_Result and Accumulation object. An accumulation object needs to be created before any methods can be applied.

Example:

.. code-block:: python

   akku = Accumulation(error=True)
   akku += timesignal

- **get_result_by_index(index)**
   If several **record** statements occur in an experiment, the results are saved in an array and can be accessed by this method. *Index* starts with 0.

   Example:

   .. code-block:: python

      timesignal.get_result_by_index(1)

   This would return the result of the second record statement.

- **get_sampling_rate()**
   Returns ADC card sampling rate

- **uses_statistics()**
   Returns *False* if the result is a single ADC result and *True* if the result is an accumulation with statistics enabled.

- **write_to_csv(destination=**\ *sys.stdout* **)**
   Writes the result into an ASCII file, destination has to be an open file. The file has to be closed after write_as_csv has been issued otherwise the data is not written to the harddrive.

   Example:

   .. code-block:: python

      f = open('testfile.dat', 'w')
      timesignal.write_to_csv(destination=f)
      f.close()

- **write_to_hdf(hdffile, where, name, title, compress=**\ *None* **)**
   Writes the data to a HDF5 file **hdffile**.

- **get_description(key)**
   Returns **value** of description **key** as string. Note that in the case of application to an accumulation object, only the common descriptions are stored, leading to a proper set of relevant parameters.

- **get_xdata()**
   Returns a copy of timedata.

- **set_xdata(pos, value)**

- **get_ydata(channel)**
   Returns a copy of the data from channel **channel**

- **set_ydata(channel, pos, value)**

- **get_number_of_channels()**
   Returns the number of ADC channels, usually 2

- **get_xlabel()/get_ylabel()**
   Returns the x/y-axis description in display register

- **set_xlabel(label)/set_ylabel(label)**
   Sets the label of the x/y-axis

- **get_text(index)**
   Returns labels to be plotted (List)

- **set_text(index, text)**
   Sets labels to be plotted

- **get_title()**
   Returns the title of the plot

- **set_title(title)**
   Sets the title of the plot

- **get_legend()**
   Returns the legend of the plot (dictionary)

- **set_legend(channel, value)**
   Sets the legend of the plot

- **get_xmin()**
   Returns minimum of x

- **get_xmax()**
   Returns maximum of x

- **get_ymin()**
   Returns minimum of y

- **get_ymax()**
   Returns maximum of y



.. bibliography::

