# Project: Qhronology (https://github.com/lgbishop/qhronology)
# Author: lgbishop <lachlanbishop@protonmail.com>
# Copyright: Lachlan G. Bishop 2025
# License: AGPLv3 (non-commercial use), proprietary (commercial use)
# For more details, see the README in the project repository:
# https://github.com/lgbishop/qhronology,
# or visit the website:
# https://qhronology.com.
"""
A class for the creation of quantum circuits.
"""
# https://peps.python.org/pep-0649/
# https://peps.python.org/pep-0749/
from __future__ import annotations
import copy
from typing import Any
import sympy as sp
from sympy.physics.quantum import TensorProduct
from sympy.physics.quantum.dagger import Dagger
from qhronology.utilities.classification import (
num,
sym,
mat,
arr,
Forms,
Kinds,
Shapes,
matrix_form,
matrix_shape,
)
from qhronology.utilities.diagrams import (
Families,
Sections,
Styles,
DiagramColumn,
DiagramCircuit,
)
from qhronology.utilities.helpers import (
flatten_list,
check_systems_conflicts,
adjust_targets,
symbolize_expression,
symbolize_tuples,
extract_matrix,
recursively_simplify,
count_systems,
extract_matrix,
)
from qhronology.utilities.objects import QuantumObject
from qhronology.utilities.symbolics import SymbolicsProperties
from qhronology.quantum.states import QuantumState
from qhronology.quantum.gates import (
QuantumGate,
_Single,
)
from qhronology.mechanics.operations import densify
[docs]
class QuantumCircuit(SymbolicsProperties):
"""A class for creating quantum circuits and storing their metadata.
Instances provide complete descriptions of quantum circuits, along with various associated
attributes (such as mathematical conditions, including normalization).
The circuit's input is recorded as a list of :py:class:`~qhronology.quantum.states.QuantumState`
objects, with the composition of the elements of this forming the total input.
Similarly, the circuit's transformation on its input is recorded as a list of
:py:class:`~qhronology.quantum.gates.QuantumGate` objects, with the product of the (linear)
elements of this list forming the total transformation (e.g., unitary matrix).
Arguments
---------
inputs : list[QuantumState]
An ordered list of :py:class:`~qhronology.quantum.states.QuantumState` instances.
The total input state is the tensor product of these individual states in the order
in which they appear in ``inputs``.
Must all have the same value of the ``dim`` property.
Defaults to ``[]``.
gates : list[QuantumGate]
An ordered list of :py:class:`~qhronology.quantum.gates.QuantumGate` instances.
The total gate is the product of these individual gates in the order in which they appear
in ``gates``.
Must all have the same values of the ``dim`` and ``num_systems`` properties.
Defaults to ``[]``.
traces : list[int]
The numerical indices of the subsystems to be traced over.
Defaults to ``[]``.
postselections: list[tuple[mat | arr | QuantumObject, int | list[int]]]
A list of 2-tuples of vectors or matrix operators paired with the first (smallest) index
of their postselection target systems.
Must all have the same value of the ``dim`` property.
Defaults to ``[]``.
symbols : dict[sym | str, dict[str, Any]]
A dictionary in which the keys are individual symbols and the values are dictionaries
of their respective SymPy keyword-argument ``assumptions``.
The value of the ``symbols`` property of all states in ``inputs`` and gates in ``gates`` are automatically merged into the instance's corresponding ``symbols`` property.
Defaults to ``{}``.
conditions : list[tuple[num | sym | str, num | sym | str]]
A list of :math:`2`-tuples of conditions to be applied to all objects (such as states and
gates) computed from the circuit.
All instances of the expression in each tuple's first element are replaced by the
expression in the respective second element.
This uses the same format as the SymPy ``subs()`` method.
The order in which they are applied is simply their order in the list.
The value of the ``conditions`` property of all states in ``inputs`` and gates in ``gates``
are automatically merged into the instance's corresponding ``conditions`` property.
Defaults to ``[]``.
Note
----
All states, gates, postselections, and measurement operators recorded in the instance must
share the same dimensionality (i.e., the value of the ``dim`` property).
Note
----
The sum of the ``num_systems`` properties of the quantum states in ``inputs`` should match
that of each of the gates in ``gates``.
"""
def __init__(
self,
inputs: list[QuantumState] | None = None,
gates: list[QuantumGate] | None = None,
traces: list[int] | None = None,
postselections: (
list[tuple[mat | arr | QuantumObject, int | list[int]]] | None
) = None,
symbols: dict[sym | str, dict[str, Any]] | None = None,
conditions: list[tuple[num | sym | str, num | sym | str]] | None = None,
):
SymbolicsProperties.__init__(self, symbols=symbols, conditions=conditions)
inputs = [] if inputs is None else inputs
gates = [] if gates is None else gates
postselections = [] if postselections is None else postselections
traces = [] if traces is None else traces
self.inputs = inputs
self.gates = gates
self._postselections = postselections
self._traces = traces
self.postselections = postselections
self.traces = traces
def __repr__(self) -> str:
return repr(self.output())
@property
def inputs(self) -> list[QuantumState]:
"""An ordered list of :py:class:`~qhronology.quantum.states.QuantumState` instances.
The total input state is the tensor product of these individual states in the order
in which they appear in the list.
Each state's ``symbols`` and ``conditions`` properties are merged into their counterparts
in the instance upon their addition to the ``gates`` property.
"""
return self._inputs
@inputs.setter
def inputs(self, inputs: list[QuantumState]):
inputs = flatten_list([copy.deepcopy(inputs)])
for state in inputs:
self.conditions += list(state.conditions)
self.symbols |= dict(state.symbols)
self._inputs = inputs
@property
def gates(self) -> list[QuantumGate]:
"""An ordered list of :py:class:`~qhronology.quantum.gates.QuantumGate` instances.
The total gate is the product of these individual gates in the order in which they appear
in the list.
Must all have the same ``num_systems`` property.
Each gate's ``symbols`` and ``conditions`` properties are merged into their counterparts
in the instance upon their addition to the ``gates`` property.
"""
return self._gates
@gates.setter
def gates(self, gates: list[QuantumGate]):
gates = flatten_list([copy.deepcopy(gates)])
for gate in gates:
self.conditions += list(gate.conditions)
self.symbols |= dict(gate.symbols)
self._gates = gates
@property
def postselections(self) -> list[tuple[mat | arr | QuantumObject, int | list[int]]]:
"""A list of 2-tuples of vectors or matrix operators paired with the first (smallest)
index of their postselection target systems.
Any ``symbols`` and ``conditions`` properties of each postselection are merged into their
counterparts in the instance upon their addition to the ``postselections`` property.
"""
return self._postselections
@postselections.setter
def postselections(
self, postselections: list[tuple[mat | arr | QuantumObject, int | list[int]]]
):
if len(postselections) > 0:
systems_postselections = []
for postselection in postselections:
length = count_systems(extract_matrix(postselection[0]), self.dim)
listed = flatten_list([postselection[1]])
systems = [(min(listed) + n) for n in range(0, length)]
if len(listed) > 1:
if set(systems) != set(listed):
raise ValueError(
"Mismatch between the postselection's specified targets and its \
calculated size."
)
systems_postselections += systems
systems_postselections = list(set(systems_postselections))
for k in systems_postselections:
if k not in self.systems:
raise ValueError(
"At least one of the postselection's target systems does not exist."
)
check_systems_conflicts(
self.systems_traces, systems_postselections, self.systems_postselections
)
for operator, targets in postselections:
if hasattr(operator, "_conditions") is True:
self.conditions += list(operator.conditions)
if hasattr(operator, "_symbols") is True:
self.symbols |= dict(operator.symbols)
self._postselections = postselections
@property
def traces(self) -> list[int]:
"""The numerical indices of the subsystems to be traced over."""
return self._traces
@traces.setter
def traces(self, traces: list[int]):
systems_traces = flatten_list([traces])
for k in systems_traces:
if k not in self.systems:
raise ValueError(
"At least one of the partial trace target systems does not exist."
)
check_systems_conflicts(
systems_traces, self.systems_traces, self.systems_postselections
)
self._traces = traces
@property
def systems_traces(self) -> list[int]:
"""The indices of the systems to be traced over."""
return self.traces
@property
def systems_postselections(self) -> list[int]:
"""The indices of the systems to be postselected."""
systems_postselections = []
for postselection in self.postselections:
length = count_systems(extract_matrix(postselection[0]), self.dim)
listed = flatten_list([postselection[1]])
systems = [(min(listed) + n) for n in range(0, length)]
systems_postselections += systems
return list(set(systems_postselections))
@property
def systems_removed(self) -> list[int]:
"""The indices of all of the systems targeted by the ``traces`` and ``postselections``
properties."""
return flatten_list(self.systems_traces + self.systems_postselections)
@property
def num_systems_inputs(self) -> int:
"""The total number of systems spanned by the circuit's input states."""
num_systems_inputs = 0
for state in self.inputs:
num_systems_inputs += state.num_systems
return num_systems_inputs
@property
def num_systems_gates(self) -> int:
"""The total number of systems spanned by the circuit's gates."""
num_systems_gates = []
for gate in self.gates:
num_systems_gates.append(gate.num_systems)
if len(num_systems_gates) > 0:
if len(set(num_systems_gates)) != 1:
raise ValueError(
"One or more of the gates in the circuit has mismatching ``num_systems``."
)
else:
num_systems_gates = [0]
return num_systems_gates[0]
@property
def num_systems_gross(self) -> int:
"""The total number of systems spanned by the circuit's states and gates prior to any
system reduction (post-processing, i.e., traces and postselections])."""
num_systems_gross = max([self.num_systems_inputs] + [self.num_systems_gates])
return num_systems_gross
@property
def num_systems_net(self) -> int:
"""The total number of systems spanned by the circuit's states and gates after any
system reduction (post-processing, i.e., traces and postselections])."""
num_systems_net = self.num_systems_gross - len(self.systems_removed)
return num_systems_net
@property
def num_systems(self) -> int:
"""Alias for ``num_systems_gross``."""
return self.num_systems_gross
@property
def num_systems_removed(self) -> int:
"""The total number of systems removed via system reduction (post-processing,
i.e., traces and postselections])."""
return len(self.systems_removed)
@property
def systems(self) -> list[int]:
"""An ordered list of the numerical indices of the circuit's systems."""
return [k for k in range(0, self.num_systems)]
@property
def dim(self) -> int:
"""The dimensionality of the circuit.
Calculated from its states and gates, and so all must have the same value."""
dim = None
dim_input = None
if self.inputs != []:
dims_input = []
for state in self.inputs:
dims_input.append(state.dim)
dim_input = list(set(dims_input))
if len(dim_input) != 1:
raise ValueError(
"One or more of the input states has mismatching dimensionality."
)
dim_input = dim_input[0]
dim = dim_input
dim_gate = None
if self.gates != []:
dims_gates = []
for gate in self.gates:
dims_gates.append(gate.dim)
dim_gate = list(set(dims_gates))
if len(dim_gate) != 1:
raise ValueError(
"One or more of the gates has mismatching dimensionality."
)
dim_gate = dim_gate[0]
dim = dim_gate
if dim_input is not None and dim_gate is not None:
if dim_input != dim_gate:
raise ValueError(
"One or more of the gates has dimensionality different to that of the \
input state(s)."
)
return dim
@property
def input_is_vector(self) -> bool:
"""Whether all states in ``inputs`` are vector states."""
is_vector = True
if any(
form != Forms.VECTOR.value for form in [state.form for state in self.inputs]
):
is_vector = False
if self.num_systems - self.num_systems_inputs != 0:
is_vector = False
return is_vector
@property
def gate_is_linear(self) -> bool:
"""Whether all gates are linear (i.e., not measurement operations)."""
is_linear = True
if any(
family == Families.METER.value
for family in [gate.family for gate in self.gates]
):
is_linear = False
return is_linear
@property
def post_is_vector(self) -> bool:
"""Whether any traces or non-vector postselections exist in the circuit's post-processing
(trace and postselection) stage."""
is_vector = True
if len(self.systems_traces) != 0:
is_vector = False
if len(self.systems_postselections) != 0:
for postselection in self.postselections:
if matrix_form(extract_matrix(postselection[0])) != Forms.VECTOR.value:
is_vector = False
return is_vector
@property
def output_is_vector(self) -> bool:
"""Whether or not the output from the entire circuit is a vector state."""
is_vector = True
if (
not any(
not boolean
for boolean in [
self.input_is_vector,
self.gate_is_linear,
self.post_is_vector,
]
)
is False
):
is_vector = False
return is_vector
[docs]
def gate(
self,
conditions: list[tuple[num | sym | str, num | sym | str]] | None = None,
simplify: bool | None = None,
conjugate: bool | None = None,
exponent: num | sym | str | None = None,
label: str | None = None,
notation: str | None = None,
) -> QuantumGate:
"""Construct the combined gate describing the total sequence of gates in the quantum circuit
as a :py:class:`~qhronology.quantum.gates.QuantumGate` instance and return it.
This is computed as the matrix product of the individual gates in the reverse order
in which they appear in the ``gates`` property.
Arguments
---------
conditions : list[tuple[num | sym | str, num | sym | str]]
Algebraic conditions to be applied to the gate.
Defaults to the value of ``self.conditions``.
simplify : bool
Whether to perform algebraic simplification on the gate.
Defaults to ``False``.
conjugate : bool
Whether to perform Hermitian conjugation on the gate when it is called.
Defaults to ``False``.
exponent : num | sym | str
A numerical or string representation of a scalar value to which gate's operator
(residing on ``targets``) is exponentiated.
Must be a non-negative integer.
Defaults to ``1``.
label : str
The unformatted string used to represent the gate in mathematical expressions.
Defaults to ``"U"``.
notation : str
The formatted string used to represent the gate in mathematical expressions.
When not ``None``, overrides the value passed to ``label``.
Not intended to be set by the user in most cases.
Defaults to ``None``.
Returns
-------
mat
The matrix or vector representation of the total gate sequence.
Note
----
This construction excludes measurement gates as they do not have a corresponding
matrix representation.
"""
spec = sp.eye(self.dim**self.num_systems_gross)
for gate in self.gates:
gate = copy.deepcopy(gate)
gate.num_systems = self.num_systems_gross
spec = gate.output() * spec
spec = symbolize_expression(spec, self.symbols_list)
# Conditions
conditions = self.conditions if conditions is None else conditions
conditions = symbolize_tuples(conditions, self.symbols_list)
spec = spec.subs(conditions)
# Simplification
simplify = False if simplify is None else simplify
if simplify is True:
spec = recursively_simplify(spec, conditions)
gate_total = QuantumGate(
spec=spec,
targets=self.systems,
controls=[],
anticontrols=[],
num_systems=self.num_systems,
dim=self.dim,
symbols=self.symbols,
conditions=conditions,
conjugate=conjugate,
exponent=exponent,
coefficient=1,
label=label,
notation=notation,
)
return gate_total
@property
def matrix(self) -> mat:
"""The matrix representation of the total output state prior to any post-processing
(i.e., traces and postselections)."""
input_state = sp.Matrix(self.input().output())
if self.gate_is_linear is True:
gate_total = sp.Matrix(self.gate().output())
if self.input_is_vector is True:
output_state = gate_total * input_state
else:
output_state = gate_total * input_state * Dagger(gate_total)
else: # Gate in nonlinear and nonunitary so destroys any vector purity.
output_state = densify(input_state)
for gate in self.gates:
gate = copy.deepcopy(gate)
gate.num_systems = self.num_systems_gross
if hasattr(gate, "_observable") is True:
pre_measurement_state = output_state
post_measurement_state = sp.zeros(self.dim**self.num_systems_gross)
if gate.observable is False:
for matrix_measurement_operator in gate.matrices:
post_measurement_state += (
densify(matrix_measurement_operator)
* pre_measurement_state
* Dagger(densify(matrix_measurement_operator))
)
else:
for matrix_measurement_operator in gate.matrices:
post_measurement_state += sp.trace(
densify(matrix_measurement_operator)
* pre_measurement_state
) * densify(matrix_measurement_operator)
output_state = post_measurement_state
else:
gate_matrix = sp.Matrix(gate.output(conditions=[]))
output_state = gate_matrix * output_state * Dagger(gate_matrix)
return output_state
[docs]
def output(
self,
conditions: list[tuple[num | sym | str, num | sym | str]] | None = None,
simplify: bool | None = None,
conjugate: bool | None = None,
postprocess: bool | None = None,
) -> mat:
"""Compute the matrix representation of the total output state of the circuit
(including any post-processing, i.e., traces and postselections) and return it.
Arguments
---------
conditions : list[tuple[num | sym | str, num | sym | str]]
Algebraic conditions to be applied to the state.
Defaults to the value of ``self.conditions``.
simplify : bool
Whether to perform algebraic simplification on the state.
Defaults to ``False``.
conjugate : bool
Whether to perform Hermitian conjugation on the state.
Defaults to ``False``.
postprocess : bool
Whether to post-process the state
(i.e., perform the circuit's traces and postselections).
Defaults to ``True``.
Returns
-------
mat
The matrix representation of the (post-processed) output state.
"""
conditions = self.conditions if conditions is None else conditions
output_state = self.matrix
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
if self.input_is_vector is True:
form = Forms.VECTOR.value
kind = Kinds.PURE.value
if self.gate_is_linear is False:
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
output_state = QuantumState(
spec=output_state,
form=form,
kind=kind,
symbols=self.symbols,
dim=self.dim,
conditions=conditions,
norm=False,
conjugate=False,
label=None,
notation=None,
debug=False,
)
postprocess = True if postprocess is None else postprocess
if postprocess is True:
systems_removed = []
# Partial traces
traces = adjust_targets(self.systems_traces, systems_removed)
output_state.partial_trace(targets=traces)
systems_removed += traces
# Postselections
for postselection in self.postselections:
length = count_systems(extract_matrix(postselection[0]), self.dim)
listed = flatten_list([postselection[1]])
systems = [(min(listed) + n) for n in range(0, length)]
targets_postselection = adjust_targets(systems, systems_removed)
output_state.postselect(
postselections=[(postselection[0], targets_postselection)]
)
systems_removed += systems
if self.post_is_vector is False:
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
# Simplification
simplify = False if simplify is None else simplify
if simplify is True:
output_state.simplify()
# Conjugation
conjugate = False if conjugate is None else conjugate
if conjugate is True:
output_state.dagger()
output_state = QuantumState(
spec=output_state.output(),
form=form,
kind=kind,
symbols=self.symbols,
dim=self.dim,
conditions=conditions,
norm=False,
conjugate=False,
label=None,
notation=None,
debug=False,
)
return sp.Matrix(output_state.output())
[docs]
def state(
self,
conditions: list[tuple[num | sym | str, num | sym | str]] | None = None,
simplify: bool | None = None,
conjugate: bool | None = None,
norm: bool | num | sym | str | None = None,
label: str | None = None,
notation: str | None = None,
traces: list[int] | None = None,
postprocess: bool | None = None,
debug: bool | None = None,
) -> QuantumState:
"""Compute the total output state of the circuit (including any post-processing,
i.e., traces and postselections) as a :py:class:`~qhronology.quantum.states.QuantumState`
instance and return it.
Arguments
---------
conditions : list[tuple[num | sym | str, num | sym | str]]
Algebraic conditions to be applied to the state.
Defaults to the value of ``self.conditions``.
simplify : bool
Whether to perform algebraic simplification on the state before committing it to the
``matrix`` property.
Defaults to ``False``.
conjugate : bool
Whether to perform Hermitian conjugation on the state.
Defaults to ``False``.
norm : bool | num | sym | str
The value to which the state is normalized.
If ``True``, normalizes to a value of :math:`1`.
If ``False``, does not normalize.
Defaults to ``False``.
label : str
The unformatted string used to represent the state in mathematical expressions.
Must have a non-zero length.
Defaults to ``"ρ"`` (if ``form == "matrix"``) or ``"ψ"`` (if ``form == "vector"``).
notation : str
The formatted string used to represent the state in mathematical expressions.
When not ``None``, overrides the value passed to ``label``.
Must have a non-zero length.
Not intended to be set by the user in most cases.
Defaults to ``None``.
traces : list[int]
A list of indices of the systems (relative to the entire circuit) on which to perform
partial traces.
Performed regardless of the value of ``postprocess``.
Defaults to ``[]``.
postprocess : bool
Whether to post-process the state
(i.e., perform the circuit's traces and postselections).
Defaults to ``True``.
debug : bool
Whether to print the internal state (held in ``matrix``) on change.
Defaults to ``False``.
Returns
-------
QuantumState
The (post-processed) output state as a
:py:class:`~qhronology.quantum.states.QuantumState` instance.
"""
conditions = self.conditions if conditions is None else conditions
simplify = False if simplify is None else simplify
traces = [] if traces is None else traces
postprocess = True if postprocess is None else postprocess
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
if self.input_is_vector is True:
form = Forms.VECTOR.value
kind = Kinds.PURE.value
if self.gate_is_linear is False:
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
if postprocess is True:
traces = list(set(traces) - set(self.systems_removed))
traces = adjust_targets(traces, self.systems_removed)
if self.post_is_vector is False:
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
if len(traces) != 0:
form = Forms.MATRIX.value
kind = Kinds.MIXED.value
matrix = sp.Matrix(
self.output(
conditions=conditions,
simplify=simplify,
conjugate=False,
postprocess=postprocess,
)
)
state = QuantumState(
form=form,
kind=kind,
spec=matrix,
symbols=self.symbols,
dim=self.dim,
conditions=conditions,
conjugate=conjugate,
norm=norm,
label=label,
notation=notation,
debug=debug,
)
state.partial_trace(targets=traces)
return state
[docs]
def measure(
self,
operators: list[mat | arr | QuantumObject],
targets: int | list[int] | None = None,
observable: bool | None = None,
statistics: bool | None = None,
) -> QuantumState | list[num | sym]:
"""Perform a quantum measurement on one or more systems (indicated in ``targets``)
of the circuit's total output state.
This occurs prior to any post-processing (i.e., traces and postselections).
This method has two main modes of operation:
- When ``statistics`` is ``True``, the (reduced) state (:math:`\\op{\\rho}`)
(residing on the systems indicated in ``targets``) is measured and the set of resulting
statistics is returned. This takes the form of an ordered list of values
:math:`\\{p_i\\}_i` associated with each given operator, where:
- :math:`p_i = \\trace[\\Kraus_i^\\dagger \\Kraus_i \\op{\\rho}]`
(measurement probabilities) when ``observable`` is ``False``
(``operators`` is a list of Kraus operators or projectors :math:`\\Kraus_i`)
- :math:`p_i = \\trace[\\Observable_i \\op{\\rho}]`
(expectation values) when ``observable`` is ``True``
(``operators`` is a list of observables :math:`\\Observable_i`)
- When ``statistics`` is ``False``, the (reduced) state (:math:`\\op{\\rho}`)
(residing on the systems indicated in ``targets``) is measured and mutated it according
to its predicted post-measurement form (i.e., the sum of all possible measurement
outcomes). This yields the transformed states:
- When ``observable`` is ``False``:
.. math:: \\op{\\rho}^\\prime = \\sum_i \\Kraus_i \\op{\\rho} \\Kraus_i^\\dagger.
- When ``observable`` is ``True``:
.. math::
\\op{\\rho}^\\prime
= \\sum_i \\trace[\\Observable_i \\op{\\rho}] \\Observable_i.
In the case where ``operators`` contains only a single item (:math:`\\Kraus`),
and the current state (:math:`\\ket{\\psi}`) is a vector form,
the transformation of the state is in accordance with the rule
.. math::
\\ket{\\psi^\\prime} = \\frac{\\Kraus \\ket{\\psi}}
{\\sqrt{\\bra{\\psi} \\Kraus^\\dagger \\Kraus \\ket{\\psi}}}
when ``observable`` is ``False``. In all other mutation cases, the post-measurement state
is a matrix, even if the pre-measurement state was a vector.
The items in the list ``operators`` can also be vectors (e.g., :math:`\\ket{\\xi_i}`),
in which case each is converted into its corresponding operator matrix representation
(e.g., :math:`\\ket{\\xi_i}\\bra{\\xi_i}`) prior to any measurements.
Arguments
---------
operators : list[mat | arr | QuantumObject]
The operator(s) with which to perform the measurement.
These would typically be a (complete) set of Kraus operators forming a POVM,
a (complete) set of (orthogonal) projectors forming a PVM,
or a set of observables constituting a complete basis for the relevant state space.
targets : int | list[int]
The numerical indices of the system(s) to be measured.
They must be consecutive, and their number must match the number of systems spanned
by all given operators.
Indexing begins at ``0``.
All other systems are discarded (traced over) in the course of performing the
measurement.
observable : bool
Whether to treat the items in ``operators`` as observables
(as opposed to Kraus operators or projectors).
Defaults to ``False``.
statistics : bool
Whether to return a list of probabilities (``True``) or mutate the state into
a post-measurement probabilistic sum of all outcomes (``False``).
Defaults to ``False``.
Returns
-------
list[num | sym]
A list of probabilities corresponding to each operator given in ``operators``.
Returned only if ``statistics`` is ``True``.
QuantumState
A quantum state that takes the form of the post-measurement probabilistic sum
of all outcomes of measurements corresponding to each operator given in ``operators``.
Returned only if ``statistics`` is ``False``.
"""
statistics = False if statistics is None else statistics
state = self.state(postprocess=False)
if statistics is True:
state = state.measure(
operators=operators,
targets=targets,
observable=observable,
statistics=statistics,
)
else:
state.measure(
operators=operators,
targets=targets,
observable=observable,
statistics=statistics,
)
return state
[docs]
def diagram(
self,
pad: tuple[int, int] | None = None,
sep: tuple[int, int] | None = None,
uniform_spacing: bool | None = None,
force_separation: bool | None = None,
style: str | None = None,
return_string: bool | None = None,
) -> None | str:
"""Print or return a diagram of the quantum circuit as a multiline string.
Arguments
---------
pad : tuple[int, int]
A two-tuple describing the horizontal and vertical interior paddings between the content
at the centre of each gate (e.g., label) and its outer edge (e.g., block border).
Both integers must be non-negative.
Defaults to ``(0, 0)``.
sep : tuple[int, int]
A two-tuple describing the horizontal and vertical exterior separation distances
between the edges of neighbouring gates.
Both integers must be non-negative.
Defaults to ``(1, 1)``.
uniform_spacing : bool
Whether to uniformly space the gates horizontally such that the midpoint of each
is equidistant from those of its neighbours.
Defaults to ``False``.
force_separation : bool
Whether to force the horizontal gate separation to be exactly the value given
in ``sep`` for all gates in the circuit.
When not ``False``, the value of ``uniform_spacing`` is ignored.
Defaults to ``False``.
style : str
A string specifying the style for the circuit visualization to take.
Can be any of ``"ascii"``, ``"unicode"``, or ``"unicode_alt"``.
Defaults to ``"unicode"``.
return_string : bool
Whether to return the assembled diagram as a multiline string.
Defaults to ``False``.
Returns
-------
None
Returned only if ``return_string`` is ``False``.
str
The rendered circuit diagram. Returned only if ``return_string`` is ``True``.
Note
----
The quality of the visualization depends greatly on the output's configuration.
For best results, the terminal should have a monospace font with good Unicode coverage.
"""
pad = (0, 0) if pad is None else pad
sep = (1, 1) if sep is None else sep
if isinstance(sep, tuple) is True:
sep = {"upper": sep[1], "lower": sep[1], "left": sep[0], "right": sep[0]}
style = Styles.UNICODE.value if style is None else style
uniform_spacing = False if uniform_spacing is None else uniform_spacing
force_separation = False if force_separation is None else force_separation
return_string = False if return_string is None else return_string
cells_input = []
if hasattr(self, "_systems_respecting") is True:
for system in self.systems:
if system == min(self.systems_respecting):
for state in self.inputs:
cells_input.append(
[*state.diagram_column(pad=pad, sep=sep, style=style).cells]
)
else:
if system in self.systems_violating:
cells_input.append(
[
_Single(family=Families.WORMHOLE.value + "_PAST")
.diagram_column(pad=pad, sep=sep, style=style)
.cells
]
)
else:
for state in self.inputs:
cells_input.append(
[*state.diagram_column(pad=pad, sep=sep, style=style).cells]
)
if self.num_systems_inputs != 0:
for _ in range(0, self.num_systems - self.num_systems_inputs):
identity = QuantumState(
form=Forms.MATRIX.value,
kind=Kinds.MIXED.value,
spec=sp.eye(self.dim),
symbols=dict(),
dim=self.dim,
conditions=[],
norm=1,
conjugate=False,
label="I",
notation=None,
debug=False,
)
cells_input.append(
[identity.diagram_column(pad=pad, sep=sep, style=style).cells]
)
column_input = []
if self.num_systems_inputs != 0:
column_input = DiagramColumn(
cells=flatten_list(cells_input),
pad=(2, 0),
section=Sections.INPUTS.value,
)
columns_gate = []
for index_column, gate in enumerate(self.gates):
gate = copy.deepcopy(gate)
gate.num_systems = self.num_systems_gross
columns_gate.append(gate.diagram_column(pad=pad, sep=sep, style=style))
cells_output = []
for system in self.systems:
if (
hasattr(self, "_systems_respecting") is True
and system in self.systems_violating
):
cells_output.append(
[
_Single(family=Families.WORMHOLE.value + "_FUTURE")
.diagram_column(pad=pad, sep=sep, style=style)
.cells
]
)
elif system in self.systems_traces:
cells_output.append(
[
_Single(family=Families.TRACE.value)
.diagram_column(pad=pad, sep=sep, style=style)
.cells
]
)
elif system in self.systems_postselections:
for postselection in self.postselections:
if system == min(flatten_list([postselection[1]])):
if hasattr(postselection[0], "_family") is True:
Postselection = postselection[0]
Postselection.family = Families.RSTICK.value
if (
matrix_shape(Postselection.matrix)
== Shapes.COLUMN.value
):
Postselection.dagger()
else:
length = count_systems(
extract_matrix(postselection[0]), self.dim
)
Postselection = QuantumState(
form=Forms.MATRIX.value,
kind=Kinds.MIXED.value,
spec=sp.eye(self.dim**length),
symbols=dict(),
dim=self.dim,
conditions=[],
norm=1,
conjugate=False,
label="?",
notation=None,
family=Families.RSTICK.value,
debug=False,
)
cells_output.append(
[
Postselection.diagram_column(
pad=pad, sep=sep, style=style
).cells
]
)
else:
cells_output.append(
[
_Single(family=Families.TERM.value)
.diagram_column(pad=pad, sep=sep, style=style)
.cells
]
)
column_output = []
if len(self.inputs) != 0:
if len(self.gates) != 0 or self.num_systems_removed != 0:
column_output = DiagramColumn(
cells=flatten_list(cells_output),
pad=(2, 0),
section=Sections.OUTPUTS.value,
)
grid = DiagramCircuit(
columns=flatten_list([column_input, [*columns_gate], column_output])
)
if return_string is True:
return grid.diagram(
pad=pad,
sep=sep,
style=style,
uniform_spacing=uniform_spacing,
force_separation=force_separation,
return_string=True,
)
else:
grid.diagram(
pad=pad,
sep=sep,
style=style,
uniform_spacing=uniform_spacing,
force_separation=force_separation,
return_string=False,
)