<jupyter_start><jupyter_text>Tutorial: QPU Database**This tutorial requires version >=0.0.5 of the QPU DB** Using the QPU DBThe QPU database is a permanent store built for storing calibration data for Quantum Processing Units (QPU).It provides the following features and benefits:* Persistent storage of any python object related to QPU calibration info* Metadata on parameter calibration state and last modified time* Convenient addressing of quantum elements* Easy revert to previously stored parametersIn this short tutorial we will learn how to use the QPU DB by looking at a simplified example of a QPU with two superconductingqubits, two readout resonators and a parametric coupling element. Creating the databaseBelow we can see a simple usage example. The DB is created by calling the `create_new_database` method.This method is similar to initializing a git repo in the sense that we only do it once. Here we initialize itwith an initial dictionary which contains some basic attributes of our QPU. We'll be able to add more attributes,and also elements, later on. Once we call `create_new_qpu_database`, a set of database files will be created for us atthe working directory of the python script.These files are the persistent storage of our DB. They can be saved to a different location by specifyingthe `path` argument to the function.<jupyter_code># %load_ext autoreload
# %autoreload 2
from entropylab_qpudb import create_new_qpu_database, CalState, QpuDatabaseConnection
initial_dict = {
    'q1': {
        'f01': 5.65e9  # an initial guess for our transition frequency
    },
    'q2': {
        'f01': 5.25e9
    },
    'res1': {
        'f_r': 7.1e9
    },
    'res2': {
        'f_r': 7.3e9
    },
    'c1_2': {
        'f_r': 0.4e9
    }
}
create_new_qpu_database('db1', initial_dict, force_create=True)<jupyter_output><empty_output><jupyter_text>Notes:1. here we allow for the possibility of overwriting an existing databaseby passing the `force_create=True` flag. This option is useful when experimenting with the database creation, however incommon usage it is recommended to remove this flag, since when it's false (by default), it will prevent overwriting an existingdatabase and deleting all the data stored in it.2. (For experts): if you need to create a DB server, rather than create a filesystem storage, please let us know.The DB backend is currentlythe [ZODB](https://zodb.org/en/latest/) database, with plans to be replaced by[gitdb](https://github.com/gitpython-developers/gitdb).The keys of `initial_dict` are called the *elements* (and are similar in nature to QUA's quantum elements), and thevalues of these elements are subdictionaries of *attributes*. The values of the attributes can be anything you like,or more accurately, any python object that can be pickled. The different elements need not have the same attributes. Connecting to the database and basic usageNow create a connection to our DB. The connection to the DB is our main "workhorse" - we create the DB once, andwhenever we want to connect to it in order to retrieve or store data, we open a connection object. Note that currentlyonly a single connection object per DB is allowed.<jupyter_code>db1 = QpuDatabaseConnection('db1')<jupyter_output>opening qpu database db1 from commit <timestamp: 05/30/2021 06:24:19, message: initial commit> at index 0<jupyter_text>and let's view the contents of our DB by calling `print`:<jupyter_code>db1.print()<jupyter_output>q1
----
f01:	QpuParameter(value=5400000000.0, last updated: 05/30/2021 09:24:45, calibration state: COARSE)

q2
----
f01:	QpuParameter(value=5250000000.0, last updated: 05/30/2021 09:24:19, calibration state: UNCAL)

res1
----
f_r:	QpuParameter(value=7100000000.0, last updated: 05/30/2021 09:24:19, calibration state: UNCAL)

res2
----
f_r:	QpuParameter(value=7300000000.0, last updated: 05/30/2021 09:24:19, calibration state: UNCAL)

c1_2
----
f_r:	QpuParameter(value=400000000.0, last updated: 05/30/2021 09:24:19, calibration state: UNCAL)<jupyter_text>Congratulations! You've just created your first QPU DB. As you can see when calling `print` the values we enteredin `initial_dict` are now objects of type `QpuParameter`. These objects have 3 attributes:* `value`: the value you created initially and can be any python object* `last_updated`: the time when this parameter was last updated (see *committing* section to understand how toupdate). This parameter is handled by the DB itself.* `cal_state`: an enumerated metadata that can take the values `UNCAL`, `COARSE`, `MED` and `FINE`. Thiscan be used by the user to communicate what is the calibration level of these parameters. They can be set and queriedduring the script execution, but are not used by the DB itself. Modifying and using QPU parametersWe can use and modify values and calibration states of QPU parameters in two different ways: Using `get` and `set`let's modify the value of `f01` and then get the actual value:<jupyter_code>db1.set('q1', 'f01', 5.33e9)
db1.get('q1', 'f01').value<jupyter_output><empty_output><jupyter_text>We can also modify the calibration state when setting:<jupyter_code>db1.set('q1', 'f01', 5.36e9, CalState.COARSE)<jupyter_output><empty_output><jupyter_text>To get the full `QpuParameter` object we can omit `.value`. We can see that the cal state and modification date were updated.<jupyter_code>db1.get('q1', 'f01')
#db1.get('q1', 'f01').cal_state<jupyter_output><empty_output><jupyter_text>Note that we can't modify the value by assigning to value directly - this will raise an exception.  Using resolved namesThe names we chose for the elements, namely `'q1'`, `'res1'` and `'c1_2'` have a special significance. If we follow thisconvention of naming qubit elements with the format 'q'+number, resonators with the format 'res'+numberand couplers with the format 'c'+number1+'_'+number2, as shown above, this allows us to get and set values in a moreconvenient way:<jupyter_code>print(db1.q(1).f01.value)
print(db1.res(1).f_r.value)
print(db1.coupler(1, 2).f_r.value)
print(db1.coupler(2, 1).f_r.value)<jupyter_output>5360000000.0
7100000000.0
400000000.0
400000000.0<jupyter_text>while this method basically syntactic sugar, it allows us to conveniently address elements by indices, which is useful whenworking with multiple qubit systems, and especially with couplers. We can also set values using this resolved addressing method:<jupyter_code>db1.update_q(1, 'f01', 5.4e9)
db1.q(1).f01<jupyter_output><empty_output><jupyter_text>Note: This default mapping between integer indices and strings can be modified by subclassing the`Resolver` class found under `entropylab_qpudb._resolver.py`. Committing (saving to persistent storage) and viewing historyEverything we've done so far did not modify the persistent storage. In order to do this, we need to *commit* the changes we made.This allows us to control at which stages we want to make aggregated changes to the database.Let's see how this is done. We need to call `commit`, and specify an optional commit message:<jupyter_code>db1.update_q(1, 'f01', 6.e9)
db1.commit('a test commit')<jupyter_output>commiting qpu database db1 with commit <timestamp: 05/30/2021 06:26:20, message: a test commit> at index 1<jupyter_text>Now the actual file was changed. To see this, we need to close the db. We can then delete db1,and when re-opening the DB we'll see f01 of q1 has the modified value.<jupyter_code>db1.close()
del db1
db1 = QpuDatabaseConnection('db1')
db1.q(1).f01<jupyter_output>closing qpu database db1
closing qpu database db1
opening qpu database db1 from commit <timestamp: 05/27/2021 06:44:34, message: a test commit> at index 1<jupyter_text>Note that the commit was saved with an index. This index can be later used to revert to a [previous state](reverting-to-a-previous-state).To view a history of all the commits, we call `get_history`.Note that the timestamps of the commits are in UTC time.<jupyter_code>db1.get_history()<jupyter_output><empty_output><jupyter_text>Adding attributes and elementsIn many cases you realize while calibrating your system that you want to add attributes that did not exist in the initialdictionary, or even new elements. This is easy using the `add_element` and `add_attribute` methods.Let's see an example for `add_attribute`:<jupyter_code>db1.add_attribute('q1', 'anharmonicity')

print(db1.q(1).anharmonicity)

db1.update_q(1, 'anharmonicity', -300e6, new_cal_state=CalState.COARSE)

print(db1.q(1).anharmonicity)<jupyter_output>QpuParameter(None)
QpuParameter(value=-300000000.0, last updated: 05/30/2021 09:26:25, calibration state: COARSE)<jupyter_text>Reverting to a previous stateMany times when we work on bringing up a QPU, we reach a point where everything is calibrated properly and our measurementsand calibrations give good results. We want to be able to make additional changes, but to possibly revert to the good stateif things go wrong. We can do this using `restore_from_history`. We simply need to provide it with the historyindex to which we want to return:<jupyter_code>db1.restore_from_history(0)
print(db1.q(1).f01)
assert db1.q(1).f01.value == initial_dict['q1']['f01']<jupyter_output>opening qpu database db1 from commit <timestamp: 05/30/2021 06:24:19, message: initial commit> at index 0
QpuParameter(value=5650000000.0, last updated: 05/30/2021 09:24:19, calibration state: UNCAL)<jupyter_text>Calling this method will replace the current working DB with the DB that was stored in the commit with the indexsupplied to `restore_from_history`. The new values will not be committed. It is possible to modify the values andcommit them as usual. Next stepsWhile the QPU DB is a standalone tool, it is designed with QUA calibration node framework in mind.In the notebook called `2_qubit_graph_calibration.ipynb` we explore how the QUA calibration nodes framework can be usedto generate calibration graphs. Remove DB filesTo remove the DB files created in your workspace for the purpose of this demonstration, first close the db connection:<jupyter_code>db1.close()<jupyter_output>closing qpu database db1<jupyter_text>then run this cell:<jupyter_code>from glob import glob
import os
for fl in glob("db1*"):
    os.remove(fl)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07853096307872631, 'fraction_numerical': 0.04268411799179527, 'mean_word_length': 5.419435736677116, 'pattern_counts': {'":': 0, '<': 57, '<?xml version=': 0, '>': 58, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 11, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '491078', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 3317, 'n_tokens_neox': 2991, 'n_words': 1287, 'path': 'docs/1_qpu_db.ipynb', 'repo_name': 'qguyk/entropy-qpu', 'size': '10238'}