Public Access
770 lines
30 KiB
Python
770 lines
30 KiB
Python
# -*- coding: utf-8 -*-
|
|
import types
|
|
import numpy
|
|
|
|
class StateBase(object):
|
|
def __init__(self) -> None:
|
|
pass
|
|
def to_xml(self, indent: str = "") -> str:
|
|
return indent + "<!-- " + repr(self) + " -->"
|
|
|
|
class StateSimple(StateBase):
|
|
def __init__(self, time: float, content: object | None = None) -> None:
|
|
super(StateSimple, self).__init__()
|
|
if time < 0:
|
|
raise AssertionError("time for state is negative!")
|
|
self.time = float(time)
|
|
self.content = content
|
|
|
|
def to_xml(self, indent: str = "") -> str:
|
|
s = indent + '<state time="%s"' % repr(self.time)
|
|
if self.content is None:
|
|
return s + '/>\n'
|
|
s += '>\n'
|
|
s += indent + ' ' + str(self.content) + '\n'
|
|
s += indent + '</state>\n'
|
|
return s
|
|
def __repr__(self) -> str:
|
|
return 'StateSimple(%s, %s)' % (self.time, repr(self.content))
|
|
|
|
class StateList(StateBase):
|
|
def __init__(self) -> None:
|
|
super(StateList, self).__init__()
|
|
self.list: list[object] = []
|
|
def to_xml(self, indent: str = " ") -> str:
|
|
s = ""
|
|
for k in self.list:
|
|
if hasattr(k, "to_xml"):
|
|
s += k.to_xml(indent)
|
|
else:
|
|
s += indent + str(k)
|
|
return s
|
|
def append(self, val: object) -> None:
|
|
self.list.append(val)
|
|
|
|
|
|
class StateLoop(StateList):
|
|
"""Represents a loop in the state tree"""
|
|
def __init__(self, repeat: int) -> None:
|
|
super(StateLoop, self).__init__()
|
|
self.repeat = repeat
|
|
def to_xml(self, indent: str = "") -> str:
|
|
s = indent + ('<sequent repeat="%d">\n' % self.repeat)
|
|
s += super(StateLoop, self).to_xml(indent + " ")
|
|
s += indent + '</sequent>\n'
|
|
return s
|
|
def __repr__(self) -> str:
|
|
return 'StateLoop(repeat=%d, %s)' \
|
|
% (self.repeat, repr(self.list))
|
|
|
|
|
|
#############################################################
|
|
# #
|
|
# Class: Experiment #
|
|
# #
|
|
# Purpose: Represents one full experiment (one program on #
|
|
# the pulse-card; one file) #
|
|
# #
|
|
#############################################################
|
|
from . import dac
|
|
|
|
class Experiment:
|
|
"""
|
|
Class holding the complete state tree for a single experiment. This state tree represents one
|
|
program on the pulse card. It is written in a single file and picked up by the backend.
|
|
|
|
:param Gating gating: gate length in s
|
|
:param list rf_sources: list of rf sources ttl channels
|
|
:param list rf_gates: list of rf gates ttl channels len(rf_gates) == len(rf_sources)
|
|
|
|
"""
|
|
job_id: int = 0
|
|
|
|
def __init__(self, gating: float | None = None, rf_sources: list[int] = [], rf_gates: list[int] = []) -> None:
|
|
self.job_id: int = Experiment.job_id
|
|
Experiment.job_id += 1
|
|
|
|
self.state_list: StateList = StateList()
|
|
self.list_stack: list[StateList] = []
|
|
self.description: dict[str, object] = { }
|
|
self.gating: float | None = gating
|
|
assert type(rf_sources) == type(list()), "rf_sources needs to be a list with the channels"
|
|
assert type(rf_gates) == type(list()), "rf_gates needs to be a list with the channels"
|
|
assert len(rf_gates) == len(rf_sources), "rf_sources and rf_gates must have equal number of entries"
|
|
self.rf_sources: list[int] = rf_sources
|
|
self.rf_gates: list[int] = rf_gates
|
|
|
|
#for tracking the experiment length:
|
|
#because loops are possible we need to track the length for each loop level
|
|
self.total_time: list[float] = [0.0]
|
|
|
|
#and we need to know the number of iterations of the loops to multiply the state.
|
|
self.loop_iterations: list[int] = [1]
|
|
|
|
# Commands -------------------------------------------------------------------------------------
|
|
|
|
def ttl_pulse(self, length: float, channel: int | None = None, value: int | None = None) -> None:
|
|
"""
|
|
Creates a state with length **length** and switches requested bits of the pulse programmer to HIGH.
|
|
|
|
This command generates a TTL pulse with the specified duration on a specific channel or multiple channels.
|
|
|
|
Parameters:
|
|
-----------
|
|
length : float
|
|
Pulse length in seconds.
|
|
channel : int, optional
|
|
Selects a single channel (No. 1 - 24). If provided, the value is calculated as 2^channel.
|
|
value : int, optional
|
|
Lines to set (integer). For example, value=3 selects channels 0 and 1 (2**0 + 2**1).
|
|
If both channel and value are None, the pulse is set to 0.
|
|
|
|
Examples:
|
|
---------
|
|
>>> e.ttl_pulse(length=5e-6, channel=1)
|
|
Creates a 5 microsecond pulse on channel 1.
|
|
|
|
>>> e.ttl_pulse(length=3e-6, value=3)
|
|
Creates a 3 microsecond pulse on channels 0 and 1.
|
|
|
|
>>> e.ttl_pulse(length=1e-6, value=0xffffff)
|
|
Creates a 1 microsecond pulse on all channels.
|
|
|
|
Notes:
|
|
------
|
|
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.
|
|
|
|
See Also:
|
|
---------
|
|
ttls : Same as ttl_pulse, but no channel keyword.
|
|
"""
|
|
the_value=0
|
|
if value is not None:
|
|
the_value=int(value)
|
|
elif channel is not None:
|
|
the_value=1<<channel
|
|
self.state_list.append(StateSimple(length, \
|
|
'<ttlout value="0x%06x"/>' % the_value))
|
|
|
|
self.total_time[-1] += length
|
|
|
|
## Same as ttl_pulse, but no *channel* keyword
|
|
def ttls(self, length: float | None = None, value: int | None = None) -> None:
|
|
"""
|
|
Same as ttl_pulse, but no *channel* keyword
|
|
|
|
:param float length:
|
|
:param int value: lines to set (integer)
|
|
:return:
|
|
"""
|
|
assert value is not None, "ttls needs an integer value"
|
|
the_value=int(value)
|
|
s_content = '<ttlout value="0x%06x"/>' % the_value
|
|
if length is not None:
|
|
self.state_list.append(StateSimple(length, s_content))
|
|
self.total_time[-1] += length
|
|
else:
|
|
self.state_list.append(s_content)
|
|
|
|
|
|
def rf_pulse(self, length: float | None = None, phase: float = 0, source: int = 0) -> None:
|
|
"""
|
|
Make an rf pulse, including gating and phase switching. Only possible if Experiment is configured.
|
|
|
|
:param float length: pulse length
|
|
:param float phase: pulse phase
|
|
:param int source: source id
|
|
"""
|
|
if not self.gating:
|
|
raise SyntaxError("Can not use rf_pulse without configuration: Experiment(gating=None, rf_sources=[], rf_gates=[]")
|
|
self.set_phase(phase, ttls=self.rf_gates[source])
|
|
self.ttls(length=self.gating-0.5e-6,value=self.rf_gates[source])
|
|
self.ttls(length=length, value=self.rf_gates[source]+self.rf_sources[source])
|
|
|
|
## Beginning of a new state
|
|
def state_start(self, time: float) -> None:
|
|
"""
|
|
Starts a state in the pulse programs with duration *time*.
|
|
This must be closed with :func:`state_end`.
|
|
|
|
Typically only used in very special cases.
|
|
"""
|
|
self.state_list.append('<state time="%s">\n' % repr(float(time)))
|
|
self.total_time[-1] += time
|
|
|
|
## End of *state_start*
|
|
def state_end(self) -> None:
|
|
"""
|
|
Closes a previous :func:`state_start`
|
|
"""
|
|
self.state_list.append('</state>\n')
|
|
|
|
def wait(self, time: float, ttls: int | None = None, gating: bool = False) -> None:
|
|
"""
|
|
Waits for a specified amount of time without performing any actions.
|
|
|
|
This command inserts a delay in the pulse sequence, allowing time for relaxation,
|
|
or synchronization with other events.
|
|
|
|
Parameters:
|
|
-----------
|
|
time : float
|
|
Time to wait in seconds.
|
|
The minimum time is 90 ns, and the maximum is essentially unlimited (years).
|
|
The backend driver circumvents the limit imposed by the pulse programmer by adding loops.
|
|
|
|
ttls : int, optional
|
|
Additional TTL lines to set (integer) during the wait period.
|
|
Allows simultaneous control of TTL lines while waiting.
|
|
For example, ttls=3 activates channels 0 and 1 (2^0 + 2^1).
|
|
|
|
gating : bool, optional
|
|
If True, reduces the wait time by the gating time (typically 2 µs).
|
|
This is useful for waiting in front of an rf_pulse to account for gating delays.
|
|
Default is False.
|
|
|
|
Returns:
|
|
--------
|
|
None
|
|
|
|
Examples:
|
|
---------
|
|
>>> e.wait(time=2e-3)
|
|
Waits for 2 milliseconds.
|
|
|
|
>>> e.wait(time=1e-6, ttls=3)
|
|
Waits for 1 microsecond while activating TTL channels 0 and 1.
|
|
|
|
>>> e.wait(time=5e-6, gating=True)
|
|
Waits for 5 microseconds, reduced by the gating time for rf_pulse synchronization.
|
|
|
|
"""
|
|
if gating:
|
|
assert self.gating is not None, "gating time must be set for gating=True"
|
|
time -= self.gating
|
|
if ttls is not None:
|
|
s_content = '<ttlout value="0x%06x"/>' % ttls
|
|
self.state_list.append(StateSimple(time,s_content))
|
|
else:
|
|
self.state_list.append(StateSimple(time))
|
|
|
|
self.total_time[-1] += time
|
|
|
|
|
|
def record(self, samples: int, frequency: float, timelength: float | None = None, sensitivity: float | list[float] | None = None, ttls: int | None = None, channels: int = 3, offset: int | list[int] | None = None, impedance: float | list[float] | None = None) -> None:
|
|
"""
|
|
Records data with a given number of samples, sampling frequency, and sensitivity.
|
|
This command starts data acquisition from the ADC (Analog-to-Digital Converter).
|
|
|
|
Parameters:
|
|
-----------
|
|
samples : int
|
|
Number of samples to record. This determines the number of data points in the resulting signal.
|
|
|
|
frequency : float
|
|
Sampling frequency in Hz. This determines how often samples are taken from the analog signal.
|
|
The maximum sampling frequency is 20 MHz.
|
|
|
|
timelength : float, optional
|
|
Length of this state in seconds. If not specified (None), it is calculated automatically as samples/frequency.
|
|
This parameter allows overriding the automatic calculation for special timing requirements.
|
|
|
|
sensitivity : float or list, optional
|
|
Sensitivity in U_MAX/V. Specifies the input voltage range for the ADC.
|
|
Accepted values are 0.2, 0.5, 1, 2, 5, and 10 V.
|
|
Can be a single value (applied to all channels) or a list of values (one per channel).
|
|
|
|
ttls : int, optional
|
|
Additional TTL lines to set (integer) during recording. Allows simultaneous control of TTL lines.
|
|
For example, ttls=3 activates channels 0 and 1 (2^0 + 2^1).
|
|
|
|
channels : int, optional
|
|
Channels to activate. Default is 3 (channels 0 and 1).
|
|
Accepted values are 1 (channel 0), 3 (channels 0 and 1), 5 (channels 0, 1, and 2), and 15 (all 4 channels).
|
|
|
|
offset : int or list, optional
|
|
Voltage offset for the ADC input. Can be a single integer value (applied to all channels)
|
|
or a list of values (one per channel).
|
|
Normally not used.
|
|
|
|
impedance : float or list, optional
|
|
Input impedance for the ADC. Can be a single number (applied to all channels)
|
|
or a list of values (one per channel).
|
|
Normally set in the backend config and not changeable.
|
|
|
|
Returns:
|
|
--------
|
|
None
|
|
|
|
Examples:
|
|
---------
|
|
>>> 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, providing a resolution of 0.2 mV with a 14-bit ADC card.
|
|
|
|
>>> e.record(samples=4096, frequency=10e6, sensitivity=[1, 2], channels=3)
|
|
Records a signal with 4096 data points and 10 MHz sampling frequency.
|
|
Channel 0 uses 1 V sensitivity, and channel 1 uses 2 V sensitivity.
|
|
|
|
>>> e.record(samples=2048, frequency=5e6, ttls=3)
|
|
Records a signal with 2048 data points and 5 MHz sampling frequency.
|
|
Simultaneously activates TTL channels 0 and 1.
|
|
|
|
Notes:
|
|
------
|
|
- Multiple record statements can be in a single scan (gated sampling) or in a loop.
|
|
- The onboard memory can hold 8M samples shared by all channels (depends on the board).
|
|
- The sensitivity setting affects the resolution of the ADC.
|
|
- The actual timing may vary slightly due to hardware limitationslike pre- and post-trigger delays.
|
|
|
|
"""
|
|
attributes='s="%d" f="%d"'%(samples,frequency)#%g
|
|
if channels != 1 and channels != 3 and channels != 5 and channels != 15:
|
|
raise ValueError("Channel definition is illegal")
|
|
attributes += ' channels="%i"'%(channels)
|
|
|
|
nchannels = 0
|
|
if channels == 1:
|
|
nchannels = 1
|
|
elif channels == 3 or channels == 5:
|
|
nchannels = 2
|
|
elif channels == 15:
|
|
nchannels = 4
|
|
if sensitivity is not None:
|
|
# float values are allowed and applied to all channels
|
|
if isinstance(sensitivity, float) or isinstance(sensitivity, int):
|
|
for i in range(nchannels):
|
|
attributes +=' sensitivity%i="%f"'%(i, float(sensitivity))
|
|
else:
|
|
for i in range(nchannels):
|
|
attributes +=' sensitivity%i="%f"'%(i, sensitivity[i])
|
|
if offset is not None:
|
|
# int values are allowed and applied to all channels
|
|
if isinstance(offset, int):
|
|
for i in range(nchannels):
|
|
attributes +=' offset%i="%f"'%(i, offset)
|
|
else:
|
|
for i in range(nchannels):
|
|
attributes +=' offset%i="%f"'%(i, offset[i])
|
|
if impedance is not None:
|
|
# float values are allowed and applied to all channels
|
|
if isinstance(impedance, float):
|
|
for i in range(nchannels):
|
|
attributes += ' impedance%i="%i"'%(i, impedance)
|
|
else:
|
|
for i in range(nchannels):
|
|
attributes += ' impedance%i="%i"'%(i, impedance[i])
|
|
|
|
s_content = '<analogin %s/>' % attributes
|
|
if ttls is not None:
|
|
s_content+='<ttlout value="0x%06x"/>' % ttls
|
|
if timelength is None:
|
|
timelength = samples / float(frequency)#*1.01
|
|
self.state_list.append(StateSimple(timelength, s_content))
|
|
self.total_time[-1] += timelength
|
|
|
|
## Create a loop on the pulse programmer. Loop contents can not change inside the loop.
|
|
# @params iterations Number of loop iterations
|
|
def loop_start(self, iterations: int) -> None:
|
|
"""
|
|
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.
|
|
Close the loop with :func:`loop_end`.
|
|
|
|
:param int iterations: number of iterations to loop
|
|
"""
|
|
l = StateLoop(iterations)
|
|
self.state_list.append(l)
|
|
# (These two lines could probably be guarded by a mutex)
|
|
self.list_stack.append(self.state_list)
|
|
self.state_list = l
|
|
|
|
self.total_time.append(0.0)
|
|
self.loop_iterations.append(iterations)
|
|
|
|
## End loop state
|
|
def loop_end(self) -> None:
|
|
"""
|
|
Closes a l:func:`loop_start` statement.
|
|
"""
|
|
# (This line could probably be guarded by a mutex)
|
|
self.state_list = self.list_stack.pop(-1)
|
|
|
|
looptime=self.total_time.pop(-1)
|
|
loopiterations=self.loop_iterations.pop(-1)
|
|
self.total_time[-1] += looptime*loopiterations
|
|
|
|
|
|
def set_frequency(self, frequency: float, phase: float, ttls: int = 0) -> None:
|
|
"""
|
|
Sets the frequency and phase of the frequency source and optionally further channels.
|
|
The state length is 2 µs. Stabilisation time depends on RF source.
|
|
|
|
:param float frequency: frequency to set in Hz
|
|
:param float phase: phase to set, normalized to the range [0, 360)
|
|
:param int ttls: default=0, additional ttl lines to set (integer)
|
|
"""
|
|
s_content = '<analogout id="0" f="%f" phase="%f"/>' % (frequency, phase % 360)
|
|
if ttls != 0:
|
|
s_content += '<ttlout value="0x%06x"/>' % ttls
|
|
self.state_list.append(StateSimple(2e-6, s_content))
|
|
|
|
self.total_time[-1] += 2e-6
|
|
|
|
def set_pfg(self, dac_value: int | None = None, length: float | None = None, shape: tuple[str, float] = ('rec', 0), trigger: int = 4, is_seq: bool = False) -> None:
|
|
"""
|
|
This sets the value for the PFG, it also sets it back to dac_value=0 automatically.
|
|
If you don't wish to do so (i.e. line shapes) set is_seq=1
|
|
If you want to set a trigger, set trigger (default=4, i.e. channel 2)
|
|
If you want shaped gradients: shape=(ashape, resolution), ashape can be rec, sin2, sin
|
|
|
|
The default DAC ID is hardcoded (id=1)!
|
|
|
|
:param int dac_value: DAC value to set
|
|
:param float length: Duration of the state, minimum length is 42*90ns=3.78us (default)
|
|
:param tuple shape: Tuple of (shape, resolution/seconds), shape can be one of: rec (default), sin2, sin
|
|
:param bool is_seq: If set to *True*, do *NOT* set DAC to zero at the end of this state
|
|
:param int trigger: default=4, lines to set (integer)
|
|
"""
|
|
try:
|
|
form, resolution = shape
|
|
except:
|
|
raise SyntaxError("shape argument needs to be a tuple, i.e. ('shape',resolution), shape can be sin, sin2, rec")
|
|
|
|
if length == None:
|
|
# mimimum length
|
|
length=42*9e-8
|
|
if resolution >= length:
|
|
raise ValueError("Resolution %.3e of shaped gradients can not be longer than total length %.3e"%(resolution, length))
|
|
if resolution < 42*9e-8:
|
|
raise ValueError("Resulution %.3e can not be smaller than %.3e"%(resolution, 42*9e-8))
|
|
|
|
t_steps = numpy.arange(0,length,resolution)
|
|
|
|
if form == 'rec': # shape==None --> rectangular gradients
|
|
s_content = '<ttlout value="%s"/><analogout id="1" dac_value="%i"/>' % (trigger, dac_value)
|
|
self.state_list.append(StateSimple(length, s_content))
|
|
self.total_time[-1] += length
|
|
|
|
if not is_seq:
|
|
s_content = '<analogout id="1" dac_value="0"/>'
|
|
self.state_list.append(StateSimple(42*9e-8, s_content))
|
|
|
|
self.total_time[-1] += 42*9e-8
|
|
|
|
elif form == 'sin2':
|
|
# sin**2 shape
|
|
for t in t_steps:
|
|
dac = int (dac_value*numpy.sin(numpy.pi/length*t)**2)
|
|
s_content = '<ttlout value="%s"/><analogout id="1" dac_value="%i"/>' % (trigger, dac)
|
|
self.state_list.append(StateSimple(resolution, s_content))
|
|
|
|
# set it back to zero
|
|
s_content = '<ttlout value="%s"/><analogout id="1" dac_value="0"/>' % (trigger)
|
|
self.state_list.append(StateSimple(resolution, s_content))
|
|
|
|
self.total_time[-1] += resolution*(len(t_steps)+1)
|
|
|
|
elif form == 'sin':
|
|
# sin shape
|
|
for t in t_steps:
|
|
dac = int (dac_value*numpy.sin(numpy.pi/length*t))
|
|
s_content = '<ttlout value="%s"/><analogout id="1" dac_value="%i"/>' % (trigger, dac)
|
|
self.state_list.append(StateSimple(resolution, s_content))
|
|
# set it back to zero
|
|
s_content = '<ttlout value="%s"/><analogout id="1" dac_value="0"/>' % (trigger)
|
|
self.state_list.append(StateSimple(resolution, s_content))
|
|
|
|
self.total_time[-1] += resolution*(len(t_steps)+1)
|
|
else: # don't know what to do
|
|
raise SyntaxError("form is unknown: %s"%form)
|
|
|
|
def set_dac(self, dac_value: int, dac_id: int = 1, length: float | None = None, is_seq: bool = False, ttls: int = 0) -> None:
|
|
"""
|
|
Sets the value for the DAC (Digital-to-Analog Converter) and optionally additional TTL lines.
|
|
This command is used to control analog output signals, such as pulsed field gradients.
|
|
|
|
Parameters:
|
|
-----------
|
|
dac_value : int
|
|
DAC value, between -2**19-1 (-524287) and +2**19 (524288).
|
|
This value determines the analog output voltage.
|
|
|
|
dac_id : int, optional
|
|
Specifies which DAC to control. Default is 1.
|
|
This allows control of multiple DAC channels if available.
|
|
|
|
length : float, optional
|
|
Length of this state in seconds. Default is None, which sets the length to 42*90ns=3.78µs.
|
|
If is_seq is False, the total length is doubled due to the automatic reset to zero.
|
|
|
|
is_seq : bool, optional
|
|
If True, the DAC value is not reset to zero after the pulse. Default is False.
|
|
Use is_seq=True for creating line shapes or sequential pulses.
|
|
|
|
ttls : int, optional
|
|
Lines to set (integer) for TTL output. Default is 0.
|
|
Allows simultaneous control of TTL lines along with the DAC.
|
|
|
|
Returns:
|
|
--------
|
|
None
|
|
|
|
Examples:
|
|
---------
|
|
>>> e.set_dac(dac_value=15040, dac_id=1, length=1e-3)
|
|
Sets DAC channel 1 to value 15040 for 1 millisecond and then resets to zero.
|
|
|
|
>>> e.set_dac(dac_value=10000, is_seq=True)
|
|
Sets DAC to value 10000 for 3.78µs without resetting to zero.
|
|
|
|
>>> e.set_dac(dac_value=20000, ttls=3)
|
|
Sets DAC to value 20000 and activates TTL channel 1 (2^1 + 2^0 = 3).
|
|
|
|
Notes:
|
|
------
|
|
- The minimum state length is 3.78 µs when is_seq=True.
|
|
- When is_seq=False, the DAC is automatically reset to zero, adding another 3.78 µs to the total length.
|
|
- The actual length may be longer than specified if additional time is required for DAC settling.
|
|
|
|
"""
|
|
if length==None:
|
|
length=42*9e-8
|
|
s_content = '<analogout id="%d" dac_value="%i"/><ttlout value="0x%06x"/>' \
|
|
% (dac_id, dac_value, ttls)
|
|
self.state_list.append(StateSimple(length, s_content))
|
|
|
|
self.total_time[-1] += length
|
|
|
|
if not is_seq:
|
|
s_content = '<analogout id="%d" dac_value="0"/><ttlout value="0x%06x"/>' \
|
|
% (dac_id, ttls)
|
|
self.state_list.append(StateSimple(42*9e-8, s_content))
|
|
|
|
self.total_time[-1] += 42*9e-8
|
|
|
|
def set_phase(self, phase: float, ttls: int = 0) -> None:
|
|
"""
|
|
Sets the phase of the RF source to the specified value.
|
|
|
|
This command changes the phase of the frequency source. The phase is given in degrees and is
|
|
relative to the phase at the beginning of the experiment.
|
|
|
|
Parameters:
|
|
-----------
|
|
phase : float
|
|
Phase to set in degrees. This value determines the phase of the RF signal.
|
|
|
|
ttls : int, optional
|
|
Lines to set (integer) for TTL output. Default is 0.
|
|
Allows simultaneous control of TTL lines along with setting the phase.
|
|
|
|
Returns:
|
|
--------
|
|
None
|
|
|
|
Examples:
|
|
---------
|
|
>>> e.set_phase(phase=90)
|
|
Sets the receiver phase to 90 degrees.
|
|
|
|
>>> e.set_phase(phase=180, ttls=3)
|
|
Sets the phase to 180 degrees and activates TTL channels 0 and 1.
|
|
|
|
>>> e.set_phase(phase=45)
|
|
Sets the phase to 45 degrees.
|
|
|
|
Notes:
|
|
------
|
|
- The state length for this command is 0.5 µs.
|
|
- The stabilization time for the phase change is RF source dependent. A typical value for PTS310 is 2 µs.
|
|
- The phase is relative to the initial phase set at start of the experiment.
|
|
- Negative phases are normalized to the range [0, 360) before writing the job file.
|
|
"""
|
|
s_content = '<analogout phase="%f" />' % (phase % 360)
|
|
if ttls!=0:
|
|
s_content += '<ttlout value="%d"/>' % ttls
|
|
self.state_list.append(StateSimple(0.5e-6, s_content))
|
|
|
|
self.total_time[-1] += 0.5e-6
|
|
|
|
|
|
def set_description(self, key: str, value: object) -> None:
|
|
"""
|
|
Sets a description key-value pair in the experiment description dictionary.
|
|
|
|
This command creates an entry with the specified key and value in the description dictionary.
|
|
In case of data being stored in a HDF5 file, this dictionary is stored as well, allowing
|
|
parameter passing between the experiment script and the result script.
|
|
|
|
Parameters:
|
|
-----------
|
|
key : str
|
|
The key identifier for the description entry. This will be used to retrieve the value later.
|
|
|
|
value : any
|
|
The value to be associated with the key. Can be of any type (string, number, list, etc.).
|
|
|
|
Returns:
|
|
--------
|
|
None
|
|
|
|
Examples:
|
|
---------
|
|
>>> e.set_description(key="tau", value=2e-3)
|
|
Sets the description entry with key "tau" to the value 0.002 seconds.
|
|
|
|
>>> e.set_description(key="temperature", value=298.15)
|
|
Sets the description entry with key "temperature" to the value 298.15 Kelvin.
|
|
|
|
>>> e.set_description(key="pulse_sequence", value="CPMG")
|
|
Sets the description entry with key "pulse_sequence" to the string "CPMG".
|
|
|
|
Notes:
|
|
------
|
|
If the key already exists in the description dictionary, a warning message will be printed
|
|
indicating that the existing value will be overwritten.
|
|
"""
|
|
if key in list(self.description.keys()):
|
|
print('Warning: Overwriting existing description "%s" = "%s" with "%s"' % (key, self.description[key], value))
|
|
self.description[key] = value
|
|
|
|
def set_pts_local(self) -> None:
|
|
"""
|
|
Sets the PTS310/PTS500 frequency source to local mode.
|
|
State length is 2 µs.
|
|
"""
|
|
self.state_list.append(StateSimple(1e-6, '<ttlout value="0xf000"/>'))
|
|
self.state_list.append(StateSimple(1e-6, '<ttlout value="0x8000"/>'))
|
|
|
|
self.total_time[-1] += 2e-6
|
|
|
|
# / Commands -----------------------------------------------------------------------------------
|
|
|
|
|
|
# Public Methods -------------------------------------------------------------------------------
|
|
|
|
def get_job_id(self) -> int:
|
|
"""
|
|
Returns the job-id of the current experiment
|
|
|
|
:returns: job_id
|
|
"""
|
|
return self.job_id
|
|
|
|
|
|
def write_xml_string(self) -> str:
|
|
"""
|
|
Returns the current program as a string
|
|
|
|
:returns: XML string
|
|
"""
|
|
|
|
# Standart XML-Kopf
|
|
xml_string = '<?xml version="1.0" encoding="ISO-8859-1"?>\n'
|
|
|
|
# Experiment-Start-Tag einfügen
|
|
xml_string += '<experiment no="%d">\n' % self.job_id
|
|
|
|
# Descriptions einfügen
|
|
if len(self.description)==0:
|
|
xml_string += ' <description/>\n'
|
|
else:
|
|
xml_string += ' <description>\n'
|
|
for key,value in self.description.items():
|
|
type_string="repr"
|
|
if value is None:
|
|
type_string="None"
|
|
value=""
|
|
elif type(value) is float or isinstance(value, numpy.floating):
|
|
type_string="Float"
|
|
value=repr(float(value))
|
|
elif type(value) is int or isinstance(value, numpy.integer):
|
|
type_string="Int"
|
|
value=repr(int(value))
|
|
elif type(value) is int:
|
|
type_string="Long"
|
|
value=repr(value)
|
|
elif type(value) is complex or isinstance(value, numpy.complexfloating):
|
|
type_string="Complex"
|
|
value=repr(complex(value))
|
|
elif type(value) is bool or isinstance(value, numpy.bool_):
|
|
type_string="Boolean"
|
|
value=repr(bool(value))
|
|
elif type(value) in (str,):
|
|
type_string="String"
|
|
else:
|
|
value=repr(value)
|
|
xml_string += ' <item key="%s" type="%s">%s</item>\n'%(key, type_string ,value)
|
|
xml_string += " </description>\n"
|
|
|
|
# Experiment-Inhalt einfügen
|
|
xml_string += self.state_list.to_xml(indent = " ")
|
|
|
|
# Experiment-End-Tag
|
|
xml_string += '</experiment>\n'
|
|
|
|
return xml_string
|
|
|
|
def write_quit_job(self) -> str:
|
|
"""
|
|
Returns a xml quit-job
|
|
:returns: XML quit-job
|
|
"""
|
|
return '<?xml version="1.0" encoding="ISO-8859-1"?>\n<quit/>'
|
|
|
|
def get_length(self) -> float:
|
|
timelength = 0.0
|
|
#calculate the correct timelength also for unclosed loops, if there is no unclosed loop
|
|
#the timelength of this experiment is self.total_time[-1].
|
|
for i in range(len(self.total_time)):
|
|
timelength += self.total_time[i]
|
|
timelength *= self.loop_iterations[i]
|
|
return timelength
|
|
|
|
class Quit(Experiment):
|
|
def write_xml_string(self) -> str:
|
|
return '<?xml version="1.0" encoding="ISO-8859-1"?>\n<quit no="%d"/>'%self.job_id
|
|
|
|
|
|
# /Public Methods ------------------------------------------------------------------------------
|
|
|
|
def self_test() -> None:
|
|
e = Experiment()
|
|
e.set_description("key", "value")
|
|
e.set_frequency(85e6, 90, ttls=16)
|
|
e.wait(1e-6)
|
|
#e.rf_pulse(1, 1e-6/3) # val = 1
|
|
e.ttl_pulse(1e-6/3, 1) # val = 2
|
|
e.ttl_pulse(1e-6/3, None, 7) # val = 7
|
|
if True:
|
|
e.loop_start(30)
|
|
e.set_pfg(dac_value=1024, length=5e-6, is_seq = True, shape=('rec', 42*9e-8))
|
|
e.loop_start(400)
|
|
e.set_phase(270, ttls = 32)
|
|
e.loop_end()
|
|
e.ttl_pulse(5e-6, channel = 6)
|
|
e.loop_end()
|
|
else:
|
|
l = StateLoop(3)
|
|
l.append(StateSimple(5e-6, '<ttlout value="1"/>'))
|
|
e.state_list.append(l)
|
|
e.set_dac(12345, dac_id=2, is_seq = True, ttls=16)
|
|
e.record(1024, 20e6)
|
|
try:
|
|
e.wait(-1)
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise AssertionError("An exception should happen")
|
|
e.set_pts_local()
|
|
print(e.write_xml_string())
|
|
|
|
print(e.get_length())
|
|
|
|
if __name__ == '__main__':
|
|
self_test()
|