# 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:

1. The dimension of the device’s registers.

In the case of the basic() constructor, the default is Dimension 2, which corresponds to qubits.

2. The mode property, which specifies how single-qudit unitaries are implemented.

In the case of the basic() constructor, the default mode is ZXZXZ, 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)$.

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, the basic() 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:
- Z(phi):
Hamiltonian:
- [Z, phi]
- X(phi):
Hamiltonian:
- [X, 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 GateFactorys, 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

Mode: ZXZXZ
Dimension: 2
Gates:
- XXGate:
Hamiltonian:
- [XX, 90.0]
Involving: {'(0, 1)': ()}
- U3Gate(theta, phi, lam):
Hamiltonian:
- [Z, phi]
- [Y, theta]
- [Z, lam]



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:

config.yaml
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]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
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.