Module hybridq.circuit.utils
Author: Salvatore Mandra (salvatore.mandra@nasa.gov)
Copyright © 2021, United States Government, as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.
The HybridQ: A Hybrid Simulator for Quantum Circuits platform is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Expand source code
"""
Author: Salvatore Mandra (salvatore.mandra@nasa.gov)
Copyright © 2021, United States Government, as represented by the Administrator
of the National Aeronautics and Space Administration. All rights reserved.
The HybridQ: A Hybrid Simulator for Quantum Circuits platform is licensed under
the Apache License, Version 2.0 (the "License"); you may not use this file
except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from __future__ import annotations
from hybridq.gate import Gate, BaseGate
from hybridq.utils import sort, argsort
from hybridq.circuit import Circuit
from tqdm.auto import tqdm
import numpy as np
def flatten(a: Circuit) -> Circuit:
"""
Return a flattened circuit. More precisely, `flatten` iteratively looks for
gates that provide `flatten` in order to return a flattened circuit.
Parameters
----------
a: Circuit
Circuit to flatten.
Returns
-------
Circuit
Flattened circuit.
"""
return Circuit(
g for gs in a for g in (gs if gs.provides('flatten') else (gs,)))
def isidentity(a: Circuit, atol: float = 1e-8) -> bool:
"""
Check if `a` is close to identity within an absolute tollerance of `atol`.
The check is done by getting the matrix representation of the circuit `a`.
Parameters
----------
a: Circuit
Circuit to check.
atol: float, optional
Absolute tollerance.
Returns
-------
bool
`True` if `a` is close to the identity, otherwise `False`.
"""
# Get matrix
M = matrix(a)
# Check if close to identity
return np.allclose(M, np.eye(M.shape[0]), atol=atol)
def isclose(a: Circuit,
b: Circuit,
use_matrix_commutation: bool = True,
max_n_qubits_matrix: int = 10,
atol: float = 1e-8,
verbose: bool = False) -> bool:
"""
Check if `a` is close to `b` within the absolute tollerance
`atol`.
Parameters
----------
circuit: Circuit[BaseGate]
`Circuit` to compare with.
use_matrix_commutation: bool, optional
Use commutation rules. See `hybridq.circuit.utils.simplify`.
max_n_qubits_matrix: int, optional
Matrices are computes for gates with up to `max_n_qubits_matrix` qubits
(default: 10).
atol: float, optional
Absolute tollerance.
Returns
-------
bool
`True` if the two circuits are close within the absolute tollerance
`atol`, and `False` otherwise.
See Also
--------
hybridq.circuit.utils.simplify
Example
-------
>>> c = Circuit(Gate('H', [q]) for q in range(10))
>>> c.isclose(Circuit(g**1.1 for g in c))
False
>>> c.isclose(Circuit(g**1.1 for g in c), atol=1e-1)
True
"""
# Get simplified circuit
s = simplify(a + b.inv(),
use_matrix_commutation=use_matrix_commutation,
max_n_qubits_matrix=max_n_qubits_matrix,
atol=atol,
verbose=verbose)
return not s or all(
isidentity([g], atol=atol)
for g in tqdm(s, disable=not verbose, desc='Check'))
def insert_from_left(circuit: iter[BaseGate],
gate: BaseGate,
atol: float = 1e-8,
*,
use_matrix_commutation: bool = True,
max_n_qubits_matrix: int = 10,
simplify: bool = True,
pop: bool = False,
pinned_qubits: list[any] = None,
inplace: bool = False) -> Circuit:
"""
Add a gate to circuit starting from the left, commuting with existing gates
if necessary.
Parameters
----------
circuit: Circuit
`gate` will be added to `circuit`.
gate: Gate
Gate to add to `circuit`.
atol: float, optional
Absolute tollerance while simplifying.
use_matrix_commutation: bool, optional
Use matrix commutation while simplifying `circuit`.
max_n_qubits_matrix: int, optional
Matrices are computes for gates with up to `max_n_qubits_matrix` qubits
(default: 10).
simplify: bool, optional
Simplify `circuit` while adding `gate` (default: `True`).
pop: bool, optional
Remove `gate` if it commutes with all gates in `circuit` (default: `False`).
pinned_qubits: list[any], optional
If `pop` is `True`, remove gates unless `gate` share qubits with
`pinned_qubits` (default: `None`).
inplace: bool, optional
If `True`, add `gate` to `circuit` in-place (default: `False`)
Returns
-------
Circuit
Circuit with `gate` added to it.
"""
from copy import deepcopy
# Copy circuit if required
if not inplace:
circuit = Circuit(circuit, copy=True)
# Get qubits
if gate.provides('qubits') and gate.qubits is not None:
_qubits = set(gate.qubits)
else:
# If gate has not qubits, just append to the left
circuit.insert(0, deepcopy(gate))
return circuit
# Iterate over all the gates
for _p, _g in enumerate(circuit):
# Remove if gate simplifies with _g
try:
if simplify and gate.inv().isclose(_g, atol=atol):
del (circuit[_p])
return circuit
except:
pass
# Otherwise, check if gate can commute with _g. If not, insert gate
# and exit loop.
_commute = False
try:
if _g.n_qubits <= max_n_qubits_matrix:
_commute |= not _qubits.intersection(_g.qubits)
_commute |= use_matrix_commutation and gate.commutes_with(
_g, atol=atol)
except:
pass
finally:
if not _commute:
circuit.insert(_p, deepcopy(gate))
return circuit
# If commutes with everything, just append at the end
if not pop or _qubits.intersection(pinned_qubits):
circuit.append(deepcopy(gate))
# Return circuit
return circuit
def to_nx(circuit: iter[BaseGate],
add_final_nodes: bool = True,
node_tags: dict = None,
edge_tags: dict = None,
return_qubits_map: bool = False,
leaves_prefix: str = 'q') -> networkx.Graph:
"""
Return graph representation of circuit. `to_nx` is deterministic, so it can
be reused elsewhere.
Parameters
----------
circuit: iter[BaseGate]
Circuit to get graph representation from.
add_final_nodes: bool, optional
Add final nodes for each qubit to the graph representation of `circuit`.
node_tags: dict, optional
Add specific tags to nodes.
edge_tags: dict, optional
Add specific tags to edges.
return_qubits_map: bool, optional
Return map associated to the Circuit qubits.
leaves_prefix: str, optional
Specify prefix to use for leaves.
Returns
-------
networkx.Graph
Graph representing `circuit`.
Example
-------
>>> import networkx as nx
>>>
>>> # Define circuit
>>> circuit = Circuit(
>>> [Gate('X', qubits=[0])**1.2,
>>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1]))
>>>
>>> # Draw graph
>>> nx.draw_planar(utils.to_nx(circuit))
.. image:: ../../images/circuit_nx.png
"""
import networkx as nx
# Initialize
if node_tags is None:
node_tags = {}
if edge_tags is None:
edge_tags = {}
# Check if node is a leaf
def _is_leaf(node):
return type(node) == str and node[:len(leaves_prefix)] == leaves_prefix
# Convert iterable to Circuit
circuit = Circuit(circuit)
# Get graph
graph = nx.DiGraph()
# Get qubits
qubits = circuit.all_qubits()
# Get qubits_map
qubits_map = {q: i for i, q in enumerate(qubits)}
# Check that no qubits is 'confused' as leaf
if any(_is_leaf(q) for q in qubits):
raise ValueError(
f"No qubits must start with 'leaves_prefix'={leaves_prefix}.")
# Add first layer
for q in qubits:
graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_i',
qubits=[q],
**node_tags)
# Last leg
last_leg = {q: f'{leaves_prefix}_{qubits_map[q]}_i' for q in qubits}
# Build network
for x, gate in enumerate(circuit):
# Add node
graph.add_node(x,
circuit=Circuit([gate]),
qubits=sort(gate.qubits),
**node_tags)
# Add edges (time directed)
graph.add_edges_from([(last_leg[q], x) for q in gate.qubits],
**edge_tags)
# Update last_leg
last_leg.update({q: x for q in gate.qubits})
# Add last indexes if required
if add_final_nodes:
for q in qubits:
graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_f',
qubits=[q],
**node_tags)
graph.add_edges_from([(x, f'{leaves_prefix}_{qubits_map[q]}_f')
for q, x in last_leg.items()], **edge_tags)
if return_qubits_map:
return graph, qubits_map
else:
return graph
def to_tn(circuit: iter[BaseGate],
complex_type: any = 'complex64',
return_qubits_map: bool = False,
leaves_prefix: str = 'q_') -> quimb.tensor.TensorNetwork:
"""
Return `quimb.tensor.TensorNetwork` representing `circuit`. `to_tn` is
deterministic, so it can be reused elsewhere.
Parameters
----------
circuit: iter[BaseGate]
Circuit to get `quimb.tensor.TensorNetwork` representation from.
complex_type: any, optional
Complex type to use while getting the `quimb.tensor.TensorNetwork`
representation.
return_qubits_map: bool, optional
Return map associated to the Circuit qubits.
leaves_prefix: str, optional
Specify prefix to use for leaves.
Returns
-------
quimb.tensor.TensorNetwork
Tensor representing `circuit`.
Example
-------
>>> # Define circuit
>>> circuit = Circuit(
>>> [Gate('X', qubits=[0])**1.2,
>>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1]))
>>>
>>> # Draw graph
>>> utils.to_tn(circuit).graph()
.. image:: ../../images/circuit_tn.png
"""
import quimb.tensor as tn
# Convert iterable to Circuit
circuit = Circuit(circuit)
# Get all qubits
all_qubits = circuit.all_qubits()
# Get qubits map
qubits_map = {q: i for i, q in enumerate(all_qubits)}
# Get last_tag
last_tag = {q: 'i' for q in all_qubits}
# Node generator
def _get_node(t, gate):
# Get matrix
U = np.reshape(gate.matrix().astype(complex_type),
[2] * (2 * len(gate.qubits)))
# Get indexes
inds = [f'{leaves_prefix}_{qubits_map[q]}_{t}' for q in gate.qubits] + [
f'{leaves_prefix}_{qubits_map[q]}_{last_tag[q]}'
for q in gate.qubits
]
# Update last_tag
for q in gate.qubits:
last_tag[q] = t
# Return node
return tn.Tensor(
U.astype(complex_type),
inds=inds,
tags=[f'{leaves_prefix}_{qubits_map[q]}' for q in gate.qubits] +
[f'gate-idx_{t}'])
# Get list of tensors
tensor = [_get_node(t, gate) for t, gate in enumerate(circuit)]
# Generate new output map
output_map = {
f'{leaves_prefix}_{qubits_map[q]}_{t}':
f'{leaves_prefix}_{qubits_map[q]}_f' for q, t in last_tag.items()
}
# Rename output legs
for node in tensor:
node.reindex(output_map, inplace=True)
# Return tensor network
if return_qubits_map:
return tn.TensorNetwork(tensor), qubits_map
else:
return tn.TensorNetwork(tensor)
def to_matrix_gate(circuit: iter[BaseGate],
complex_type: any = 'complex64',
**kwargs) -> BaseGate:
"""
Convert `circuit` to a matrix `BaseGate`.
Parameters
----------
circuit: iter[BaseGate]
Circuit to convert to `BaseGate`.
complex_type: any, optional
Float type to use while converting to `BaseGate`.
Returns
-------
Gate
`BaseGate` representing `circuit`.
Example
-------
>>> # Define circuit
>>> circuit = Circuit(
>>> [Gate('X', qubits=[0])**1.2,
>>> Gate('ISWAP', qubits=[0, 1])**2.3])
>>>
>>> gate = utils.to_matrix_gate(circuit)
>>> gate
Gate(name=MATRIX, qubits=[0, 1], U=np.array(shape=(4, 4), dtype=complex64))
>>> gate.U
array([[ 0.09549151-0.29389262j, 0. +0.j ,
0.9045085 +0.29389262j, 0. +0.j ],
[ 0.13342446-0.41063824j, -0.08508356+0.26186025j,
-0.13342446-0.04335224j, -0.8059229 -0.26186025j],
[-0.8059229 -0.26186025j, -0.13342446-0.04335224j,
-0.08508356+0.26186025j, 0.13342446-0.41063824j],
[ 0. +0.j , 0.9045085 +0.29389262j,
0. +0.j , 0.09549151-0.29389262j]],
dtype=complex64)
"""
# Convert iterable to Circuit
circuit = Circuit(circuit)
return Gate('MATRIX',
qubits=circuit.all_qubits(),
U=matrix(circuit, complex_type=complex_type, **kwargs))
def compress(circuit: iter[BaseGate],
max_n_qubits: int = 2,
*,
exclude_qubits: iter[any] = None,
use_matrix_commutation: bool = True,
max_n_qubits_matrix: int = 10,
skip_compression: iter[{type, str}] = None,
skip_commutation: iter[{type, str}] = None,
atol: float = 1e-8,
verbose: bool = False) -> list[Circuit]:
"""
Compress gates together up to the specified number of qubits. `compress`
is deterministic, so it can be reused elsewhere.
Parameters
----------
circuit: iter[BaseGate]
Circuit to compress.
max_n_qubits: int, optional
Maximum number of qubits that a compressed gate may have.
exclude_qubits: list[any], optional
Exclude gates which act on `exclude_qubits` to be compressed.
use_matrix_commutation: bool, optional
If `True`, use commutation to maximize compression.
max_n_qubits_matrix: int, optional
Limit the size of matrices when checking for commutation.
skip_compression: iter[{type, str}], optional
If `BaseGate` is either an instance of any types in `skip_compression`,
it provides any methods in `skip_compression`, or `BaseGate` name will
match any names in `skip_compression`, `BaseGate` will not be
compressed. However, if `use_matrix_commutation` is `True`, commutation will
be checked against `BaseGate`.
skip_commutation: iter[{type, str}], optional
If `BaseGate` is either an instance of any types in `skip_commutation`,
it provides any methods in `skip_commutation`, or `BaseGate` name will
match any names in `skip_commutation`, `BaseGate` will not be checked
against commutation.
atol: float
Absolute tollerance for commutation.
verbose: bool, optional
Verbose output.
Returns
-------
list[Circuit]
A list of `Circuit`s, with each `Circuit` representing a compressed
`BaseGate`.
See Also
--------
hybridq.gate.commutes_with
Example
-------
>>> # Define circuit
>>> circuit = Circuit(
>>> [Gate('X', qubits=[0])**1.2,
>>> Gate('ISWAP', qubits=[0, 1])**2.3,
>>> Gate('ISWAP', qubits=[0, 2])**2.3])
>>>
>>> # Compress circuit up to 1-qubit gates
>>> utils.compress(circuit, 1)
[Circuit([
Gate(name=X, qubits=[0])**1.2
]),
Circuit([
Gate(name=ISWAP, qubits=[0, 1])**2.3
]),
Circuit([
Gate(name=ISWAP, qubits=[0, 2])**2.3
])]
>>> # Compress circuit up to 2-qubit gates
>>> utils.compress(circuit, 2)
[Circuit([
Gate(name=X, qubits=[0])**1.2
Gate(name=ISWAP, qubits=[0, 1])**2.3
]),
Circuit([
Gate(name=ISWAP, qubits=[0, 2])**2.3
])]
>>> # Compress circuit up to 3-qubit gates
>>> utils.compress(circuit, 3)
[Circuit([
Gate(name=X, qubits=[0])**1.2
Gate(name=ISWAP, qubits=[0, 1])**2.3
Gate(name=ISWAP, qubits=[0, 2])**2.3
])]
"""
# If max_n_qubits <= 0, split every gate
if max_n_qubits <= 0:
return [Circuit([g]) for g in circuit]
# Initialize skip_compression and skip_commutation
skip_compression = tuple() if skip_compression is None else tuple(
skip_compression)
skip_commutation = tuple() if skip_commutation is None else tuple(
skip_commutation)
def _check_skip(gate, x):
if isinstance(x, type):
return isinstance(gate, x)
elif isinstance(x, str):
return gate.name == x.upper() or gate.provides(x)
else:
raise ValueError(f"'{x}' not supported.")
# Initialize exclude_qubits
exclude_qubits = set([] if exclude_qubits is None else exclude_qubits)
# Convert to Circuit
circuit = Circuit(circuit)
# Initialize compressed circuit
new_circuit = []
# For every gate in circuit ..
for gate in tqdm(circuit,
disable=not verbose,
desc=f'Compress ({max_n_qubits})'):
# Initialize matrix gate
_gate = None
# Initialize _compress
gate_properties = dict(compress=True, commute=True)
# Initialize index
_merge_to = len(new_circuit)
# If gate does not provide qubits or qubits is None,
# then gate is not compressible
if not gate.provides('qubits') or gate.qubits is None:
gate_properties['compress'] = False
gate_properties['commute'] = False
# Otherwise ...
else:
# Get qubits
_q = set(gate.qubits)
# Get Matrix gate if possible
try:
_gate = to_matrix_gate(
[gate], max_compress=0) if use_matrix_commutation and len(
_q) <= max_n_qubits_matrix else None
except:
_gate = None
# Check if gate must skip compression
if any(_check_skip(gate, t) for t in skip_compression
) or _q.intersection(exclude_qubits):
gate_properties['compress'] = False
# Check if gate must skip commutation
if any(_check_skip(gate, t) for t in skip_commutation):
gate_properties['commute'] = False
# Check for each existing layer
for i, (_circ, _circ_gate,
_circ_properties) in reversed(list(enumerate(new_circuit))):
# If _circ does not provide any qubits, just break
try:
# Get circuit qubits
_cq = set(_circ.all_qubits())
except:
break
# Check if both gate and _circ can be compressed
if gate_properties['compress'] and _circ_properties['compress']:
# Check if it can be merged
if len(_q.union(_cq)) <= max(max_n_qubits, len(_cq),
len(_q)):
_merge_to = i
# Check commutation
if use_matrix_commutation and gate_properties[
'commute'] and _circ_properties['commute']:
# Check if gate and _circ share any qubit
if not _q.intersection(_cq):
continue
# Check if gate and _circ commute using matrix
try:
if _gate.commutes_with(_circ_gate):
continue
except:
pass
# Otherwise, just break
break
# If it possible to merge the gate to an existing layer
if _merge_to < len(new_circuit):
# Get layer
_nc = new_circuit[_merge_to]
# Update circuit
_nc[0].append(gate)
# Update matrix
try:
_nc[1] = to_matrix_gate(
[_nc[1], _gate],
max_compress=0) if use_matrix_commutation and len(
set(_gate.qubits).union(
_nc[1].qubits)) <= max_n_qubits_matrix else None
except:
_nc[1] = None
# Update properties
for k in ['compress', 'commute']:
_nc[2][k] &= gate_properties[k]
# Otherwise, create a new layer
else:
new_circuit.append([Circuit([gate]), _gate, gate_properties])
# Return only circuits
return [c for c, _, _ in new_circuit]
def matrix(circuit: iter[BaseGate],
order: iter[any] = None,
complex_type: any = 'complex64',
max_compress: int = 4,
verbose: bool = False) -> numpy.ndarray:
"""
Return matrix representing `circuit`.
Parameters
----------
circuit: iter[BaseGate]
Circuit to get the matrix from.
order: iter[any], optional
If specified, a matrix is returned following the order given by
`order`. Otherwise, `circuit.all_qubits()` is used.
max_compress: int, optional
To reduce the computational cost, `circuit` is compressed prior to
compute the matrix.
complex_type: any, optional
Complex type to use to compute the matrix.
verbose: bool, optional
Verbose output.
Returns
-------
numpy.ndarray
Unitary matrix of `circuit`.
Example
-------
>>> # Define circuit
>>> circuit = Circuit([Gate('CX', [1, 0])])
>>> # Show qubits
[0, 1]
>>> circuit.all_qubits()
>>> # Get matrix without specifying any qubits order
>>> # (therefore using circuit.all_qubits() == [0, 1])
>>> utils.matrix()
array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]], dtype=complex64)
>>> # Get matrix with a specific order of qubits
>>> utils.matrix(Circuit([Gate('CX', [1, 0])]), order=[1, 0])
array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64)
"""
# Convert iterable to Circuit
circuit = Circuit(circuit)
# Check order
if order is not None:
# Conver to list
order = list(order)
if set(order).difference(circuit.all_qubits()):
raise ValueError(
"'order' must be a valid permutation of indexes in 'Circuit'.")
# Compress circuit
if max_compress > 0:
return matrix(Circuit(
to_matrix_gate(c, complex_type=complex_type, max_compress=0)
for c in compress(circuit, max_n_qubits=max_compress)),
order=order,
complex_type=complex_type,
max_compress=0,
verbose=verbose)
# Get qubits
qubits = circuit.all_qubits()
n_qubits = len(qubits)
# Initialize matrix
U = np.reshape(np.eye(2**n_qubits, order='C', dtype=complex_type),
[2] * (2 * n_qubits))
for g in tqdm(circuit, disable=not verbose):
# Get gate's qubits
_qubits = g.qubits
_n_qubits = len(_qubits)
# Get map
_map = [qubits.index(q) for q in _qubits]
_map += [x for x in range(n_qubits) if x not in _map]
# Reorder qubits
qubits = [qubits[x] for x in _map]
# Update U
U = np.reshape(
g.matrix().astype(complex_type) @ np.reshape(
np.transpose(U, _map + list(range(n_qubits, 2 * n_qubits))),
(2**_n_qubits, 2**(2 * n_qubits - _n_qubits))),
(2,) * (2 * n_qubits))
# Get U
U = np.reshape(
np.transpose(U,
argsort(qubits) + list(range(n_qubits, 2 * n_qubits))),
(2**n_qubits, 2**n_qubits))
# Reorder if required
if order and order != circuit.all_qubits():
qubits = circuit.all_qubits()
U = np.reshape(
np.transpose(np.reshape(U, (2,) * (2 * n_qubits)),
[qubits.index(q) for q in order] +
[n_qubits + qubits.index(q) for q in order]),
(2**n_qubits, 2**n_qubits))
# Check U has the right type and order
assert (U.dtype == np.dtype(complex_type))
assert (U.data.c_contiguous)
# Return matrix
return U
def unitary(*args, **kwargs):
"""
Alias for `utils.matrix`.
"""
from hybridq.utils import DeprecationWarning
from warnings import warn
# Warn that `self.matrix` should be used instead of `self.unitary`
warn("Since '0.7.0', 'hybridq.circuit.utils.matrix' should be used instead "
"of the less general 'hybridq.circuit.utils.unitary'")
# Call matrix
return matrix(*args, **kwargs)
def simplify(circuit: list[BaseGate],
atol: float = 1e-8,
use_matrix_commutation: bool = True,
max_n_qubits_matrix: int = 10,
remove_id_gates: bool = True,
verbose: bool = False) -> Circuit:
"""
Compress together gates up to the specified number of qubits.
"""
# Initialize new circuit
new_circuit = Circuit()
# Remove gates if required
if remove_id_gates:
rev_circuit = (g for g in reversed(circuit) if g.name != 'I' and (
not g.provides('matrix') or g.n_qubits > max_n_qubits_matrix or
not isidentity([g], atol=atol)))
else:
rev_circuit = reversed(circuit)
# Insert gates, one by one
for gate in tqdm(rev_circuit,
disable=not verbose,
total=len(circuit),
desc='Simplify'):
insert_from_left(new_circuit,
gate,
atol=atol,
use_matrix_commutation=use_matrix_commutation,
max_n_qubits_matrix=max_n_qubits_matrix,
simplify=True,
pop=False,
pinned_qubits=None,
inplace=True)
# Return simplified circuit
return new_circuit
def popright(circuit: list[BaseGate],
pinned_qubits: list[any],
atol: float = 1e-8,
use_matrix_commutation: bool = True,
max_n_qubits_matrix: int = 10,
simplify: bool = True,
verbose: bool = False) -> Circuit:
"""
Remove gates outside the lightcone created by pinned_qubits.
"""
# Initialize new circuit
new_circuit = Circuit()
# Insert gates, one by one
for gate in tqdm(reversed(circuit),
disable=not verbose,
total=len(circuit),
desc='Pop'):
insert_from_left(new_circuit,
gate,
atol=atol,
use_matrix_commutation=use_matrix_commutation,
max_n_qubits_matrix=max_n_qubits_matrix,
simplify=simplify,
pop=True,
pinned_qubits=pinned_qubits,
inplace=True)
# Return simplified circuit
return new_circuit
def popleft(circuit: list[BaseGate],
pinned_qubits: list[any],
atol: float = 1e-8,
use_matrix_commutation: bool = True,
simplify: bool = True,
verbose: bool = False) -> Circuit:
"""
Remove gates outside the lightcone created by pinned_qubits (starting from the right).
"""
return Circuit(
reversed(
popright(list(reversed(circuit)),
pinned_qubits=pinned_qubits,
atol=atol,
use_matrix_commutation=use_matrix_commutation,
simplify=simplify,
verbose=verbose)))
def pop(circuit: list[BaseGate],
direction: str,
pinned_qubits: list[any],
atol: float = 1e-8,
use_matrix_commutation: bool = True,
simplify: bool = True,
verbose: bool = False) -> Circuit:
"""
Remove gates outside the lightcone created by pinned_qubits.
"""
from functools import partial as partial_func
_popleft = partial_func(popleft,
pinned_qubits=pinned_qubits,
atol=atol,
use_matrix_commutation=use_matrix_commutation,
simplify=simplify,
verbose=verbose)
_popright = partial_func(popright,
pinned_qubits=pinned_qubits,
atol=atol,
use_matrix_commutation=use_matrix_commutation,
simplify=simplify,
verbose=verbose)
if direction == 'left':
return _popleft(circuit)
elif direction == 'right':
return _popright(circuit)
elif direction == 'both':
return _popleft(_popright(circuit))
else:
raise ValueError(f"direction='{direction}' not supported.")
def moments(
circuit: iter[{BaseGate, Circuit}]) -> list[list[{BaseGate, Circuit}]]:
"""
Split circuit in moments.
"""
from hybridq.gate import TupleGate
# Convert iterable to list
circuit = list(circuit)
# If circuit is empty, return a single empty TupleGate
if not circuit:
return [TupleGate()]
# Get qubits
def _get_qubits(x):
if isinstance(x, BaseGate):
return x.qubits if x.n_qubits else tuple()
elif isinstance(x, Circuit):
return x.all_qubits()
else:
raise ValueError(f"'{x}' is not valid.")
# Get all used qubits
qubits = sort({q for x in circuit for q in _get_qubits(x)})
# Get map of leves
level_map = {q: 0 for q in qubits}
level = [0] * len(circuit)
# Get the right level for each object ..
for i, x in enumerate(circuit):
# Get qubits
_qubits = _get_qubits(x)
# If gate is acting on qubits, add gate to the right level
if _qubits:
# Get max level
level[i] = np.max([level_map[q] for q in _get_qubits(x)]) + 1
# Update level_map
level_map.update({q: level[i] for q in _get_qubits(x)})
# .. otherwise, simply update all qubits to create a new moment
else:
level[i] = np.max(level) + 1
level_map = {q: level[i] for q in qubits}
# Initialize moments
moments = [[] for _ in range(np.max(level))]
# Update moments
for i, x in enumerate(circuit):
moments[level[i] - 1].append(x)
# Return moments
return list(map(TupleGate, moments))
def remove_swap(circuit: Circuit) -> tuple[Circuit, dict[any, any]]:
"""
Iteratively remove SWAP's from circuit by actually swapping qubits.
The output map will have the form new_qubit -> old_qubit.
"""
# Initialize map
_qubits_map = {q: q for q in circuit.all_qubits()}
# Initialize circuit
_circ = Circuit()
# Get ideal SWAP
_SWAP = Gate('SWAP').matrix()
# For each gate in circuit ..
for gate in circuit:
# Check if gate is close to SWAP
if gate.n_qubits == 2 and gate.qubits and np.allclose(
gate.matrix(), _SWAP):
# If true, swap qubits
_q0 = next(k for k, v in _qubits_map.items() if v == gate.qubits[0])
_q1 = next(k for k, v in _qubits_map.items() if v == gate.qubits[1])
_qubits_map[_q0], _qubits_map[_q1] = _qubits_map[_q1], _qubits_map[
_q0]
# Otherwise, remap qubits and append
else:
# Get the right qubits
_qubits = [
next(k
for k, v in _qubits_map.items()
if v == q)
for q in gate.qubits
]
# Append to the new circuit
_circ.append(gate.on(_qubits))
# Return circuit and map
return _circ, _qubits_map
def expand_iswap(circuit: Circuit) -> Circuit:
"""
Expand ISWAP's by iteratively replacing with SWAP's, CZ's and Phases.
"""
from copy import deepcopy
# Get ideal iSWAP
_iSWAP = Gate('ISWAP').matrix()
# Initialize circuit
_circ = Circuit()
# For each gate in circuit ..
for gate in circuit:
# Check if gate is close to SWAP
if gate.n_qubits == 2 and gate.qubits and np.allclose(
gate.matrix(), _iSWAP):
# Get tags
_tags = gate.tags if gate.provides('tags') else {}
# Expand iSWAP
_ext = [
Gate('SWAP', qubits=gate.qubits, tags=_tags),
Gate('CZ', qubits=gate.qubits, tags=_tags),
Gate('P', qubits=[gate.qubits[0]], tags=_tags),
Gate('P', qubits=[gate.qubits[1]], tags=_tags),
]
# Append to circuit
_circ.extend(_ext if gate.power == 1 else (
g**-1 for g in reversed(_ext)))
# Otherwise, just append
else:
_circ.append(deepcopy(gate))
# Return circuit
return _circ
def filter(circuit: iter,
names: list[str] = any,
qubits: list[any] = any,
params: list[any] = any,
n_qubits: int = any,
n_params: int = any,
virtual: bool = any,
exact_match: bool = False,
atol: float = 1e-8,
**kwargs) -> iter:
# Initialize
f_circuit = iter(circuit)
# Filter by name
if names is not any:
names = {str(name).upper() for name in names}
f_circuit = (gate for gate in f_circuit if gate.name in names)
# Filter by qubits
if qubits is not any:
if exact_match:
qubits = tuple(qubits)
f_circuit = (gate for gate in f_circuit
if gate.provides('qubits') and gate.qubits == qubits)
else:
qubits = set(qubits)
f_circuit = (gate for gate in f_circuit
if gate.provides('qubits') and gate.qubits and
qubits.intersection(gate.qubits))
# Filter by parameters
if params is not any:
def _isclose(x, y):
try:
_x = float(x)
_y = float(y)
except:
return x == y
else:
return np.isclose(_x, _y, atol=atol)
f_circuit = (gate for gate in f_circuit
if gate.provides('params') and gate.params and all(
_isclose(x, y) for x, y in zip(gate.params, params)))
# Filter by number of qubits
if n_qubits is not any:
f_circuit = (gate for gate in f_circuit
if gate.provides('qubits') and gate.n_qubits == n_qubits)
# Filter by number of parameters
if n_params is not any:
f_circuit = (gate for gate in f_circuit
if gate.provides('params') and gate.n_params == n_params)
# Filter virtual gates
if virtual is not any:
f_circuit = (gate for gate in f_circuit if gate.isvirtual() == virtual)
# Filter by tags
for k, v in kwargs.items():
# Define filter
if exact_match:
def _filter(gate):
if gate.provides('tags'):
for k, v in kwargs.items():
if k not in gate.tags or (v is not any and
gate.tags[k] != v):
return False
return True
else:
return False
else:
def _filter(gate):
if gate.provides('tags'):
for k, v in kwargs.items():
if k in gate.tags and (v is any or gate.tags[k] == v):
return True
return False
else:
return False
f_circuit = (gate for gate in f_circuit if _filter(gate))
return f_circuit
Functions
def compress(circuit: iter[BaseGate], max_n_qubits: int = 2, *, exclude_qubits: iter[any] = None, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, skip_compression: iter[{type, str}] = None, skip_commutation: iter[{type, str}] = None, atol: float = 1e-08, verbose: bool = False) ‑> list[Circuit]
-
Compress gates together up to the specified number of qubits.
compress()
is deterministic, so it can be reused elsewhere.Parameters
circuit
:iter[BaseGate]
- Circuit to compress.
max_n_qubits
:int
, optional- Maximum number of qubits that a compressed gate may have.
exclude_qubits
:list[any]
, optional- Exclude gates which act on
exclude_qubits
to be compressed. use_matrix_commutation
:bool
, optional- If
True
, use commutation to maximize compression. max_n_qubits_matrix
:int
, optional- Limit the size of matrices when checking for commutation.
skip_compression
:iter[{type, str}]
, optional- If
BaseGate
is either an instance of any types inskip_compression
, it provides any methods inskip_compression
, orBaseGate
name will match any names inskip_compression
,BaseGate
will not be compressed. However, ifuse_matrix_commutation
isTrue
, commutation will be checked againstBaseGate
. skip_commutation
:iter[{type, str}]
, optional- If
BaseGate
is either an instance of any types inskip_commutation
, it provides any methods inskip_commutation
, orBaseGate
name will match any names inskip_commutation
,BaseGate
will not be checked against commutation. atol
:float
- Absolute tollerance for commutation.
verbose
:bool
, optional- Verbose output.
Returns
list[Circuit]
- A list of
Circuit
s, with eachCircuit
representing a compressedBaseGate
.
See Also
hybridq.gate.commutes_with
Example
>>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3, >>> Gate('ISWAP', qubits=[0, 2])**2.3]) >>> >>> # Compress circuit up to 1-qubit gates >>> utils.compress(circuit, 1) [Circuit([ Gate(name=X, qubits=[0])**1.2 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 1])**2.3 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] >>> # Compress circuit up to 2-qubit gates >>> utils.compress(circuit, 2) [Circuit([ Gate(name=X, qubits=[0])**1.2 Gate(name=ISWAP, qubits=[0, 1])**2.3 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] >>> # Compress circuit up to 3-qubit gates >>> utils.compress(circuit, 3) [Circuit([ Gate(name=X, qubits=[0])**1.2 Gate(name=ISWAP, qubits=[0, 1])**2.3 Gate(name=ISWAP, qubits=[0, 2])**2.3 ])]
Expand source code
def compress(circuit: iter[BaseGate], max_n_qubits: int = 2, *, exclude_qubits: iter[any] = None, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, skip_compression: iter[{type, str}] = None, skip_commutation: iter[{type, str}] = None, atol: float = 1e-8, verbose: bool = False) -> list[Circuit]: """ Compress gates together up to the specified number of qubits. `compress` is deterministic, so it can be reused elsewhere. Parameters ---------- circuit: iter[BaseGate] Circuit to compress. max_n_qubits: int, optional Maximum number of qubits that a compressed gate may have. exclude_qubits: list[any], optional Exclude gates which act on `exclude_qubits` to be compressed. use_matrix_commutation: bool, optional If `True`, use commutation to maximize compression. max_n_qubits_matrix: int, optional Limit the size of matrices when checking for commutation. skip_compression: iter[{type, str}], optional If `BaseGate` is either an instance of any types in `skip_compression`, it provides any methods in `skip_compression`, or `BaseGate` name will match any names in `skip_compression`, `BaseGate` will not be compressed. However, if `use_matrix_commutation` is `True`, commutation will be checked against `BaseGate`. skip_commutation: iter[{type, str}], optional If `BaseGate` is either an instance of any types in `skip_commutation`, it provides any methods in `skip_commutation`, or `BaseGate` name will match any names in `skip_commutation`, `BaseGate` will not be checked against commutation. atol: float Absolute tollerance for commutation. verbose: bool, optional Verbose output. Returns ------- list[Circuit] A list of `Circuit`s, with each `Circuit` representing a compressed `BaseGate`. See Also -------- hybridq.gate.commutes_with Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3, >>> Gate('ISWAP', qubits=[0, 2])**2.3]) >>> >>> # Compress circuit up to 1-qubit gates >>> utils.compress(circuit, 1) [Circuit([ Gate(name=X, qubits=[0])**1.2 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 1])**2.3 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] >>> # Compress circuit up to 2-qubit gates >>> utils.compress(circuit, 2) [Circuit([ Gate(name=X, qubits=[0])**1.2 Gate(name=ISWAP, qubits=[0, 1])**2.3 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] >>> # Compress circuit up to 3-qubit gates >>> utils.compress(circuit, 3) [Circuit([ Gate(name=X, qubits=[0])**1.2 Gate(name=ISWAP, qubits=[0, 1])**2.3 Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] """ # If max_n_qubits <= 0, split every gate if max_n_qubits <= 0: return [Circuit([g]) for g in circuit] # Initialize skip_compression and skip_commutation skip_compression = tuple() if skip_compression is None else tuple( skip_compression) skip_commutation = tuple() if skip_commutation is None else tuple( skip_commutation) def _check_skip(gate, x): if isinstance(x, type): return isinstance(gate, x) elif isinstance(x, str): return gate.name == x.upper() or gate.provides(x) else: raise ValueError(f"'{x}' not supported.") # Initialize exclude_qubits exclude_qubits = set([] if exclude_qubits is None else exclude_qubits) # Convert to Circuit circuit = Circuit(circuit) # Initialize compressed circuit new_circuit = [] # For every gate in circuit .. for gate in tqdm(circuit, disable=not verbose, desc=f'Compress ({max_n_qubits})'): # Initialize matrix gate _gate = None # Initialize _compress gate_properties = dict(compress=True, commute=True) # Initialize index _merge_to = len(new_circuit) # If gate does not provide qubits or qubits is None, # then gate is not compressible if not gate.provides('qubits') or gate.qubits is None: gate_properties['compress'] = False gate_properties['commute'] = False # Otherwise ... else: # Get qubits _q = set(gate.qubits) # Get Matrix gate if possible try: _gate = to_matrix_gate( [gate], max_compress=0) if use_matrix_commutation and len( _q) <= max_n_qubits_matrix else None except: _gate = None # Check if gate must skip compression if any(_check_skip(gate, t) for t in skip_compression ) or _q.intersection(exclude_qubits): gate_properties['compress'] = False # Check if gate must skip commutation if any(_check_skip(gate, t) for t in skip_commutation): gate_properties['commute'] = False # Check for each existing layer for i, (_circ, _circ_gate, _circ_properties) in reversed(list(enumerate(new_circuit))): # If _circ does not provide any qubits, just break try: # Get circuit qubits _cq = set(_circ.all_qubits()) except: break # Check if both gate and _circ can be compressed if gate_properties['compress'] and _circ_properties['compress']: # Check if it can be merged if len(_q.union(_cq)) <= max(max_n_qubits, len(_cq), len(_q)): _merge_to = i # Check commutation if use_matrix_commutation and gate_properties[ 'commute'] and _circ_properties['commute']: # Check if gate and _circ share any qubit if not _q.intersection(_cq): continue # Check if gate and _circ commute using matrix try: if _gate.commutes_with(_circ_gate): continue except: pass # Otherwise, just break break # If it possible to merge the gate to an existing layer if _merge_to < len(new_circuit): # Get layer _nc = new_circuit[_merge_to] # Update circuit _nc[0].append(gate) # Update matrix try: _nc[1] = to_matrix_gate( [_nc[1], _gate], max_compress=0) if use_matrix_commutation and len( set(_gate.qubits).union( _nc[1].qubits)) <= max_n_qubits_matrix else None except: _nc[1] = None # Update properties for k in ['compress', 'commute']: _nc[2][k] &= gate_properties[k] # Otherwise, create a new layer else: new_circuit.append([Circuit([gate]), _gate, gate_properties]) # Return only circuits return [c for c, _, _ in new_circuit]
def expand_iswap(circuit: Circuit) ‑> Circuit
-
Expand ISWAP's by iteratively replacing with SWAP's, CZ's and Phases.
Expand source code
def expand_iswap(circuit: Circuit) -> Circuit: """ Expand ISWAP's by iteratively replacing with SWAP's, CZ's and Phases. """ from copy import deepcopy # Get ideal iSWAP _iSWAP = Gate('ISWAP').matrix() # Initialize circuit _circ = Circuit() # For each gate in circuit .. for gate in circuit: # Check if gate is close to SWAP if gate.n_qubits == 2 and gate.qubits and np.allclose( gate.matrix(), _iSWAP): # Get tags _tags = gate.tags if gate.provides('tags') else {} # Expand iSWAP _ext = [ Gate('SWAP', qubits=gate.qubits, tags=_tags), Gate('CZ', qubits=gate.qubits, tags=_tags), Gate('P', qubits=[gate.qubits[0]], tags=_tags), Gate('P', qubits=[gate.qubits[1]], tags=_tags), ] # Append to circuit _circ.extend(_ext if gate.power == 1 else ( g**-1 for g in reversed(_ext))) # Otherwise, just append else: _circ.append(deepcopy(gate)) # Return circuit return _circ
def filter(circuit: iter, names: list[str] = <built-in function any>, qubits: list[any] = <built-in function any>, params: list[any] = <built-in function any>, n_qubits: int = <built-in function any>, n_params: int = <built-in function any>, virtual: bool = <built-in function any>, exact_match: bool = False, atol: float = 1e-08, **kwargs) ‑> iter
-
Expand source code
def filter(circuit: iter, names: list[str] = any, qubits: list[any] = any, params: list[any] = any, n_qubits: int = any, n_params: int = any, virtual: bool = any, exact_match: bool = False, atol: float = 1e-8, **kwargs) -> iter: # Initialize f_circuit = iter(circuit) # Filter by name if names is not any: names = {str(name).upper() for name in names} f_circuit = (gate for gate in f_circuit if gate.name in names) # Filter by qubits if qubits is not any: if exact_match: qubits = tuple(qubits) f_circuit = (gate for gate in f_circuit if gate.provides('qubits') and gate.qubits == qubits) else: qubits = set(qubits) f_circuit = (gate for gate in f_circuit if gate.provides('qubits') and gate.qubits and qubits.intersection(gate.qubits)) # Filter by parameters if params is not any: def _isclose(x, y): try: _x = float(x) _y = float(y) except: return x == y else: return np.isclose(_x, _y, atol=atol) f_circuit = (gate for gate in f_circuit if gate.provides('params') and gate.params and all( _isclose(x, y) for x, y in zip(gate.params, params))) # Filter by number of qubits if n_qubits is not any: f_circuit = (gate for gate in f_circuit if gate.provides('qubits') and gate.n_qubits == n_qubits) # Filter by number of parameters if n_params is not any: f_circuit = (gate for gate in f_circuit if gate.provides('params') and gate.n_params == n_params) # Filter virtual gates if virtual is not any: f_circuit = (gate for gate in f_circuit if gate.isvirtual() == virtual) # Filter by tags for k, v in kwargs.items(): # Define filter if exact_match: def _filter(gate): if gate.provides('tags'): for k, v in kwargs.items(): if k not in gate.tags or (v is not any and gate.tags[k] != v): return False return True else: return False else: def _filter(gate): if gate.provides('tags'): for k, v in kwargs.items(): if k in gate.tags and (v is any or gate.tags[k] == v): return True return False else: return False f_circuit = (gate for gate in f_circuit if _filter(gate)) return f_circuit
def flatten(a: Circuit) ‑> Circuit
-
Return a flattened circuit. More precisely,
flatten()
iteratively looks for gates that provideflatten()
in order to return a flattened circuit.Parameters
a
:Circuit
- Circuit to flatten.
Returns
Circuit
- Flattened circuit.
Expand source code
def flatten(a: Circuit) -> Circuit: """ Return a flattened circuit. More precisely, `flatten` iteratively looks for gates that provide `flatten` in order to return a flattened circuit. Parameters ---------- a: Circuit Circuit to flatten. Returns ------- Circuit Flattened circuit. """ return Circuit( g for gs in a for g in (gs if gs.provides('flatten') else (gs,)))
def insert_from_left(circuit: iter[BaseGate], gate: BaseGate, atol: float = 1e-08, *, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, simplify: bool = True, pop: bool = False, pinned_qubits: list[any] = None, inplace: bool = False) ‑> Circuit
-
Add a gate to circuit starting from the left, commuting with existing gates if necessary.
Parameters
circuit
:Circuit
gate
will be added tocircuit
.gate
:Gate
- Gate to add to
circuit
. atol
:float
, optional- Absolute tollerance while simplifying.
use_matrix_commutation
:bool
, optional- Use matrix commutation while simplifying
circuit
. max_n_qubits_matrix
:int
, optional- Matrices are computes for gates with up to
max_n_qubits_matrix
qubits (default: 10). simplify
:bool
, optional- Simplify
circuit
while addinggate
(default:True
). pop
:bool
, optional- Remove
gate
if it commutes with all gates incircuit
(default:False
). pinned_qubits
:list[any]
, optional- If
pop()
isTrue
, remove gates unlessgate
share qubits withpinned_qubits
(default:None
). inplace
:bool
, optional- If
True
, addgate
tocircuit
in-place (default:False
)
Returns
Circuit
- Circuit with
gate
added to it.
Expand source code
def insert_from_left(circuit: iter[BaseGate], gate: BaseGate, atol: float = 1e-8, *, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, simplify: bool = True, pop: bool = False, pinned_qubits: list[any] = None, inplace: bool = False) -> Circuit: """ Add a gate to circuit starting from the left, commuting with existing gates if necessary. Parameters ---------- circuit: Circuit `gate` will be added to `circuit`. gate: Gate Gate to add to `circuit`. atol: float, optional Absolute tollerance while simplifying. use_matrix_commutation: bool, optional Use matrix commutation while simplifying `circuit`. max_n_qubits_matrix: int, optional Matrices are computes for gates with up to `max_n_qubits_matrix` qubits (default: 10). simplify: bool, optional Simplify `circuit` while adding `gate` (default: `True`). pop: bool, optional Remove `gate` if it commutes with all gates in `circuit` (default: `False`). pinned_qubits: list[any], optional If `pop` is `True`, remove gates unless `gate` share qubits with `pinned_qubits` (default: `None`). inplace: bool, optional If `True`, add `gate` to `circuit` in-place (default: `False`) Returns ------- Circuit Circuit with `gate` added to it. """ from copy import deepcopy # Copy circuit if required if not inplace: circuit = Circuit(circuit, copy=True) # Get qubits if gate.provides('qubits') and gate.qubits is not None: _qubits = set(gate.qubits) else: # If gate has not qubits, just append to the left circuit.insert(0, deepcopy(gate)) return circuit # Iterate over all the gates for _p, _g in enumerate(circuit): # Remove if gate simplifies with _g try: if simplify and gate.inv().isclose(_g, atol=atol): del (circuit[_p]) return circuit except: pass # Otherwise, check if gate can commute with _g. If not, insert gate # and exit loop. _commute = False try: if _g.n_qubits <= max_n_qubits_matrix: _commute |= not _qubits.intersection(_g.qubits) _commute |= use_matrix_commutation and gate.commutes_with( _g, atol=atol) except: pass finally: if not _commute: circuit.insert(_p, deepcopy(gate)) return circuit # If commutes with everything, just append at the end if not pop or _qubits.intersection(pinned_qubits): circuit.append(deepcopy(gate)) # Return circuit return circuit
def isclose(a: Circuit, b: Circuit, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, atol: float = 1e-08, verbose: bool = False) ‑> bool
-
Check if
a
is close tob
within the absolute tolleranceatol
.Parameters
circuit
:Circuit[BaseGate]
Circuit
to compare with.use_matrix_commutation
:bool
, optional- Use commutation rules. See
simplify()
. max_n_qubits_matrix
:int
, optional- Matrices are computes for gates with up to
max_n_qubits_matrix
qubits (default: 10). atol
:float
, optional- Absolute tollerance.
Returns
bool
True
if the two circuits are close within the absolute tolleranceatol
, andFalse
otherwise.
See Also
Example
>>> c = Circuit(Gate('H', [q]) for q in range(10)) >>> c.isclose(Circuit(g**1.1 for g in c)) False >>> c.isclose(Circuit(g**1.1 for g in c), atol=1e-1) True
Expand source code
def isclose(a: Circuit, b: Circuit, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, atol: float = 1e-8, verbose: bool = False) -> bool: """ Check if `a` is close to `b` within the absolute tollerance `atol`. Parameters ---------- circuit: Circuit[BaseGate] `Circuit` to compare with. use_matrix_commutation: bool, optional Use commutation rules. See `hybridq.circuit.utils.simplify`. max_n_qubits_matrix: int, optional Matrices are computes for gates with up to `max_n_qubits_matrix` qubits (default: 10). atol: float, optional Absolute tollerance. Returns ------- bool `True` if the two circuits are close within the absolute tollerance `atol`, and `False` otherwise. See Also -------- hybridq.circuit.utils.simplify Example ------- >>> c = Circuit(Gate('H', [q]) for q in range(10)) >>> c.isclose(Circuit(g**1.1 for g in c)) False >>> c.isclose(Circuit(g**1.1 for g in c), atol=1e-1) True """ # Get simplified circuit s = simplify(a + b.inv(), use_matrix_commutation=use_matrix_commutation, max_n_qubits_matrix=max_n_qubits_matrix, atol=atol, verbose=verbose) return not s or all( isidentity([g], atol=atol) for g in tqdm(s, disable=not verbose, desc='Check'))
def isidentity(a: Circuit, atol: float = 1e-08) ‑> bool
-
Check if
a
is close to identity within an absolute tollerance ofatol
. The check is done by getting the matrix representation of the circuita
.Parameters
a
:Circuit
- Circuit to check.
atol
:float
, optional- Absolute tollerance.
Returns
bool
True
ifa
is close to the identity, otherwiseFalse
.
Expand source code
def isidentity(a: Circuit, atol: float = 1e-8) -> bool: """ Check if `a` is close to identity within an absolute tollerance of `atol`. The check is done by getting the matrix representation of the circuit `a`. Parameters ---------- a: Circuit Circuit to check. atol: float, optional Absolute tollerance. Returns ------- bool `True` if `a` is close to the identity, otherwise `False`. """ # Get matrix M = matrix(a) # Check if close to identity return np.allclose(M, np.eye(M.shape[0]), atol=atol)
def matrix(circuit: iter[BaseGate], order: iter[any] = None, complex_type: any = 'complex64', max_compress: int = 4, verbose: bool = False) ‑> numpy.ndarray
-
Return matrix representing
circuit
.Parameters
circuit
:iter[BaseGate]
- Circuit to get the matrix from.
order
:iter[any]
, optional- If specified, a matrix is returned following the order given by
order
. Otherwise,circuit.all_qubits()
is used. max_compress
:int
, optional- To reduce the computational cost,
circuit
is compressed prior to compute the matrix. complex_type
:any
, optional- Complex type to use to compute the matrix.
verbose
:bool
, optional- Verbose output.
Returns
numpy.ndarray
- Unitary matrix of
circuit
.
Example
>>> # Define circuit >>> circuit = Circuit([Gate('CX', [1, 0])]) >>> # Show qubits [0, 1] >>> circuit.all_qubits() >>> # Get matrix without specifying any qubits order >>> # (therefore using circuit.all_qubits() == [0, 1]) >>> utils.matrix() array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]], dtype=complex64) >>> # Get matrix with a specific order of qubits >>> utils.matrix(Circuit([Gate('CX', [1, 0])]), order=[1, 0]) array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64)
Expand source code
def matrix(circuit: iter[BaseGate], order: iter[any] = None, complex_type: any = 'complex64', max_compress: int = 4, verbose: bool = False) -> numpy.ndarray: """ Return matrix representing `circuit`. Parameters ---------- circuit: iter[BaseGate] Circuit to get the matrix from. order: iter[any], optional If specified, a matrix is returned following the order given by `order`. Otherwise, `circuit.all_qubits()` is used. max_compress: int, optional To reduce the computational cost, `circuit` is compressed prior to compute the matrix. complex_type: any, optional Complex type to use to compute the matrix. verbose: bool, optional Verbose output. Returns ------- numpy.ndarray Unitary matrix of `circuit`. Example ------- >>> # Define circuit >>> circuit = Circuit([Gate('CX', [1, 0])]) >>> # Show qubits [0, 1] >>> circuit.all_qubits() >>> # Get matrix without specifying any qubits order >>> # (therefore using circuit.all_qubits() == [0, 1]) >>> utils.matrix() array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]], dtype=complex64) >>> # Get matrix with a specific order of qubits >>> utils.matrix(Circuit([Gate('CX', [1, 0])]), order=[1, 0]) array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64) """ # Convert iterable to Circuit circuit = Circuit(circuit) # Check order if order is not None: # Conver to list order = list(order) if set(order).difference(circuit.all_qubits()): raise ValueError( "'order' must be a valid permutation of indexes in 'Circuit'.") # Compress circuit if max_compress > 0: return matrix(Circuit( to_matrix_gate(c, complex_type=complex_type, max_compress=0) for c in compress(circuit, max_n_qubits=max_compress)), order=order, complex_type=complex_type, max_compress=0, verbose=verbose) # Get qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # Initialize matrix U = np.reshape(np.eye(2**n_qubits, order='C', dtype=complex_type), [2] * (2 * n_qubits)) for g in tqdm(circuit, disable=not verbose): # Get gate's qubits _qubits = g.qubits _n_qubits = len(_qubits) # Get map _map = [qubits.index(q) for q in _qubits] _map += [x for x in range(n_qubits) if x not in _map] # Reorder qubits qubits = [qubits[x] for x in _map] # Update U U = np.reshape( g.matrix().astype(complex_type) @ np.reshape( np.transpose(U, _map + list(range(n_qubits, 2 * n_qubits))), (2**_n_qubits, 2**(2 * n_qubits - _n_qubits))), (2,) * (2 * n_qubits)) # Get U U = np.reshape( np.transpose(U, argsort(qubits) + list(range(n_qubits, 2 * n_qubits))), (2**n_qubits, 2**n_qubits)) # Reorder if required if order and order != circuit.all_qubits(): qubits = circuit.all_qubits() U = np.reshape( np.transpose(np.reshape(U, (2,) * (2 * n_qubits)), [qubits.index(q) for q in order] + [n_qubits + qubits.index(q) for q in order]), (2**n_qubits, 2**n_qubits)) # Check U has the right type and order assert (U.dtype == np.dtype(complex_type)) assert (U.data.c_contiguous) # Return matrix return U
def moments(circuit: iter[{BaseGate, Circuit}]) ‑> list[list[{BaseGate, Circuit}]]
-
Split circuit in moments.
Expand source code
def moments( circuit: iter[{BaseGate, Circuit}]) -> list[list[{BaseGate, Circuit}]]: """ Split circuit in moments. """ from hybridq.gate import TupleGate # Convert iterable to list circuit = list(circuit) # If circuit is empty, return a single empty TupleGate if not circuit: return [TupleGate()] # Get qubits def _get_qubits(x): if isinstance(x, BaseGate): return x.qubits if x.n_qubits else tuple() elif isinstance(x, Circuit): return x.all_qubits() else: raise ValueError(f"'{x}' is not valid.") # Get all used qubits qubits = sort({q for x in circuit for q in _get_qubits(x)}) # Get map of leves level_map = {q: 0 for q in qubits} level = [0] * len(circuit) # Get the right level for each object .. for i, x in enumerate(circuit): # Get qubits _qubits = _get_qubits(x) # If gate is acting on qubits, add gate to the right level if _qubits: # Get max level level[i] = np.max([level_map[q] for q in _get_qubits(x)]) + 1 # Update level_map level_map.update({q: level[i] for q in _get_qubits(x)}) # .. otherwise, simply update all qubits to create a new moment else: level[i] = np.max(level) + 1 level_map = {q: level[i] for q in qubits} # Initialize moments moments = [[] for _ in range(np.max(level))] # Update moments for i, x in enumerate(circuit): moments[level[i] - 1].append(x) # Return moments return list(map(TupleGate, moments))
def pop(circuit: list[BaseGate], direction: str, pinned_qubits: list[any], atol: float = 1e-08, use_matrix_commutation: bool = True, simplify: bool = True, verbose: bool = False) ‑> Circuit
-
Remove gates outside the lightcone created by pinned_qubits.
Expand source code
def pop(circuit: list[BaseGate], direction: str, pinned_qubits: list[any], atol: float = 1e-8, use_matrix_commutation: bool = True, simplify: bool = True, verbose: bool = False) -> Circuit: """ Remove gates outside the lightcone created by pinned_qubits. """ from functools import partial as partial_func _popleft = partial_func(popleft, pinned_qubits=pinned_qubits, atol=atol, use_matrix_commutation=use_matrix_commutation, simplify=simplify, verbose=verbose) _popright = partial_func(popright, pinned_qubits=pinned_qubits, atol=atol, use_matrix_commutation=use_matrix_commutation, simplify=simplify, verbose=verbose) if direction == 'left': return _popleft(circuit) elif direction == 'right': return _popright(circuit) elif direction == 'both': return _popleft(_popright(circuit)) else: raise ValueError(f"direction='{direction}' not supported.")
def popleft(circuit: list[BaseGate], pinned_qubits: list[any], atol: float = 1e-08, use_matrix_commutation: bool = True, simplify: bool = True, verbose: bool = False) ‑> Circuit
-
Remove gates outside the lightcone created by pinned_qubits (starting from the right).
Expand source code
def popleft(circuit: list[BaseGate], pinned_qubits: list[any], atol: float = 1e-8, use_matrix_commutation: bool = True, simplify: bool = True, verbose: bool = False) -> Circuit: """ Remove gates outside the lightcone created by pinned_qubits (starting from the right). """ return Circuit( reversed( popright(list(reversed(circuit)), pinned_qubits=pinned_qubits, atol=atol, use_matrix_commutation=use_matrix_commutation, simplify=simplify, verbose=verbose)))
def popright(circuit: list[BaseGate], pinned_qubits: list[any], atol: float = 1e-08, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, simplify: bool = True, verbose: bool = False) ‑> Circuit
-
Remove gates outside the lightcone created by pinned_qubits.
Expand source code
def popright(circuit: list[BaseGate], pinned_qubits: list[any], atol: float = 1e-8, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, simplify: bool = True, verbose: bool = False) -> Circuit: """ Remove gates outside the lightcone created by pinned_qubits. """ # Initialize new circuit new_circuit = Circuit() # Insert gates, one by one for gate in tqdm(reversed(circuit), disable=not verbose, total=len(circuit), desc='Pop'): insert_from_left(new_circuit, gate, atol=atol, use_matrix_commutation=use_matrix_commutation, max_n_qubits_matrix=max_n_qubits_matrix, simplify=simplify, pop=True, pinned_qubits=pinned_qubits, inplace=True) # Return simplified circuit return new_circuit
def remove_swap(circuit: Circuit) ‑> tuple[Circuit, dict[any, any]]
-
Iteratively remove SWAP's from circuit by actually swapping qubits. The output map will have the form new_qubit -> old_qubit.
Expand source code
def remove_swap(circuit: Circuit) -> tuple[Circuit, dict[any, any]]: """ Iteratively remove SWAP's from circuit by actually swapping qubits. The output map will have the form new_qubit -> old_qubit. """ # Initialize map _qubits_map = {q: q for q in circuit.all_qubits()} # Initialize circuit _circ = Circuit() # Get ideal SWAP _SWAP = Gate('SWAP').matrix() # For each gate in circuit .. for gate in circuit: # Check if gate is close to SWAP if gate.n_qubits == 2 and gate.qubits and np.allclose( gate.matrix(), _SWAP): # If true, swap qubits _q0 = next(k for k, v in _qubits_map.items() if v == gate.qubits[0]) _q1 = next(k for k, v in _qubits_map.items() if v == gate.qubits[1]) _qubits_map[_q0], _qubits_map[_q1] = _qubits_map[_q1], _qubits_map[ _q0] # Otherwise, remap qubits and append else: # Get the right qubits _qubits = [ next(k for k, v in _qubits_map.items() if v == q) for q in gate.qubits ] # Append to the new circuit _circ.append(gate.on(_qubits)) # Return circuit and map return _circ, _qubits_map
def simplify(circuit: list[BaseGate], atol: float = 1e-08, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, remove_id_gates: bool = True, verbose: bool = False) ‑> Circuit
-
Compress together gates up to the specified number of qubits.
Expand source code
def simplify(circuit: list[BaseGate], atol: float = 1e-8, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, remove_id_gates: bool = True, verbose: bool = False) -> Circuit: """ Compress together gates up to the specified number of qubits. """ # Initialize new circuit new_circuit = Circuit() # Remove gates if required if remove_id_gates: rev_circuit = (g for g in reversed(circuit) if g.name != 'I' and ( not g.provides('matrix') or g.n_qubits > max_n_qubits_matrix or not isidentity([g], atol=atol))) else: rev_circuit = reversed(circuit) # Insert gates, one by one for gate in tqdm(rev_circuit, disable=not verbose, total=len(circuit), desc='Simplify'): insert_from_left(new_circuit, gate, atol=atol, use_matrix_commutation=use_matrix_commutation, max_n_qubits_matrix=max_n_qubits_matrix, simplify=True, pop=False, pinned_qubits=None, inplace=True) # Return simplified circuit return new_circuit
def to_matrix_gate(circuit: iter[BaseGate], complex_type: any = 'complex64', **kwargs) ‑> BaseGate
-
Convert
circuit
to a matrixBaseGate
.Parameters
circuit
:iter[BaseGate]
- Circuit to convert to
BaseGate
. complex_type
:any
, optional- Float type to use while converting to
BaseGate
.
Returns
Gate
BaseGate
representingcircuit
.
Example
>>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3]) >>> >>> gate = utils.to_matrix_gate(circuit) >>> gate Gate(name=MATRIX, qubits=[0, 1], U=np.array(shape=(4, 4), dtype=complex64)) >>> gate.U array([[ 0.09549151-0.29389262j, 0. +0.j , 0.9045085 +0.29389262j, 0. +0.j ], [ 0.13342446-0.41063824j, -0.08508356+0.26186025j, -0.13342446-0.04335224j, -0.8059229 -0.26186025j], [-0.8059229 -0.26186025j, -0.13342446-0.04335224j, -0.08508356+0.26186025j, 0.13342446-0.41063824j], [ 0. +0.j , 0.9045085 +0.29389262j, 0. +0.j , 0.09549151-0.29389262j]], dtype=complex64)
Expand source code
def to_matrix_gate(circuit: iter[BaseGate], complex_type: any = 'complex64', **kwargs) -> BaseGate: """ Convert `circuit` to a matrix `BaseGate`. Parameters ---------- circuit: iter[BaseGate] Circuit to convert to `BaseGate`. complex_type: any, optional Float type to use while converting to `BaseGate`. Returns ------- Gate `BaseGate` representing `circuit`. Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3]) >>> >>> gate = utils.to_matrix_gate(circuit) >>> gate Gate(name=MATRIX, qubits=[0, 1], U=np.array(shape=(4, 4), dtype=complex64)) >>> gate.U array([[ 0.09549151-0.29389262j, 0. +0.j , 0.9045085 +0.29389262j, 0. +0.j ], [ 0.13342446-0.41063824j, -0.08508356+0.26186025j, -0.13342446-0.04335224j, -0.8059229 -0.26186025j], [-0.8059229 -0.26186025j, -0.13342446-0.04335224j, -0.08508356+0.26186025j, 0.13342446-0.41063824j], [ 0. +0.j , 0.9045085 +0.29389262j, 0. +0.j , 0.09549151-0.29389262j]], dtype=complex64) """ # Convert iterable to Circuit circuit = Circuit(circuit) return Gate('MATRIX', qubits=circuit.all_qubits(), U=matrix(circuit, complex_type=complex_type, **kwargs))
def to_nx(circuit: iter[BaseGate], add_final_nodes: bool = True, node_tags: dict = None, edge_tags: dict = None, return_qubits_map: bool = False, leaves_prefix: str = 'q') ‑> networkx.Graph
-
Return graph representation of circuit.
to_nx()
is deterministic, so it can be reused elsewhere.Parameters
circuit
:iter[BaseGate]
- Circuit to get graph representation from.
add_final_nodes
:bool
, optional- Add final nodes for each qubit to the graph representation of
circuit
. node_tags
:dict
, optional- Add specific tags to nodes.
edge_tags
:dict
, optional- Add specific tags to edges.
return_qubits_map
:bool
, optional- Return map associated to the Circuit qubits.
leaves_prefix
:str
, optional- Specify prefix to use for leaves.
Returns
networkx.Graph
- Graph representing
circuit
.
Example
>>> import networkx as nx >>> >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1])) >>> >>> # Draw graph >>> nx.draw_planar(utils.to_nx(circuit))
Expand source code
def to_nx(circuit: iter[BaseGate], add_final_nodes: bool = True, node_tags: dict = None, edge_tags: dict = None, return_qubits_map: bool = False, leaves_prefix: str = 'q') -> networkx.Graph: """ Return graph representation of circuit. `to_nx` is deterministic, so it can be reused elsewhere. Parameters ---------- circuit: iter[BaseGate] Circuit to get graph representation from. add_final_nodes: bool, optional Add final nodes for each qubit to the graph representation of `circuit`. node_tags: dict, optional Add specific tags to nodes. edge_tags: dict, optional Add specific tags to edges. return_qubits_map: bool, optional Return map associated to the Circuit qubits. leaves_prefix: str, optional Specify prefix to use for leaves. Returns ------- networkx.Graph Graph representing `circuit`. Example ------- >>> import networkx as nx >>> >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1])) >>> >>> # Draw graph >>> nx.draw_planar(utils.to_nx(circuit)) .. image:: ../../images/circuit_nx.png """ import networkx as nx # Initialize if node_tags is None: node_tags = {} if edge_tags is None: edge_tags = {} # Check if node is a leaf def _is_leaf(node): return type(node) == str and node[:len(leaves_prefix)] == leaves_prefix # Convert iterable to Circuit circuit = Circuit(circuit) # Get graph graph = nx.DiGraph() # Get qubits qubits = circuit.all_qubits() # Get qubits_map qubits_map = {q: i for i, q in enumerate(qubits)} # Check that no qubits is 'confused' as leaf if any(_is_leaf(q) for q in qubits): raise ValueError( f"No qubits must start with 'leaves_prefix'={leaves_prefix}.") # Add first layer for q in qubits: graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_i', qubits=[q], **node_tags) # Last leg last_leg = {q: f'{leaves_prefix}_{qubits_map[q]}_i' for q in qubits} # Build network for x, gate in enumerate(circuit): # Add node graph.add_node(x, circuit=Circuit([gate]), qubits=sort(gate.qubits), **node_tags) # Add edges (time directed) graph.add_edges_from([(last_leg[q], x) for q in gate.qubits], **edge_tags) # Update last_leg last_leg.update({q: x for q in gate.qubits}) # Add last indexes if required if add_final_nodes: for q in qubits: graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_f', qubits=[q], **node_tags) graph.add_edges_from([(x, f'{leaves_prefix}_{qubits_map[q]}_f') for q, x in last_leg.items()], **edge_tags) if return_qubits_map: return graph, qubits_map else: return graph
def to_tn(circuit: iter[BaseGate], complex_type: any = 'complex64', return_qubits_map: bool = False, leaves_prefix: str = 'q_') ‑> quimb.tensor.TensorNetwork
-
Return
quimb.tensor.TensorNetwork
representingcircuit
.to_tn()
is deterministic, so it can be reused elsewhere.Parameters
circuit
:iter[BaseGate]
- Circuit to get
quimb.tensor.TensorNetwork
representation from. complex_type
:any
, optional- Complex type to use while getting the
quimb.tensor.TensorNetwork
representation. return_qubits_map
:bool
, optional- Return map associated to the Circuit qubits.
leaves_prefix
:str
, optional- Specify prefix to use for leaves.
Returns
quimb.tensor.TensorNetwork
- Tensor representing
circuit
.
Example
>>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1])) >>> >>> # Draw graph >>> utils.to_tn(circuit).graph()
Expand source code
def to_tn(circuit: iter[BaseGate], complex_type: any = 'complex64', return_qubits_map: bool = False, leaves_prefix: str = 'q_') -> quimb.tensor.TensorNetwork: """ Return `quimb.tensor.TensorNetwork` representing `circuit`. `to_tn` is deterministic, so it can be reused elsewhere. Parameters ---------- circuit: iter[BaseGate] Circuit to get `quimb.tensor.TensorNetwork` representation from. complex_type: any, optional Complex type to use while getting the `quimb.tensor.TensorNetwork` representation. return_qubits_map: bool, optional Return map associated to the Circuit qubits. leaves_prefix: str, optional Specify prefix to use for leaves. Returns ------- quimb.tensor.TensorNetwork Tensor representing `circuit`. Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1])) >>> >>> # Draw graph >>> utils.to_tn(circuit).graph() .. image:: ../../images/circuit_tn.png """ import quimb.tensor as tn # Convert iterable to Circuit circuit = Circuit(circuit) # Get all qubits all_qubits = circuit.all_qubits() # Get qubits map qubits_map = {q: i for i, q in enumerate(all_qubits)} # Get last_tag last_tag = {q: 'i' for q in all_qubits} # Node generator def _get_node(t, gate): # Get matrix U = np.reshape(gate.matrix().astype(complex_type), [2] * (2 * len(gate.qubits))) # Get indexes inds = [f'{leaves_prefix}_{qubits_map[q]}_{t}' for q in gate.qubits] + [ f'{leaves_prefix}_{qubits_map[q]}_{last_tag[q]}' for q in gate.qubits ] # Update last_tag for q in gate.qubits: last_tag[q] = t # Return node return tn.Tensor( U.astype(complex_type), inds=inds, tags=[f'{leaves_prefix}_{qubits_map[q]}' for q in gate.qubits] + [f'gate-idx_{t}']) # Get list of tensors tensor = [_get_node(t, gate) for t, gate in enumerate(circuit)] # Generate new output map output_map = { f'{leaves_prefix}_{qubits_map[q]}_{t}': f'{leaves_prefix}_{qubits_map[q]}_f' for q, t in last_tag.items() } # Rename output legs for node in tensor: node.reindex(output_map, inplace=True) # Return tensor network if return_qubits_map: return tn.TensorNetwork(tensor), qubits_map else: return tn.TensorNetwork(tensor)
def unitary(*args, **kwargs)
-
Alias for
utils.matrix
.Expand source code
def unitary(*args, **kwargs): """ Alias for `utils.matrix`. """ from hybridq.utils import DeprecationWarning from warnings import warn # Warn that `self.matrix` should be used instead of `self.unitary` warn("Since '0.7.0', 'hybridq.circuit.utils.matrix' should be used instead " "of the less general 'hybridq.circuit.utils.unitary'") # Call matrix return matrix(*args, **kwargs)