Download
Download this file as Jupyter notebook: configuration_example.ipynb.
Example: Configuring Native Gates
When implementing a circuit on a physical or simulated device, it is often useful to compile operations and circuits into cycles that can be implemented with a specified choice of primitive gates. In physical devices, these primitive gates are generally constrained to a set of gates that are natively available on that hardware.
To facilitate integration with hardware, True-Q™ defines a configuration standard
through the Config
class that specifies:
Which gates can be implemented natively
Which operations can be performed in parallel
Which sequence of gates should be used to implement a synthesized unitary
Let’s take a look at how it works.
Two Ways of Defining Gates: Matrix and Hamiltonian
Users can specify native gates by providing either a unitary matrix or a Hamiltonian (which is inferred by a list of Pauli strings with corresponding rotation angles).
When gates cannot be implemented in parallel with operations on specific other qubits, the unparallelizable gates can be specified and will be taken into account when circuits are transpiled for the system.
In this case, two CNOTs are defined, one between (0, 2)
and another between (1,
2)
, but both of these gates also restrict the device, in that operations cannot be
performed on other qubits (highlighted in red) in parallel with these gates. This is
common in practice; some operations, such as the cross resonance gate,
frequently have to be performed independently from other operations. True-Q™ also
allows users to define gates with variable parameters — the parameters can be
defined either within the unitary matrix or Hamiltonian construction. For example:
Defining a Config
Object
Config
objects are used to inform True-Q™’s compiler tools about
the configuration of a given device. A Config
object can be
constructed inside a Python session. The easiest way to construct standard
configurations is with the basic()
constructor:
[2]:
import numpy as np
import trueq as tq
from trueq.config import GateFactory
basic_config = tq.Config.basic(
entangler=tq.Gate.cz, connectivity=[(0, 1), (1, 2), (2, 0)]
)
print(basic_config)
Mode: ZXZXZ
Dimension: 2
Gates:
- z(phi):
Hamiltonian:
- [Z, phi]
- sx:
Hamiltonian:
- [X, 90.0]
- cz:
Matrix:
- [1.0, 0.0, 0.0, 0.0]
- [0.0, 1.0, 0.0, 0.0]
- [0.0, 0.0, 1.0, 0.0]
- [0.0, 0.0, 0.0, -1.0]
Involving: {'(0, 1)': (), '(1, 2)': (), '(2, 0)': ()}
As you can see in the above example of a Config
object, there are
three main components to a device configuration:
The dimension of the device’s registers.
In the case of the
basic()
constructor, the default is Dimension 2, which corresponds to qubits.The
mode
property, which specifies how single-qudit unitaries are implemented.In the case of the
basic()
constructor, the default mode isZXZXZ
, which means that single-qudit unitaries would be implemented on a device by a series of gates of the form \(Z(\theta_1)X(90)Z(\theta_2)X(90)Z(\theta_3)\).The available gates.
In the case of the
basic()
constructor, the single-qubit gate generators \(X(90)`\) and \(Z(\phi)\) are included by default, and the minimal specification is an entangling gate. In the example above, we chose \(CZ\) as the entangling gate and imposed connectivity constraints. By default, thebasic()
constructor assumes full connectivity, i.e. that the entangling gate can act on any pair of qubits. In the example above, however, we restrict the action of the \(CZ\) gate to the qubits labeled(0, 1)
,(1, 2)
, or(2, 0)
.
If entanglers across a device are defined by a fixed parameterized multi-qubit gate
but with nonhomogenous parameters, we can create an appropriate configuration by using
from_parameterized()
. First, we need to define the
parameterized gates by defining GateFactory
objects. For
example:
[3]:
# define iSwap-like gates through a parameterized matrix form
iSwapLike_mat = [
[1, 0, 0, 0],
[0, 0, -1j, 0],
[0, -1j, 0, 0],
[0, 0, 0, "exp(-1j * phi * pi / 180)"],
]
iSwapLike_factory = GateFactory.from_matrix(name="iSwapLike", matrix=iSwapLike_mat)
From those GateFactory
objects, we can create a
configuration by using from_parameterized()
:
[4]:
parametrized_config = tq.Config.from_parameterized(
iSwapLike_factory, parameters={(0, 1): 90, (1, 2): 45}
)
print(parametrized_config)
Mode: ZXZ
Dimension: 2
Gates:
- X(phi):
Hamiltonian:
- [X, phi]
- Z(phi):
Hamiltonian:
- [Z, phi]
- iSwapLike_0_1:
Matrix:
- [(0.923879532511+0.382683432365j), 0j, 0j, 0j]
- [0j, 0j, (0.382683432365-0.923879532511j), 0j]
- [0j, (0.382683432365-0.923879532511j), 0j, 0j]
- [0j, 0j, 0j, (0.382683432365-0.923879532511j)]
Involving: {'(0, 1)': ()}
- iSwapLike_1_2:
Matrix:
- [(0.980785280403+0.195090322016j), 0j, 0j, 0j]
- [0j, 0j, (0.195090322016-0.980785280403j), 0j]
- [0j, (0.195090322016-0.980785280403j), 0j, 0j]
- [0j, 0j, 0j, (0.831469612303-0.55557023302j)]
Involving: {'(1, 2)': ()}
Notice that from_parameterized()
has the default Dimension:
2
and Mode: ZXZ
, which expresses single-qubit-gates in the form
\(Z(\theta_1)X(\theta_2) Z(\theta_3)\). The above example allows \(X(\phi)\)
and \(Z(\phi)\) gates with any values of \(\phi\). These are the default
single-qubit gates included when a Config
object is constructed
using from_parameterized()
. The configuration in this example
also allows gates of the form defined by iSwapLike_mat
with \(\phi = 90\) on
the qubit pair (0, 1)
and with \(\phi = 45\) on the qubit pair (1, 2)
.
Finally, if we want to customize the native gates directly from a list of operations,
we may create a list (or a tuple) of GateFactory
s, and pass
it to Config()
:
[5]:
# define a single-qubit gates' parameterization through a Hamiltonian form
u3_factory = GateFactory.from_hamiltonian(
"U3Gate",
[["Z", "phi "], ["Y", "theta"], ["Z", "lam"]],
parameters=["theta", "phi", "lam"],
)
# define a XX(90) entangling gate on the pair (0,1)
xx_factory = GateFactory.from_hamiltonian(
"XXGate", [["XX", 90]], involving={(0, 1): ()}
)
# construct a configuration from the gate factories
advanced_config = tq.Config([xx_factory, u3_factory])
print(advanced_config)
Mode: ZXZXZ
Dimension: 2
Gates:
- XXGate:
Hamiltonian:
- [XX, 90.0]
Involving: {'(0, 1)': ()}
- U3Gate(theta, phi, lam):
Hamiltonian:
- [Z, phi]
- [Y, theta]
- [Z, lam]
Saving and Loading Configurations
If you are working regularly with a specific device, it may be useful to store the
configuration for that device as a .yaml file so that you can re-use it without
having to write out the full device specifications every time. A True-Q™
Config
object can be instantiated from a saved file using the
trueq.Config.from_yaml()
method. Saving the configuration is as simple as
pasting its output into a .yaml file.
For example, consider the following configuration file:
Dimension: 2
Mode: ZXZXZ
Gates:
- X:
Hamiltonian:
- ["X", phi]
Involving:
(0,): ()
(1,): (4, 5, 6)
(2,): (4, 5, 6)
(5,): (7,)
- Z:
Matrix:
- [1, 0]
- [0, exp(1j*phi/180*pi)]
Involving:
(0,): ()
(1,): (4, 5, 6)
(2,): (4, 5, 6)
(5,): (7,)
- MS:
Hamiltonian:
- ["XX", phi]
Involving:
(0, 1): (3)
(1, 2): (0)
(1, 4): (0, 3)
Once loaded into our Python session, we can construct any gate from this file:
[6]:
config = tq.Config.from_yaml("config.yaml")
# Construct the MS gate from this config file:
config.MS(phi=np.pi / 2)
[6]:
- Name:
-
- MS(phi)
- Likeness:
-
- Non-Clifford
- Parameters:
-
- phi = 1.570796
- Generators:
-
- 'XX': 1.571
- Matrix:
-
Compiling According to a Configuration
Now let’s take a look at how to re-compile a circuit according to a configuration using True-Q™'s compiler using configuration defined at the beginning of this tutorial.
Note
For a more in-depth introduction to True-Q™'s compilation tools, check out our Compilation Basics example.
We begin by instantiating a Compiler
object using our
basic_config
object from above:
[7]:
transpiler = tq.Compiler.from_config(basic_config)
Now, let’s define some simple circuits to translate into the language of our native gates.
[8]:
y_circuit = tq.Circuit({0: tq.Gate.y})
y_circuit.draw()
[8]:
Next, we apply the Compiler
, transpiler
, to the
circuit, y_circuit
and draw the compiled circuit:
[9]:
# transpile y_circuit, then draw the transpiled result
transpiled_y_circuit = transpiler.compile(y_circuit)
transpiled_y_circuit.draw()
[9]:
You can hover over the gates to see that here that \(Y\) is compiled as
\(Z(90)X(90)Z(0)X(90)Z(-90)\). This stems from the Mode: ZXZXZ
option
specified in the config file.
[10]:
# define another example circuit
cx_circuit = tq.Circuit({(0, 1): tq.Gate.cx})
cx_circuit.draw()
# transpile cx_circuit, then draw the transpiled result
transpiled_cx_circuit = transpiler.compile(cx_circuit)
transpiled_cx_circuit.draw()
[10]:
Notice that in the compiled version of the cx_circuit
, the only entangling gates
used are those included in the basic_config
. Hover over the circuits to see that
each pulse between the two-qubit gates is compiled into rounds which follow the ZXZXZ
decomposition, as specified in the configuration file.
Download
Download this file as Jupyter notebook: configuration_example.ipynb.