Skip to content
Pasqal Documentation

Block system

qadence offers a block-based system to construct quantum circuits in a flexible manner.

AbstractBlock(tag=None, __array_priority__=1000) dataclass

Section titled “ AbstractBlock(tag=None, __array_priority__=1000) dataclass ”

Bases: ABC

Base class for both primitive and composite blocks.

ATTRIBUTE DESCRIPTION
name

A human-readable name attached to the block type. Notice, this is the same for all the class instances so it cannot be used for identifying different blocks

TYPE: str

qubit_support

The qubit support of the block expressed as a tuple of integers

TYPE: tuple[int, ...]

tag

A tag identifying a particular instance of the block which can be used for identification and pretty printing

TYPE: str | None

eigenvalues

The eigenvalues of the matrix representing the block. This is used mainly for primitive blocks and it's needed for generalized parameter shift rule computations. Currently unused.

TYPE: list[float] | None

Identity predicate for blocks.

The number of qubits in the whole system.

A block acting on qubit N would has at least n_qubits >= N + 1.

Source code in qadence/blocks/abstract.py
47
48
49
50
51
52
53@abstractproperty
def n_qubits(self) -> int:
"""The number of qubits in the whole system.
A block acting on qubit N would has at least n_qubits >= N + 1.
"""
pass

The number of qubits the block is acting on.

Source code in qadence/blocks/abstract.py
55
56
57
58@abstractproperty
def n_supports(self) -> int:
"""The number of qubits the block is acting on."""
pass

The indices of the qubit(s) the block is acting on.

Qadence uses the ordering [0..,N-1] for qubits.

Source code in qadence/blocks/abstract.py
39
40
41
42
43
44
45@abstractproperty
def qubit_support(self) -> Tuple[int, ...]:
"""The indices of the qubit(s) the block is acting on.
Qadence uses the ordering [0..,N-1] for qubits.
"""
pass

ControlBlock(control, target_block, noise=None)

Section titled “ ControlBlock(control, target_block, noise=None) ”

Bases: PrimitiveBlock

The abstract ControlBlock.

Source code in qadence/blocks/primitive.py
372
373
374
375
376
377
378
379
380
381
382
383
384def __init__(
self,
control: tuple[int, ...],
target_block: PrimitiveBlock,
noise: NoiseHandler | None = None,
) -> None:
self.control = control
self.blocks = (target_block,)
self.target = target_block.qubit_support
# using tuple expansion because some control operations could
# have multiple targets, e.g. CSWAP
super().__init__((*control, *self.target), noise=noise) # target_block.qubit_support[0]))

ParametricBlock(qubit_support, noise=None) dataclass

Section titled “ ParametricBlock(qubit_support, noise=None) dataclass ”

Bases: PrimitiveBlock

Parameterized primitive blocks.

Source code in qadence/blocks/primitive.py
37
38
39
40
41
42
43def __init__(
self,
qubit_support: tuple[int, ...],
noise: NoiseHandler | None = None,
):
self._qubit_support = qubit_support
self._noise = noise

The number of parameters required by the block.

This is a class property since the number of parameters is defined automatically before instantiating the operation. Also, this could correspond to a larger number of actual user-facing parameters since any parameter expression is allowed

Examples: - RX operation has 1 parameter - U operation has 3 parameters - HamEvo has 2 parameters (generator and time evolution)

Source code in qadence/blocks/primitive.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171@abstractmethod
def num_parameters(cls) -> int:
"""The number of parameters required by the block.
This is a class property since the number of parameters is defined
automatically before instantiating the operation. Also, this could
correspond to a larger number of actual user-facing parameters
since any parameter expression is allowed
Examples:
- RX operation has 1 parameter
- U operation has 3 parameters
- HamEvo has 2 parameters (generator and time evolution)
"""
pass

ParametricControlBlock(control, target_block, noise=None)

Section titled “ ParametricControlBlock(control, target_block, noise=None) ”

Bases: ParametricBlock

The abstract parametrized ControlBlock.

Source code in qadence/blocks/primitive.py
437
438
439
440
441
442
443
444
445
446def __init__(
self,
control: tuple[int, ...],
target_block: ParametricBlock,
noise: NoiseHandler | None = None,
) -> None:
self.blocks = (target_block,)
self.control = control
self.parameters = target_block.parameters
super().__init__((*control, *target_block.qubit_support), noise=noise)

PrimitiveBlock(qubit_support, noise=None)

Section titled “ PrimitiveBlock(qubit_support, noise=None) ”

Bases: AbstractBlock

Primitive blocks represent elementary unitary operations.

Examples are single/multi-qubit gates or Hamiltonian evolution. See qadence.operations for a full list of primitive blocks.

Source code in qadence/blocks/primitive.py
37
38
39
40
41
42
43def __init__(
self,
qubit_support: tuple[int, ...],
noise: NoiseHandler | None = None,
):
self._qubit_support = qubit_support
self._noise = noise

Decomposition into purely digital gates.

This method returns a decomposition of the Block in a combination of purely digital single-qubit and two-qubit 'gates', by manual/custom knowledge of how this can be done efficiently. :return:

Source code in qadence/blocks/primitive.py
53
54
55
56
57
58
59
60
61def digital_decomposition(self) -> AbstractBlock:
"""Decomposition into purely digital gates.
This method returns a decomposition of the Block in a
combination of purely digital single-qubit and two-qubit
'gates', by manual/custom knowledge of how this can be done efficiently.
:return:
"""
return self

ProjectorBlock(ket, bra, qubit_support, noise=None)

Section titled “ ProjectorBlock(ket, bra, qubit_support, noise=None) ”

Bases: PrimitiveBlock

The abstract ProjectorBlock.

Arguments:

ket (str): The ket given as a bitstring.
bra (str): The bra given as a bitstring.
qubit_support (int | tuple[int]): The qubit_support of the block.
Source code in qadence/blocks/primitive.py
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550def __init__(
self,
ket: str,
bra: str,
qubit_support: int | tuple[int, ...],
noise: NoiseHandler | None = None,
) -> None:
"""
Arguments:
ket (str): The ket given as a bitstring.
bra (str): The bra given as a bitstring.
qubit_support (int | tuple[int]): The qubit_support of the block.
"""
if isinstance(qubit_support, int):
qubit_support = (qubit_support,)
if len(bra) != len(ket):
raise ValueError(
"Bra and ket must be bitstrings of same length in the 'Projector' definition."
)
elif len(bra) != len(qubit_support):
raise ValueError("Bra or ket must be of same length as the 'qubit_support'")
for wf in [bra, ket]:
if not all(int(item) == 0 or int(item) == 1 for item in wf):
raise ValueError(
"All qubits must be either in the '0' or '1' state"
" in the 'ProjectorBlock' definition."
)
self.ket = ket
self.bra = bra
super().__init__(qubit_support, noise=noise)

Bases: ParametricBlock

Scale blocks are created when multiplying a block by a number or parameter.

Example:

from qadence import X
print(X(0) * 2)
[mul: 2]
└── X(0)
Source code in qadence/blocks/primitive.py
239
240
241
242
243
244
245def __init__(self, block: AbstractBlock, parameter: Any):
self.block = block
# TODO: more meaningful name like `scale`?
self.parameters = (
parameter if isinstance(parameter, ParamMap) else ParamMap(parameter=parameter)
)
super().__init__(block.qubit_support)

TimeEvolutionBlock(qubit_support, noise=None) dataclass

Section titled “ TimeEvolutionBlock(qubit_support, noise=None) dataclass ”

Bases: ParametricBlock

Simple time evolution block with time-independent Hamiltonian.

This class is just a convenience class which is used to label blocks which contains simple time evolution with time-independent Hamiltonian operators

Source code in qadence/blocks/primitive.py
37
38
39
40
41
42
43def __init__(
self,
qubit_support: tuple[int, ...],
noise: NoiseHandler | None = None,
):
self._qubit_support = qubit_support
self._noise = noise

To learn how to use analog blocks and how to mix digital & analog blocks, check out the digital-analog section of the documentation.

Examples on how to use digital-analog blocks can be found in the *examples folder of the qadence repo:

  • Fit a simple sinus: examples/digital-analog/fit-sin.py
  • Solve a QUBO: examples/digital-analog/qubo.py

Bases: AnalogComposite

A chain of analog blocks.

Needed because analog blocks require stricter validation than the general ChainBlock.

AnalogChains can only be constructed from AnalogKron blocks or globally supported, primitive, analog blocks (like InteractionBlocks and ConstantAnalogRotations).

Automatically constructed by the chain function if only analog blocks are given.

Example:

from qadence import X, chain, AnalogInteraction
b = chain(AnalogInteraction(200), AnalogInteraction(200))
print(type(b)) # this is an `AnalogChain`
b = chain(X(0), AnalogInteraction(200))
print(type(b)) # this is a general `ChainBlock`
<class 'qadence.blocks.analog.AnalogChain'>
<class 'qadence.blocks.composite.ChainBlock'>
Source code in qadence/blocks/analog.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277def __init__(self, blocks: Tuple[AnalogBlock, ...]):
"""A chain of analog blocks.
Needed because analog blocks require
stricter validation than the general `ChainBlock`.
`AnalogChain`s can only be constructed from `AnalogKron` blocks or
_**globally supported**_, primitive, analog blocks (like `InteractionBlock`s and
`ConstantAnalogRotation`s).
Automatically constructed by the [`chain`][qadence.blocks.utils.chain]
function if only analog blocks are given.
Example:
```python exec="on" source="material-block" result="json"
from qadence import X, chain, AnalogInteraction
b = chain(AnalogInteraction(200), AnalogInteraction(200))
print(type(b)) # this is an `AnalogChain`
b = chain(X(0), AnalogInteraction(200))
print(type(b)) # this is a general `ChainBlock`
```
"""
for b in blocks:
if not (isinstance(b, AnalogKron) or b.qubit_support.is_global):
raise ValueError("Only KronBlocks or global blocks can be chain'ed.")
self.blocks = blocks

AnalogKron(blocks, interaction=Interaction.NN) dataclass

Section titled “ AnalogKron(blocks, interaction=Interaction.NN) dataclass ”

Bases: AnalogComposite

Stack analog blocks vertically (i.e. in time).

Needed because analog require stricter validation than the general KronBlock.

AnalogKrons can only be constructed from non-global, analog blocks with the same duration.

Source code in qadence/blocks/analog.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318def __init__(self, blocks: Tuple[AnalogBlock, ...], interaction: Interaction = Interaction.NN):
"""Stack analog blocks vertically (i.e. in time).
Needed because analog require
stricter validation than the general `KronBlock`.
`AnalogKron`s can only be constructed from _**non-global**_, analog blocks
with the _**same duration**_.
"""
if len(blocks) == 0:
raise NotImplementedError("Empty KronBlocks not supported")
self.blocks = blocks
self.interaction = interaction
qubit_support = QubitSupport()
duration = blocks[0].duration
for b in blocks:
if not isinstance(b, AnalogBlock):
raise ValueError("Can only kron `AnalgoBlock`s with other `AnalgoBlock`s.")
if b.qubit_support == QubitSupport("global"):
raise ValueError("Blocks with global support cannot be kron'ed.")
if not qubit_support.is_disjoint(b.qubit_support):
raise ValueError("Make sure blocks act on distinct qubits!")
if not np.isclose(evaluate(duration), evaluate(b.duration)):
raise ValueError("Kron'ed blocks have to have same duration.")
qubit_support += b.qubit_support
self.blocks = blocks

ConstantAnalogRotation(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(alpha=0.0, duration=1000.0, omega=0.0, delta=0.0, phase=0.0), qubit_support=QubitSupport('global'), add_pattern=True) dataclass

Section titled “ ConstantAnalogRotation(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(alpha=0.0, duration=1000.0, omega=0.0, delta=0.0, phase=0.0), qubit_support=QubitSupport(&#39;global&#39;), add_pattern=True) dataclass ”

Bases: AnalogBlock

Implements a constant analog rotation with interaction dictated by the chosen Hamiltonian.

H/h =(Ω/2 cos(φ)*Xᵢ - sin(φ)*Yᵢ - δnᵢ) + Hᵢₙₜ.

To construct this block you can use of the following convenience wrappers: - The general rotation operation AnalogRot - Shorthands for rotatins around an axis: AnalogRX, AnalogRY, AnalogRZ

WARNING: do not use ConstantAnalogRotation with alpha as differentiable parameter - use the convenience wrappers mentioned above.

InteractionBlock(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(duration=1000.0), qubit_support=QubitSupport('global'), add_pattern=True) dataclass

Section titled “ InteractionBlock(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(duration=1000.0), qubit_support=QubitSupport(&#39;global&#39;), add_pattern=True) dataclass ”

Bases: AnalogBlock

Free-evolution for the Hamiltonian interaction term of a register of qubits.

In real interacting quantum devices, it means letting the system evolve freely according to the time-dependent Schrodinger equation. With emulators, this block is translated to an appropriate interaction Hamiltonian, for example, an Ising interaction

Hᵢₙₜ = ∑ᵢⱼ C₆/rᵢⱼ⁶ nᵢnⱼ

or an XY-interaction

Hᵢₙₜ = ∑ᵢⱼ C₃/rⱼⱼ³ (XᵢXⱼ + ZᵢZⱼ)

with nᵢ = (1-Zᵢ)/2.

To construct, use the AnalogInteraction function.

Chain blocks sequentially.

On digital backends this can be interpreted loosely as a matrix mutliplication of blocks. In the analog case it chains blocks in time.

PARAMETER DESCRIPTION
*args

Blocks to chain. Can also be a generator.

TYPE: Union[AbstractBlock, Generator, List[AbstractBlock]] DEFAULT: ()

RETURNS DESCRIPTION
ChainBlock

ChainBlock

Example:

from qadence import X, Y, chain
b = chain(X(0), Y(0))
# or use a generator
b = chain(X(i) for i in range(3))
print(b)
ChainBlock(0,1,2)
├── X(0)
├── X(1)
└── X(2)
Source code in qadence/blocks/utils.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81def chain(*args: Union[AbstractBlock, Generator, List[AbstractBlock]]) -> ChainBlock:
"""Chain blocks sequentially.
On digital backends this can be interpreted
loosely as a matrix mutliplication of blocks. In the analog case it chains
blocks in time.
Arguments:
*args: Blocks to chain. Can also be a generator.
Returns:
ChainBlock
Example:
```python exec="on" source="material-block" result="json"
from qadence import X, Y, chain
b = chain(X(0), Y(0))
# or use a generator
b = chain(X(i) for i in range(3))
print(b)
```
"""
# ugly hack to use `AnalogChain` if we are dealing only with analog blocks
if len(args) and all(
isinstance(a, AnalogBlock) or isinstance(a, AnalogComposite) for a in args
):
return analog_chain(*args) # type: ignore[return-value,arg-type]
return _construct(ChainBlock, args)

Stack blocks vertically.

On digital backends this can be intepreted loosely as a kronecker product of blocks. In the analog case it executes blocks parallel in time.

PARAMETER DESCRIPTION
*args

Blocks to kron. Can also be a generator.

TYPE: Union[AbstractBlock, Generator] DEFAULT: ()

RETURNS DESCRIPTION
KronBlock

KronBlock

Example:

from qadence import X, Y, kron
b = kron(X(0), Y(1))
# or use a generator
b = kron(X(i) for i in range(3))
print(b)
KronBlock(0,1,2)
├── X(0)
├── X(1)
└── X(2)
Source code in qadence/blocks/utils.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113def kron(*args: Union[AbstractBlock, Generator]) -> KronBlock:
"""Stack blocks vertically.
On digital backends this can be intepreted
loosely as a kronecker product of blocks. In the analog case it executes
blocks parallel in time.
Arguments:
*args: Blocks to kron. Can also be a generator.
Returns:
KronBlock
Example:
```python exec="on" source="material-block" result="json"
from qadence import X, Y, kron
b = kron(X(0), Y(1))
# or use a generator
b = kron(X(i) for i in range(3))
print(b)
```
"""
# ugly hack to use `AnalogKron` if we are dealing only with analog blocks
if len(args) and all(
isinstance(a, AnalogBlock) or isinstance(a, AnalogComposite) for a in args
):
return analog_kron(*args) # type: ignore[return-value,arg-type]
return _construct(KronBlock, args)

Sums blocks.

PARAMETER DESCRIPTION
*args

Blocks to add. Can also be a generator.

TYPE: Union[AbstractBlock, Generator] DEFAULT: ()

RETURNS DESCRIPTION
AddBlock

AddBlock

Example:

from qadence import X, Y, add
b = add(X(0), Y(0))
# or use a generator
b = add(X(i) for i in range(3))
print(b)
AddBlock(0,1,2)
├── X(0)
├── X(1)
└── X(2)
Source code in qadence/blocks/utils.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136def add(*args: Union[AbstractBlock, Generator]) -> AddBlock:
"""Sums blocks.
Arguments:
*args: Blocks to add. Can also be a generator.
Returns:
AddBlock
Example:
```python exec="on" source="material-block" result="json"
from qadence import X, Y, add
b = add(X(0), Y(0))
# or use a generator
b = add(X(i) for i in range(3))
print(b)
```
"""
return _construct(AddBlock, args)

Bases: CompositeBlock

Adds blocks.

Constructed via add.

Source code in qadence/blocks/composite.py
259
260def __init__(self, blocks: Tuple[AbstractBlock, ...]):
self.blocks = blocks

Bases: CompositeBlock

Chains blocks sequentially.

Constructed via chain

Source code in qadence/blocks/composite.py
191
192def __init__(self, blocks: Tuple[AbstractBlock, ...]):
self.blocks = blocks

CompositeBlock(tag=None, __array_priority__=1000) dataclass

Section titled “ CompositeBlock(tag=None, __array_priority__=1000) dataclass ”

Bases: AbstractBlock

Block which composes multiple blocks into one larger block (which can again be composed).

Composite blocks are constructed via chain, kron, and add.

Bases: CompositeBlock

Stacks blocks horizontally.

Constructed via kron.

Source code in qadence/blocks/composite.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235def __init__(self, blocks: Tuple[AbstractBlock, ...]):
if len(blocks) == 0:
raise NotImplementedError("Empty KronBlocks not supported")
qubit_support = QubitSupport()
for b in blocks:
assert (
QubitSupportType.GLOBAL,
) != b.qubit_support, "Blocks with global support cannot be kron'ed."
assert qubit_support.is_disjoint(
b.qubit_support
), "Make sure blocks act on distinct qubits!"
qubit_support += b.qubit_support
self.blocks = blocks

block_to_tensor(block, values={}, qubit_support=None, use_full_support=False, tensor_type=TensorType.DENSE, endianness=Endianness.BIG, device=None)

Section titled “ block_to_tensor(block, values={}, qubit_support=None, use_full_support=False, tensor_type=TensorType.DENSE, endianness=Endianness.BIG, device=None) ”

Convert a block into a torch tensor.

PARAMETER DESCRIPTION
block

The block to convert.

TYPE: AbstractBlock

values

A optional dict with values for parameters.

TYPE: dict DEFAULT: {}

qubit_support

The qubit_support of the block.

TYPE: tuple DEFAULT: None

use_full_support

True infers the total number of qubits.

TYPE: bool DEFAULT: False

tensor_type

the target tensor type.

TYPE: TensorType DEFAULT: DENSE

RETURNS DESCRIPTION
Tensor

A torch.Tensor.

Examples:

from qadence import hea, hamiltonian_factory, Z, block_to_tensor
block = hea(2,2)
print(block_to_tensor(block))
# In case you have a diagonal observable, you can use
obs = hamiltonian_factory(2, detuning = Z)
print(block_to_tensor(obs, tensor_type="SparseDiagonal"))
tensor([[[ 0.3554+0.3119j, -0.2412-0.5711j, -0.3515+0.0019j, -0.4690+0.2204j],
[-0.0097-0.4932j, 0.2821+0.3119j, -0.6343+0.0848j, -0.4093-0.0528j],
[-0.0809-0.5268j, -0.2912-0.4593j, 0.2103+0.2089j, -0.1240-0.5630j],
[-0.3588-0.3468j, -0.0729-0.3711j, -0.2612-0.5576j, 0.3390+0.3376j]]],
grad_fn=<UnsafeViewBackward0>)
tensor(indices=tensor([[0, 3],
[0, 3]]),
values=tensor([ 2.+0.j, -2.+0.j]),
size=(4, 4), nnz=2, layout=torch.sparse_coo)
Source code in qadence/blocks/block_to_tensor.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361def block_to_tensor(
block: AbstractBlock,
values: dict[str, TNumber | torch.Tensor] = {},
qubit_support: tuple | None = None,
use_full_support: bool = False,
tensor_type: TensorType = TensorType.DENSE,
endianness: Endianness = Endianness.BIG,
device: torch.device = None,
) -> torch.Tensor:
"""
Convert a block into a torch tensor.
Arguments:
block (AbstractBlock): The block to convert.
values (dict): A optional dict with values for parameters.
qubit_support (tuple): The qubit_support of the block.
use_full_support (bool): True infers the total number of qubits.
tensor_type (TensorType): the target tensor type.
Returns:
A torch.Tensor.
Examples:
```python exec="on" source="material-block" result="json"
from qadence import hea, hamiltonian_factory, Z, block_to_tensor
block = hea(2,2)
print(block_to_tensor(block))
# In case you have a diagonal observable, you can use
obs = hamiltonian_factory(2, detuning = Z)
print(block_to_tensor(obs, tensor_type="SparseDiagonal"))
```
"""
from qadence.blocks import embedding
(ps, embed) = embedding(block)
values = embed(ps, values)
if tensor_type == TensorType.DENSE:
return _block_to_tensor_embedded(
block,
values,
qubit_support,
use_full_support,
endianness=endianness,
device=device,
)
elif tensor_type == TensorType.SPARSEDIAGONAL:
t = block_to_diagonal(block, values, endianness=endianness)
indices, values, size = torch.nonzero(t), t[t != 0], len(t)
indices = torch.stack((indices.flatten(), indices.flatten()))
return torch.sparse_coo_tensor(indices, values, (size, size))