2.14. Storing Pulse Templates: PulseStorage
and Serialization¶
So far, we have constructed new pulse templates in code for each session (which were discarded afterwards). We now want to store them persistently in the file system to be able to reuse them in later sessions. For this, qupulse offers us serialization and deserialization using the PulseStorage
and StorageBackend
classes.
The pulse storage manages the (de-)serialization to JSON and requires a storage backend to persistently store the serialized data. This can for example be a FilesystemBackend
or a DictBackend
. Let us first use a DictBackend
to inspect the serialized pulse.
Attention: Due to the fact that PulseStorage enforces unique identifiers, executing the cells in this notebook out of order or rerunning them will likely result in errors. You will have to restart the Kernel in that case.
2.14.1. Single Pulses¶
First we will have a look at how to store pulses that do not contain other pulse templates. To store a pulse, the pulse needs to have an identifier. If you forgot to give the pulse an identifier one can use the rename
method which returns a new pulse with the requested identifier.
2.14.1.1. Storing¶
[1]:
import pprint
from qupulse.pulses import TablePT
from qupulse.serialization import PulseStorage, DictBackend
dict_backend = DictBackend()
dict_pulse_storage = PulseStorage(dict_backend)
table_pulse = TablePT({'default': [('t_begin', 'v_begin', 'hold'),
('t_end', 'v_end', 'linear')]}, identifier='my_pulse')
dict_pulse_storage['my_pulse'] = table_pulse
pprint.pprint(dict_backend.storage)
{'my_pulse': '{\n'
' "#identifier": "my_pulse",\n'
' "#type": '
'"qupulse.pulses.table_pulse_template.TablePulseTemplate",\n'
' "entries": {\n'
' "default": [\n'
' [\n'
' "t_begin",\n'
' "v_begin",\n'
' "hold"\n'
' ],\n'
' [\n'
' "t_end",\n'
' "v_end",\n'
' "linear"\n'
' ]\n'
' ]\n'
' },\n'
' "measurements": [],\n'
' "parameter_constraints": []\n'
'}'}
Now to store this in a file system we need to replace the DictBackend
with a FilesystemBackend
. The following code will create the file './serialized_pulses/my_pulse.json'
.
[2]:
from qupulse.serialization import FilesystemBackend
filesystem_backend = FilesystemBackend('./serialized_pulses')
file_pulse_storage = PulseStorage(filesystem_backend)
if 'my_pulse' in file_pulse_storage:
del file_pulse_storage['my_pulse']
file_pulse_storage['my_pulse'] = table_pulse
2.14.1.2. Loading¶
Now we will load a pulse that is shipped only as a JSON file. It is a single sine with frequency omega
. Note that loading the same pulse multiple times will give you the same object.
[3]:
%matplotlib inline
import math
from qupulse.pulses.plotting import plot as plot
sine = file_pulse_storage['my_other_pulse']
_ = plot(sine, {'omega': 2*math.pi}, sample_rate=1000, show=False)
if sine is file_pulse_storage['my_other_pulse']:
print('Loading the same pulse multiple times gives you the same object')
Loading the same pulse multiple times gives you the same object
2.14.2. Composed pulses and the role of identifiers¶
If we have a pulse that contains other pulses all pulses that have an identifier are stored seperatly. Each PulseStorage
instance expects that identifiers are unique (see below). Anonymous subpulses are stored together with their parent.
We will now only use a dictionary as a backend it is easier to see what happens.
2.14.2.1. Storing¶
[4]:
from qupulse.pulses import RepetitionPT, SequencePT
# anonymous pulse template
repeated_sine = RepetitionPT(sine, 'N_sine')
my_sequence = SequencePT(repeated_sine, table_pulse, identifier='combined')
dict_pulse_storage['combined'] = my_sequence
pprint.pprint(dict_backend.storage)
{'combined': '{\n'
' "#identifier": "combined",\n'
' "#type": '
'"qupulse.pulses.sequence_pulse_template.SequencePulseTemplate",\n'
' "subtemplates": [\n'
' {\n'
' "#type": '
'"qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate",\n'
' "body": {\n'
' "#identifier": "my_other_pulse",\n'
' "#type": "reference"\n'
' },\n'
' "repetition_count": "N_sine"\n'
' },\n'
' {\n'
' "#identifier": "my_pulse",\n'
' "#type": "reference"\n'
' }\n'
' ]\n'
'}',
'my_other_pulse': '{\n'
' "#identifier": "my_other_pulse",\n'
' "#type": '
'"qupulse.pulses.function_pulse_template.FunctionPulseTemplate",\n'
' "channel": "default",\n'
' "duration_expression": "2*pi/omega",\n'
' "expression": "sin(omega*t)",\n'
' "measurements": [],\n'
' "parameter_constraints": []\n'
'}',
'my_pulse': '{\n'
' "#identifier": "my_pulse",\n'
' "#type": '
'"qupulse.pulses.table_pulse_template.TablePulseTemplate",\n'
' "entries": {\n'
' "default": [\n'
' [\n'
' "t_begin",\n'
' "v_begin",\n'
' "hold"\n'
' ],\n'
' [\n'
' "t_end",\n'
' "v_end",\n'
' "linear"\n'
' ]\n'
' ]\n'
' },\n'
' "measurements": [],\n'
' "parameter_constraints": []\n'
'}'}
As you see, the serialization of ‘combined’ explicitly contains the anonymous RepetitionPulseTemplate
but references to ‘my_pulse’ and ‘my_other_pulse’ which are stored as separate entries.
2.14.3. Pulse registry and unique identifiers¶
There is the possibility to store pulse templates on construction into a pulse registry. This can be a PulseStorage
. To set a pulse storage as the default pulse registry call set_to_default_registry
[5]:
from qupulse.pulses import FunctionPT
dict_pulse_storage.set_to_default_registry()
new_pulse = FunctionPT(0, 1, identifier='new_pulse')
pprint.pprint(dict_backend.storage)
{'combined': '{\n'
' "#identifier": "combined",\n'
' "#type": '
'"qupulse.pulses.sequence_pulse_template.SequencePulseTemplate",\n'
' "subtemplates": [\n'
' {\n'
' "#type": '
'"qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate",\n'
' "body": {\n'
' "#identifier": "my_other_pulse",\n'
' "#type": "reference"\n'
' },\n'
' "repetition_count": "N_sine"\n'
' },\n'
' {\n'
' "#identifier": "my_pulse",\n'
' "#type": "reference"\n'
' }\n'
' ]\n'
'}',
'my_other_pulse': '{\n'
' "#identifier": "my_other_pulse",\n'
' "#type": '
'"qupulse.pulses.function_pulse_template.FunctionPulseTemplate",\n'
' "channel": "default",\n'
' "duration_expression": "2*pi/omega",\n'
' "expression": "sin(omega*t)",\n'
' "measurements": [],\n'
' "parameter_constraints": []\n'
'}',
'my_pulse': '{\n'
' "#identifier": "my_pulse",\n'
' "#type": '
'"qupulse.pulses.table_pulse_template.TablePulseTemplate",\n'
' "entries": {\n'
' "default": [\n'
' [\n'
' "t_begin",\n'
' "v_begin",\n'
' "hold"\n'
' ],\n'
' [\n'
' "t_end",\n'
' "v_end",\n'
' "linear"\n'
' ]\n'
' ]\n'
' },\n'
' "measurements": [],\n'
' "parameter_constraints": []\n'
'}',
'new_pulse': '{\n'
' "#identifier": "new_pulse",\n'
' "#type": '
'"qupulse.pulses.function_pulse_template.FunctionPulseTemplate",\n'
' "channel": "default",\n'
' "duration_expression": 1,\n'
' "expression": 0,\n'
' "measurements": [],\n'
' "parameter_constraints": []\n'
'}'}
As you see each newly created pulse is put into the pulse storage. Creating a new pulse with the same name will fail:
[6]:
try:
new_pulse = FunctionPT(0, 1, identifier='new_pulse')
except RuntimeError as err:
print('Oh No!!!')
print(err)
print()
Oh No!!!
('Pulse with name already exists', 'new_pulse')
We have to either explicitly overwrite the registry or delete the old pulse from it:
[7]:
try:
new_pulse = FunctionPT(0, 1, identifier='new_pulse', registry=dict())
except:
raise
else:
print('Overwriting the registry works!')
del dict_pulse_storage['new_pulse']
try:
new_pulse = FunctionPT(0, 1, identifier='new_pulse')
except:
raise
else:
print('Deleting the pulse works, too!')
Overwriting the registry works!
Deleting the pulse works, too!