# ********************************************************************************
# * Copyright (c) 2026 the Qrisp authors
# *
# * This program and the accompanying materials are made available under the
# * terms of the Eclipse Public License 2.0 which is available at
# * http://www.eclipse.org/legal/epl-2.0.
# *
# * This Source Code may also be made available under the following Secondary
# * Licenses when the conditions for such availability set forth in the Eclipse
# * Public License, v. 2.0 are satisfied: GNU General Public License, version 2
# * with the GNU Classpath Exception which is
# * available at https://www.gnu.org/software/classpath/license.html.
# *
# * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
# ********************************************************************************
"""Defines the QuantumVariable class, the quantum analogue of a classical variable."""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING, Any, ClassVar
from weakref import ReferenceType
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
from jax import tree_util
from qrisp.circuit.pass_management.passes.combine_single_qubit_gates import combine_single_qubit_gates
from qrisp.core.compilation import qompiler
from qrisp.misc import app_phase_function, bin_rep, check_if_fresh, custom_qv, get_measurement_from_qc, int_encoder
from qrisp.misc.utility import redirect_qfunction
from qrisp.permeability import uncompute
if TYPE_CHECKING:
from qrisp.core.quantum_session import QuantumSession
from qrisp.interface.measurement_result import DecodedMeasurementResult
from qrisp.jasp import TracingQuantumSession
[docs]
class QuantumVariable:
"""
The quantum analogue of a regular variable in classical programming languages.
All :ref:`quantum types <QuantumTypes>` inherit from this
class. The QuantumVariable allows many automizations and quality of life
improvements such as hidden qubit management, de/encoding to human readable labels
or typing.
Each QuantumVariable is registered in a :ref:`QuantumSession`. It can be accessed
using the ``.qs`` attribute:
>>> from qrisp import QuantumVariable
>>> example_qv = QuantumVariable(3)
>>> quantum_session = example_qv.qs
The qubits of the QuantumVariable are stored as a list in the ``.reg`` attribute
>>> qubits = example_qv.reg
To quickly access the qubits of a given variable, we use the [ ] operator:
>>> qubit_2 = example_qv[2]
We can find out about the amount of qubits in the QuantumVariable with the ``.size``
attribute
>>> example_qv.size
3
**Naming**
QuantumVariables can be given names to identify them independently of their naming
as Python objects.
>>> example_qv_2 = QuantumVariable(3, name = "alice")
>>> example_qv_2.name
'alice'
If not explicitely specified during construction, a name is determined
automatically. Qrisp will try to infer the name of the Python variable and if that
fails, a generic name is given.
>>> example_qv.name
'example_qv'
In order to keep the generated quantum circuits comprehensive, the qubits are named
after their containing QuantumVariable with an extra number, which indicates their
index.
>>> from qrisp import cx
>>> cx(example_qv, example_qv_2)
>>> print(example_qv.qs)
.. code-block:: none
QuantumCircuit:
--------------
example_qv.0: ──■────────────
│
example_qv.1: ──┼────■───────
│ │
example_qv.2: ──┼────┼────■──
┌─┴─┐ │ │
alice.0: ┤ X ├──┼────┼──
└───┘┌─┴─┐ │
alice.1: ─────┤ X ├──┼──
└───┘┌─┴─┐
alice.2: ──────────┤ X ├
└───┘
Live QuantumVariables:
---------------------
QuantumVariable example_qv
QuantumVariable alice
QuantumSessions can only contain uniquely named QuantumVariables. If two
QuantumSessions are :ref:`merged <SessionMerging>` containing identically named
QuantumVariables, the more recently created QuantumVariable will be renamed:
::
from qrisp import QuantumFloat
s = QuantumFloat(5)
for i in range(4):
temp = QuantumFloat(4)
temp[:] = 2**i
s += temp
>>> print(s.qs)
.. code-block:: none
QuantumCircuit:
--------------
┌───────────┐┌───────────┐┌───────────┐┌───────────┐
s.0: ─────┤0 ├┤0 ├┤0 ├┤0 ├
│ ││ ││ ││ │
s.1: ─────┤1 ├┤1 ├┤1 ├┤1 ├
│ ││ ││ ││ │
s.2: ─────┤2 ├┤2 ├┤2 ├┤2 ├
│ ││ ││ ││ │
s.3: ─────┤3 ├┤3 ├┤3 ├┤3 ├
│ ││ ││ ││ │
s.4: ─────┤4 __iadd__ ├┤4 ├┤4 ├┤4 ├
┌───┐│ ││ ││ ││ │
temp.0: ┤ X ├┤5 ├┤ ├┤ ├┤ ├
└───┘│ ││ ││ ││ │
temp.1: ─────┤6 ├┤ __iadd__ ├┤ ├┤ ├
│ ││ ││ ││ │
temp.2: ─────┤7 ├┤ ├┤ ├┤ ├
│ ││ ││ ││ │
temp.3: ─────┤8 ├┤ ├┤ __iadd__ ├┤ ├
└───────────┘│ ││ ││ │
temp_1.0: ──────────────────┤5 ├┤ ├┤ ├
┌───┐ │ ││ ││ │
temp_1.1: ┤ X ├─────────────┤6 ├┤ ├┤ __iadd__ ├
└───┘ │ ││ ││ │
temp_1.2: ──────────────────┤7 ├┤ ├┤ ├
│ ││ ││ │
temp_1.3: ──────────────────┤8 ├┤ ├┤ ├
└───────────┘│ ││ │
temp_2.0: ───────────────────────────────┤5 ├┤ ├
│ ││ │
temp_2.1: ───────────────────────────────┤6 ├┤ ├
┌───┐ │ ││ │
temp_2.2: ┤ X ├──────────────────────────┤7 ├┤ ├
└───┘ │ ││ │
temp_2.3: ───────────────────────────────┤8 ├┤ ├
└───────────┘│ │
temp_3.0: ────────────────────────────────────────────┤5 ├
│ │
temp_3.1: ────────────────────────────────────────────┤6 ├
│ │
temp_3.2: ────────────────────────────────────────────┤7 ├
┌───┐ │ │
temp_3.3: ┤ X ├───────────────────────────────────────┤8 ├
└───┘ └───────────┘
Live QuantumVariables:
---------------------
QuantumFloat s
QuantumFloat temp
QuantumFloat temp_1
QuantumFloat temp_2
QuantumFloat temp_3
Renaming does not happen for names given through the ``name`` keyword, unless the
name ends with a ``*``.
>>> example_qv_3 = QuantumVariable(3, name = "alice")
>>> cx(example_qv, example_qv_3)
Exception: Tried to merge QuantumSession containing identically named
QuantumVariables
>>> example_qv_4 = QuantumVariable(3, name = "alice*")
>>> cx(example_qv, example_qv_4)
>>> example_qv_4.name
'alice_1'
Examples
--------
Writing a function that brings an arbitrary QuantumVariable into a GHZ state
::
from qrisp import QuantumVariable, h, cx
def GHZ(qv):
h(qv[0])
for i in range(1, qv.size):
cx(qv[0], qv[i])
Evaluation:
>>> qv = QuantumVariable(5)
>>> GHZ(qv)
>>> print(qv)
{'00000': 0.5, '11111': 0.5}
"""
name: str
# Whether `name` is fixed and can't be renamed, mainly for static
# QuantumSessions.
# See `generate_name` `resolve_naming_collisions` in session_merging_tools.py.
is_fixed_name: bool
reg: Any # pyright: ignore[reportExplicitAny, reportUninitializedInstanceVariable]
creation_time: int # pyright: ignore[reportUninitializedInstanceVariable]
live_qvs: ClassVar[list[ReferenceType[QuantumVariable]]] = []
creation_counter: ClassVar[int] = 0
name_tracker: ClassVar[dict[str, int]] = {}
qs: QuantumSession | TracingQuantumSession
static_attributes: list[str]
traced_attributes: list[str]
[docs]
def __init__(self, size: int, qs: QuantumSession | None = None, name: str | None = None):
r"""
Constructs a QuantumVariable.
Constructs a QuantumVariable, possibly with a given name or in a given
QuantumSession.
Parameters
----------
size : int
The amount of qubits this QuantumVariable contains.
qs : QuantumSession, optional
A QuantumSession object, where the QuantumVariable is supposed to be
registered. The default is None.
name : string, optional
A name which uniquely identifies the QuantumVariable. If ended with a
\*, name is allowed to be updated if a naming collision arises. By default,
Qrisp will try to infer the name of the Python variable - otherwise a
generic name is given.
Raises
------
QuantumVariableNamingError
Name of qv is already used in this QuantumSession.
"""
# The following lists are used to indicate to the
# (un)flattening mechanism of Jax which attributes
# of the QuantumVariable should be considered static
# and which are dynamic.
# For instance, in the case of QuantumFloat,
# the exponent is dynamic and the signed boolean is
# static.
# For reference check:
# https://docs.jax.dev/en/latest/custom_pytrees.html
# Specify the traced attributes (empty for base type QuantumVariable)
self.traced_attributes = []
# Specify the static attributes (empty for base type QuantumVariable)
self.static_attributes = []
# Imported locally to avoid a circular import: qrisp.core.quantum_session
# imports QuantumVariable from this module at load time.
from qrisp.core.quantum_session import QuantumSession
# Imported locally to avoid a circular import: qrisp.jasp
# imports QuantumVariable from this module at load time.
from qrisp.jasp import TracingQuantumSession, check_for_tracing_mode
# Store quantum session
self.qs = TracingQuantumSession.get_instance() if check_for_tracing_mode() else qs or QuantumSession()
declaration_stack_level = 1 if type(self) is QuantumVariable else 2
(self.name, self.is_fixed_name) = self.qs.generate_name(name, self, declaration_stack_level)
self.qs.register_qv(self, size)
# Imported locally to avoid a circular import: qrisp.jasp.tracing_logic
# imports QuantumVariable from this module at load time.
from qrisp.jasp.tracing_logic import flatten_qv, unflatten_qv
# This attribute tracks the created QuantumVariables for the
# auto_uncompute decorator
# We use weak references as some qrisp modules rely on reference counting
try:
# Register as a PyTree with JAX
tree_util.register_pytree_node(type(self), flatten_qv, unflatten_qv)
except ValueError:
pass
def __or__(self, other):
# Imported locally: the qrisp package imports this module before defining these names.
from qrisp import cx, mcx, x
if len(self) > len(other):
or_res = self.duplicate()
else:
or_res = other.duplicate()
for i in range(min(len(self), len(other))):
mcx([self[i], other[i]], or_res[i], ctrl_state=0)
x(or_res[i])
for i in range(min(len(self), len(other)), len(self)):
cx(self[i], or_res[i])
for i in range(min(len(self), len(other)), len(other)):
cx(other[i], or_res[i])
return or_res
def __and__(self, other):
# Imported locally: the qrisp package imports this module before defining these names.
from qrisp import mcx
if len(self) > len(other):
and_res = self.duplicate()
else:
and_res = other.duplicate()
for i in range(min(len(self), len(other))):
mcx([self[i], other[i]], and_res[i])
return and_res
def __xor__(self, other):
# Imported locally: the qrisp package imports this module before defining these names.
from qrisp import cx
if len(self) > len(other):
and_res = self.duplicate()
else:
and_res = other.duplicate()
for i in range(min(len(self), len(other))):
cx(self[i], and_res[i])
cx(other[i], and_res[i])
return and_res
def __lshift__(self, other):
if not callable(other):
raise Exception("Tried to inject QuantumVariable into non-callable")
def return_function(*args, **kwargs):
return redirect_qfunction(other)(*args, target=self, **kwargs)
return return_function
[docs]
def delete(self, verify=False, recompute=False):
r"""This method is for deleting a QuantumVariable and thus freeing up and resetting
the used qubits.
Note that this method has a different function than the destructor. Calling this
method will tell the QuantumSession to mark the used qubits as free and apply a
reset gate.
If set to True, the keyword verify will cause a simulation to check, wether the
deleted qubits are in the $\ket{0}$ state prior to resetting. This is helpfull
during debugging, as it indicates wether the uncomputation of this
QuantumVariable was successfull.
After deletion, the QuantumVariable object is basically unchanged but an error
will be raised if further operations on the deleted qubits are attempted.
Parameters
----------
verify : bool, optional
If this bool is set to True, Qrisp will verify that the deleted qubits are
indeed in the $\ket{0}$ state. The default is ``False``.
recompute : bool, optional
If set to ``True``, this QuantumVariable can be recomputed if it is required
for the uncomputation of another QuantumVariable. For more information on
the (dis)advantages check :ref:`recomputation <recomputation>`. The default
is ``False``.
Raises
------
Exception
Tried to delete qubits not in \|0> state.
Examples
--------
We create a QuantumVariable, execute some gates and try to delete with
verify = True
>>> from qrisp import QuantumVariable, x, h
>>> qv = QuantumVariable(2)
>>> x(qv[0])
>>> h(qv[1])
>>> qv.delete(verify = True)
Exception: Tried to delete qubits not in |0> state.
We now (manually) uncompute the gates
>>> x(qv[0])
>>> h(qv[1])
>>> qv.delete(verify = True)
>>> qv.is_deleted()
True
>>> x(qv[0])
Exception: Tried to perform operation x on unallocated qubit qv_1.0.
"""
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import TracingQuantumSession
if not isinstance(self.qs, TracingQuantumSession) and self.is_deleted():
return
self.qs.delete_qv(self, verify)
i = 0
while i < len(QuantumVariable.live_qvs):
live_qv = QuantumVariable.live_qvs[i]()
if live_qv is None:
del QuantumVariable.live_qvs[i]
continue
# creation_time is a unique identifier across sessions.
if live_qv.creation_time == self.creation_time:
del QuantumVariable.live_qvs[i]
break
i += 1
if recompute:
for qb in self.reg:
qb.recompute = True
def is_deleted(self):
for qb in self.reg:
if not qb.allocated:
return True
return False
[docs]
def duplicate(self, name: str | None = None, qs=None, init=False, qubits=None):
r"""
Duplicate a QuantumVariable.
Duplicates the QuantumVariable in the sense that a new QuantumVariable is
created with same type and parameters but initialized in the $\ket{0}$ state.
Parameters
----------
name : string, optional
A unique name to identify that QuantumVariable. If not given, a name will be
generated.
qs : QuantumSession, optional
A QuantumSession, where the result should be registered. If not given, a new
QuantumSession will be generated.
init : bool, optional
If set to True, the :meth:`init_from <qrisp.QuantumVariable.init_from>`
method of the result will be called on self. The default is False.
qubits : list[Qubit] | DynamicQubitArray, optional
Qubits to initialize the duplicated quantum variable from.
If not provided ``self.size`` is used for qubits extraction.
The object is used as the duplicate's register as is (not copied), so passing
another QuantumVariable's ``reg`` makes both share it: deleting, extending
or reducing the duplicate also affects that QuantumVariable.
Raises
------
Exception
If merging the duplicate's quantum session with ``qs`` fails.
TracingModeError
If ``init`` is True while tracing, since initializing from another
QuantumVariable is not supported in a ``TracingQuantumSession``.
Exception
If ``init`` is True and the provided ``qubits`` are not fresh.
Returns
-------
duplicate : Type of self
The duplicated QuantumVariable.
Examples
--------
We create a QuantumFloat and duplicate:
>>> from qrisp import QuantumFloat
>>> qf_0 = QuantumFloat(4, signed = False)
>>> qf_1 = qf_0.duplicate()
>>> type(qf_1)
qrisp.qtypes.quantum_float.QuantumFloat
>>> qf_1.size
4
"""
# Imported locally: qrisp.core.quantum_session and qrisp.jasp import QuantumVariable from this module.
from qrisp.core.quantum_session import QuantumSession
from qrisp.jasp import TracingQuantumSession, check_for_tracing_mode
new_qs = TracingQuantumSession.get_instance() if check_for_tracing_mode() else QuantumSession()
duplicate = copy.copy(self)
if qubits is not None:
duplicate.reg = qubits
size = None
else:
size = self.size
# Set name of duplicate variable.
# The name passed is never None, so no code introspection happens and
# the declaration stack level is irrelevant.
(duplicate.name, duplicate.is_fixed_name) = new_qs.generate_name(
name if name is not None else self.name,
duplicate,
declaration_stack_level=0,
is_duplicated_name=name is None,
)
# Register duplicate variable in session.
new_qs.register_qv(duplicate, size)
# Imported locally: the qrisp package imports this module before defining these names.
from qrisp import merge
duplicate.qs = new_qs
if qs is not None and isinstance(qs, QuantumSession):
merge(qs, new_qs)
if init:
duplicate.init_from(self)
return duplicate
[docs]
def decoder(self, i):
"""The decoder method specifies how a QuantumVariable turns the outcomes of
measurements into human-readable values. It recieves an integer ``i`` and
returns a human-readable value.
This method is supposed to be overloaded when defining new
:ref:`quantum types <QuantumTypes>`.
Parameters
----------
i : int
Integer representing the outcome of a measurement of the qubits of this
QuantumVariable.
Returns
-------
A human-readable value. Has to be hashable.
Examples
--------
We create a QuantumFloat and inspect its decoder:
>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(3, -1, signed = False)
>>> print(qf.decoder(1))
0.5
This implies that if the 3 qubits of this QuantumFloat are measured in state
001, this outcome corresponds to the value 0.5.
"""
return bin_rep(i, self.size)[::-1]
def jdecoder(self, i):
return i
[docs]
def encoder(self, value):
"""The encoder reverses the decoder, it turns human-readable values into integers.
If not overloaded, the encoder will perform a linear search on decoder inputs to
match the given value.
Parameters
----------
label :
A human-readable value.
Raises
------
Exception
Unknown input value.
Returns
-------
i : int
The integer encoding the given value.
Examples
--------
We create a QuantumChar and inspect it's encoder:
>>> from qrisp import QuantumChar
>>> q_ch = QuantumChar()
>>> print(q_ch.encoder("f"))
5
This implies that if the 5 qubits of this QuantumChar are measured to
``5 = 00101``, the out come will be displayed as f.
"""
for i in range(2**self.size):
if self.decoder(i) == value:
return i
raise Exception("Value " + str(value) + " not supported by encoder.")
[docs]
def encode(self, value, permit_dirtyness=False):
"""The encode method allows to quickly bring a QuantumVariable in a desired
computational basis state.
A shorthand for this method is given by the ``[:]`` operator.
Note that the qubits to initialize have to be fresh (i.e. no operations
performed on them).
Parameters
----------
value :
A value supported by the encoder.
permit_dirtyness : bool, optional
Surpresses the error message when calling encode on dirty qubits.
Returns
-------
None.
Examples
--------
We create two quantum floats and encode the value 2.5. For one of them, we
perform an x gate onto the corresponding qubits, resulting in an error.
>>> from qrisp import QuantumFloat, x
>>> qf_0 = QuantumFloat(3, -1, signed = False)
>>> qf_1 = QuantumFloat(3, -1, signed = False)
>>> x(qf_0)
>>> qf_0.encode(2.5)
Exception: Tried to initialize qubits which are not fresh anymore.
>>> qf_1[:] = 2.5
>>> print(qf_1)
{2.5: 1.0}
"""
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import TracingQuantumSession
if not isinstance(self.qs, TracingQuantumSession):
if not permit_dirtyness:
if not check_if_fresh(self.reg, self.qs):
raise Exception("Tried to initialize qubits which are not fresh anymore.")
int_encoder(self, self.encoder(value))
[docs]
def init_state(self, params, method="auto"):
r"""Initialize an arbitrary quantum state on this quantum variable.
This method supports two input formats:
**1. Dictionary input**
A dictionary ``{value: amplitude}`` describing the (possibly
non-normalized) wavefunction in the logical basis of the quantum
variable. Any value not listed is assigned amplitude zero.
**2. Explicit statevector input**
A flat vector of complex amplitudes of length :math:`2^{n}`, where
:math:`n` is the number of qubits. The vector is automatically
normalized. The little-endian convention is assumed for indexing
basis states. See :ref:`prepare <prepare>` for details.
.. note::
In Jasp mode, Python-based shape and checks on the norm of the
statevector are skipped to avoid tracing side effects.
Parameters
----------
params : dict or array-like
Either a dictionary ``{value: amplitude}`` or a length
:math:`2^{n}` complex statevector.
method : {"auto", "qiskit", "qswitch"}, optional
Select the state-preparation backend.
The possible options are:
- ``"auto"`` (default):
Use the Qiskit-based initializer outside Jasp mode and fall back
to the internal ``qswitch`` initializer in Jasp mode.
- ``"qiskit"``:
Force the Qiskit state-preparation circuit. Not available in
Jasp mode.
- ``"qswitch"``:
Use the :ref:`q_switch <q_switch>`-based implementation, which
is compatible with Jasp mode.
Defaults to ``"auto"``.
Examples
--------
**Dictionary input**
We create a ``QuantumFloat`` and encode the state
.. math::
\ket{\psi} = \sqrt{\tfrac{1}{3}}\,\ket{0.5}
+ i\sqrt{\tfrac{2}{3}}\,\ket{2.0}
>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(3, -1)
>>> qf.init_state({0.5: (1/3)**0.5, 2.0: 1j*(2/3)**0.5})
or equivalently:
>>> qf[:] = {0.5: (1/3)**0.5, 2.0: 1j*(2/3)**0.5}
**Explicit statevector input**
In this example, we create a :ref:`QuantumFloat` and prepare the normalized state
$\sum_{i=0}^3 \tilde b_i\ket{i}$ for $\tilde b=(0,1,2,3)/\sqrt{14}$.
>>> import numpy as np
>>> b = np.array([0, 1, 2, 3], dtype=float)
>>> b /= np.linalg.norm(b)
>>> qf = QuantumFloat(2)
>>> qf.init_state(b)
We can use the ``statevector`` method to get a function that maps basis states
to amplitudes to verify the prepared state:
>>> sv_function = qf.qs.statevector("function")
>>> print(f"b[1]: {b[1]:.6f} -> {sv_function({qf: 1}):.6f}")
b[1]: 0.267261 -> 0.267261-0.000000j
>>> print(f"b[2]: {b[2]:.6f} -> {sv_function({qf: 2}):.6f}")
b[2]: 0.534522 -> 0.534522-0.000000j
where index 1 in little-endian corresponds to the basis state :math:`\ket{q_0=1, q_1=0}`
and index 2 to :math:`\ket{q_0=0, q_1=1}`.
**Forcing a backend**
We can also explicitly choose the state-preparation backend:
>>> qf.init_state(psi, method="qswitch") # Always allowed
>>> qf.init_state(psi, method="qiskit") # Only outside Jasp mode
After initialization, the amplitudes can be inspected
using ``qf.qs.statevector("function")`` as above.
"""
# Imports here to avoid circular dependencies
from qrisp.alg_primitives.state_preparation import (
prepare,
)
if isinstance(params, dict):
target_array = np.zeros(1 << self.size, dtype=np.complex128)
for key, amp in params.items():
target_array[self.encoder(key)] = amp
else:
# Use JAX array to allow tracing; convert later if needed
target_array = jnp.asarray(params, dtype=jnp.complex128)
prepare(self, target_array, False, method=method)
def append(self, operation):
self.qs.append(operation, self)
[docs]
def extend(self, amount, position=-1):
"""This method is used to add more qubits to the QuantumVariable. Using the
position keyword it is possible to specify the position where the qubits should
be added. By default, the qubits are added at the end.
Parameters
----------
amount : int
The amount of qubits to add.
position : int, optional
The position of where to add the qubits. By default, qubits are added at the
end.
st of qubits which are to be added to the QuantumVariable.
The default is None.
Raises
------
Exception
Missmatch between proposed qubits and amount integer.
Returns
-------
None.
Examples
--------
We create a QuantumVariable and extend it with some extra qubits.
>>> from qrisp import QuantumVariable
>>> qv = QuantumVariable(3)
>>> print(qv.reg)
[Qubit(qv.0), Qubit(qv.1), Qubit(qv.2)]
>>> qv.extend(3)
>>> print(qv.reg)
[Qubit(qv.0), Qubit(qv.1), Qubit(qv.2), Qubit(qv.6), Qubit(qv.6), Qubit(qv.6)]
"""
insertion_qubits = self.qs.request_qubits(amount)
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import check_for_tracing_mode
if check_for_tracing_mode():
if isinstance(position, int) and position in [0, -1]:
if position == -1:
self.reg = self.reg + insertion_qubits
elif position == 0:
self.reg = insertion_qubits + self.reg
else:
self.reg = self.reg[:position] + insertion_qubits + self.reg[position:]
else:
if position == -1:
position = self.size
for i in range(amount):
insertion_qubits[i].identifier = ( # pyright: ignore
self.name + "_ext_" + str(self.qs.qubit_index_counter[0]) + "." + str(self.size) # pyright: ignore
)
self.reg.insert(position + i, insertion_qubits[i])
[docs]
def reduce(self, qubits: Any, verify=False):
r"""Reduces the qubit count of the QuantumVariable by removing a specified set of
qubits.
Parameters
----------
qubits : list
The qubits to remove from the QuantumVariable.
verify : bool
Boolean value which indicates wether Qrisp should verify that the reduced
qubits are in the $\ket{0}$ state.
Raises
------
Exception
Qubits not present in QuantumVariable.
Exception
Verification that the given qubits are in $\ket{0}$ state failed.
Examples
--------
We create a QuantumVariable with 5 qubits and remove the first 2
>>> from qrisp import QuantumVariable
>>> qv = QuantumVariable(5)
>>> print(qv.reg)
[Qubit(qv.0), Qubit(qv.1), Qubit(qv.2), Qubit(qv.3), Qubit(qv.4)]
>>> qv.reduce(qv[:2])
>>> print(qv.reg)
[Qubit(qv.2), Qubit(qv.3), Qubit(qv.4)]
"""
try:
len(qubits)
except TypeError:
qubits = [qubits]
if not set(qubits).issubset(self.reg):
raise Exception("Tried to reduce QuantumVariable by invalid qubits")
# Find Qubits to be cleared
for i in range(len(qubits)):
for j in range(self.size):
if self.reg[j] == qubits[i]:
self.reg[j].identifier = "reduced_" + str(self.qs.qubit_index_counter[0]) # pyright: ignore
self.qs.qubit_index_counter += 1 # pyright: ignore
self.reg.pop(j)
break
self.qs.clear_qubits(qubits, verify)
# Adjust variable size
[docs]
def get_measurement(
self,
plot=False,
backend=None,
shots=None,
compile=True,
compilation_kwargs={},
subs_dic={},
circuit_preprocessor=None,
filename=None,
precompiled_qc=None,
) -> "DecodedMeasurementResult":
r"""
Get the measurement results of the state of the variable.
Method for quick access to the measurement results of the state of the variable.
Returns a :class:`~qrisp.interface.DecodedMeasurementResult`, which behaves like
a dictionary of the type ``{value: p}`` where ``p`` is the measurement probability.
Parameters
----------
plot : Bool, optional
Plots the measurement results as a historgram. The default is False.
backend : BackendLike, optional
The backend on which to evaluate the quantum circuit.
The default can be specified in the file default_backend.py.
shots : integer, optional
The amount of shots to evaluate the circuit. The default is given by the backend it runs on.
compile : bool, optional
Boolean indicating if the .compile method of the underlying QuantumSession
should be called before. The default is True.
compilation_kwargs : dict, optional
Keyword arguments for the compile method. For more details check
:meth:`QuantumSession.compile <qrisp.QuantumSession.compile>`. The default
is ``{}``.
subs_dic : dict, optional
A dictionary of Sympy symbols and floats to specify parameters in the case
of a circuit with unspecified, :ref:`abstract parameters<QuantumCircuit>`.
The default is {}.
circuit_preprocessor : Python function, optional
A function which recieves a QuantumCircuit and returns one, which is applied
after compilation and parameter substitution. The default is None.
filename : string, optional
The location of where to save a generated plot. The default is None.
precompiled_qc : QuantumCircuit, optional
A circuit that has already been compiled from the containing
QuantumSession. If given, it is used (as a copy) instead of compiling
the session, and ``compile`` and ``compilation_kwargs`` are ignored.
Parameter binding and ``circuit_preprocessor`` are still applied. The
circuit must contain the qubits of this QuantumVariable. The default
is None.
Raises
------
Exception
If the containing QuantumSession is in a quantum environment, it is not
possible to execute measurements.
TracingModeError
If the QuantumVariable is registered in a ``TracingQuantumSession`` (i.e. while tracing),
measurements are not supported.
Returns
-------
DecodedMeasurementResult
A lazy :class:`~qrisp.interface.DecodedMeasurementResult` mapping each
decoded value to its measurement probability.
Examples
--------
We create an integer :ref:`QuantumFloat`, encode the value 1 and bring the qubit
with significance 2 in superposition. We utilize the Qiskit transpiler by
transpiling into the gate set $\{\text{CX}, \text{U}\}$
>>> from qrisp import QuantumFloat, h
>>> qf = QuantumFloat(3,-1)
>>> qf[:] = 1
>>> h(qf[2])
>>> mes_results = qf.get_measurement(circuit_preprocessor=lambda qc: qc.transpile(basis_gates=["cx", "u"]))
>>> print(mes_results)
{1.0: 0.5, 3.0: 0.5}
"""
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import TracingModeError, TracingQuantumSession
if isinstance(self.qs, TracingQuantumSession):
raise TracingModeError("Tried to get measurement of a QuantumVariable in tracing mode")
if backend is None:
if self.qs.backend is None:
# Imported locally: qrisp.default_backend imports from qrisp.core, which is still loading this module.
from qrisp.default_backend import def_backend
backend = def_backend
else:
backend = self.qs.backend
if len(self.qs.env_stack) != 0:
raise Exception("Tried to get measurement within open environment")
if self.is_deleted():
raise Exception("Tried to get measurement from deleted QuantumVariable")
if self.size == 0:
# TODO: Create a const DecodedMeasurmentResult that describes this construct.
return {"": 1.0} # pyright: ignore[reportReturnType]
if precompiled_qc is None:
if compile:
qc = qompiler(self.qs, intended_measurements=self.reg, **compilation_kwargs)
else:
qc = self.qs.copy()
else:
qc = precompiled_qc.copy()
# Bind parameters
if subs_dic:
qc = qc.bind_parameters(subs_dic)
qc = combine_single_qubit_gates(qc)
# Copy circuit in over to prevent modification
# from qrisp.quantum_network import QuantumNetworkClient
# if isinstance(backend, QuantumNetworkClient):
# self.qs.data = []
# shots = 1
# Execute user specified circuit_preprocessor
if circuit_preprocessor is not None:
qc = circuit_preprocessor(qc)
# qc = qc.transpile()
# Imported locally: qrisp.interface.measurement_result imports from qrisp.core,
# which is still loading this module.
from qrisp.interface.measurement_result import DecodedMeasurementResult
counts = get_measurement_from_qc(qc, self.reg, backend, shots)
result = DecodedMeasurementResult(counts, self.decoder)
if plot:
outcome_labels = []
for i in range(2**self.size):
temp = self.decoder(i)
try:
hash(temp)
except TypeError:
raise Exception("Outcome value " + str(self.decoder(i)) + " is not hashable")
outcome_labels.append(temp)
plot_histogram(outcome_labels, dict(result), filename)
plt.show()
return result
[docs]
def most_likely(self, **kwargs):
"""Performs a measurement and returns the most likely outcome.
Parameters
----------
**kwargs : Keyword arguments for the get_measurement call.
Examples
--------
>>> from qrisp import QuantumFloat, ry
>>> import numpy as np
>>> qf = QuantumFloat(3)
>>> ry(np.pi*9/8, qf[0])
>>> print(qf)
{1: 0.9619, 0: 0.0381}
>>> qf.most_likely()
1
"""
return list(self.get_measurement())[0]
def __getitem__(self, key):
return self.reg[key]
def __str__(self):
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import check_for_tracing_mode
if check_for_tracing_mode():
return self.__repr__()
else:
return str(self.get_measurement())
def __repr__(self):
return "<" + str(type(self)).split(".")[-1][:-2] + " '" + self.name + "'>"
return str(type(self)).split(".")[-1][:-2] + "(name = " + self.name + ")"
return str(self)
def __del__(self):
i = 0
while i < len(self.live_qvs):
if self.live_qvs[i]() is None or id(self) == id(self.live_qvs[i]()):
self.live_qvs.pop(i)
continue
i += 1
def __len__(self):
return self.size
@property
def size(self):
if isinstance(self.reg, list):
return len(self.reg)
else:
return self.reg.size
# Overload equality operator to use python syntax for if environments?
# Not sure if the possible user confusion is worth it
def __eq__(self, other): # pyright: ignore[reportIncompatibleMethodOverride]
r"""
Compare self with another QuantumVariable or a classical label against equality (==), returning a QuantumBool.
Parameters
----------
other : QuantumVariable
A QuantumVariable of the same size, or a classical label of this
QuantumVariable's type, to compare self with.
Returns
-------
QuantumBool
The result of the comparison.
Raises
------
Exception
Tried to evaluate the comparison for QuantumVariables of differing size.
Exception
The classical comparand is not a valid label of this QuantumVariable.
Examples
--------
Comparing to another QuantumVariable:
>>> from qrisp import QuantumChar
>>> qch_a = QuantumChar()
>>> qch_b = QuantumChar()
>>> qch_a[:] = "b"
>>> qch_b[:] = "c"
>>> print(qch_a == qch_b)
{False: 1.0}
Comparing a QuantumVariable in the $\ket{+}$ state to a classical label. Both
outcomes have equal weight, so the result is True and False in equal parts:
>>> from qrisp import QuantumVariable, h
>>> qv = QuantumVariable(1)
>>> h(qv[0])
>>> print(qv == "0")
{False: 0.5, True: 0.5}
"""
# Imported locally: qrisp.environments imports QuantumVariable from this module.
from qrisp.environments import q_eq
return q_eq(self, other)
def __ne__(self, other): # pyright: ignore[reportIncompatibleMethodOverride]
r"""
Compare self with another QuantumVariable or a classical label against inequality (!=), returning a QuantumBool.
Parameters
----------
other : QuantumVariable
A QuantumVariable of the same size, or a classical label of this
QuantumVariable's type, to compare self with.
Returns
-------
QuantumBool
The result of the comparison.
Raises
------
Exception
Tried to evaluate the comparison for QuantumVariables of differing size.
Exception
The classical comparand is not a valid label of this QuantumVariable.
Examples
--------
Comparing to another QuantumVariable:
>>> from qrisp import QuantumChar
>>> qch_a = QuantumChar()
>>> qch_b = QuantumChar()
>>> qch_a[:] = "b"
>>> qch_b[:] = "c"
>>> print(qch_a != qch_b)
{True: 1.0}
Comparing a QuantumVariable in the $\ket{+}$ state to a classical label. Both
outcomes have equal weight, so the result is True and False in equal parts:
>>> from qrisp import QuantumVariable, h
>>> qv = QuantumVariable(1)
>>> h(qv[0])
>>> print(qv != "0")
{False: 0.5, True: 0.5}
"""
# Imported locally: qrisp.environments imports QuantumVariable from this module.
from qrisp.environments import q_eq
return q_eq(self, other, invert=True)
def __hash__(self):
return self.creation_time
def __setitem__(self, key, value):
if key != slice(None, None, None):
raise Exception("Tried to encode value into QuantumVariable using non-trivial slicing.")
if isinstance(type(value), type(None)):
return
if isinstance(value, dict):
self.init_state(value)
return
if isinstance(value, QuantumVariable):
self.init_from(value)
return
self.encode(value)
[docs]
def app_phase_function(self, phi):
r"""Applies a previously specified phase function to each computational basis state
of the QuantumVariable using Gray-Synthesis.
For a given phase function $\phi(x)$ and a QuantumVariable in state
$\ket{\psi} = \sum_{x \in \text{Labels}} a_x \ket{x}$ this method acts as:
.. math::
U_{\phi} \sum_{x \in \text{Labels}} a_x \ket{x} =
\sum_{x \in \text{Labels}} \text{exp}(i\phi(x)) a_x \ket{x}
Parameters
----------
phi : Python function
A Python function which turns the labels of the QuantumVariable into floats.
Examples
--------
We create a QuantumFloat and encode the k-th basis state of the Fourier basis.
Finally, we will apply an inverse Fourier transformation to measure k in the
computational basis.
>>> import numpy as np
>>> from qrisp import QuantumFloat, h, QFT
>>> n = 5
>>> qf = QuantumFloat(n, signed = False)
>>> h(qf)
After this, qf is in the state
.. math::
\ket{\text{qf}} = \frac{1}{\sqrt{2^n}} \sum_{x = 0}^{2^n} \ket{x}
We specify phi
>>> k = 4
>>> def phi(x):
>>> return 2*np.pi*x*k/2**n
And apply phi as a phase function
>>> qf.app_phase_function(phi)
qf is now in the state
.. math::
\ket{\text{qf}} = \frac{1}{\sqrt{2^n}} \sum_{x = 0}^{2^n}
\text{exp}\left( \frac{2\pi ikx}{2^n}\right) \ket{x}
Finally we apply the inverse Fourier transformation and measure:
>>> QFT(qf, inv = True)
>>> print(qf)
{4: 1.0}
"""
app_phase_function([self], phi)
[docs]
def uncompute(self, do_it=True, recompute=False):
"""Method for automatic uncomputation.
Uses a generalized form of
`this algorithm <https://dl.acm.org/doi/10.1145/3453483.3454040>`_ to perform uncomputation automatically.
For more information check the
:ref:`uncomputation documentation<uncomputation>`.
Parameters
----------
do_it : bool, optional
If set to False, this variable will be appended to the uncomputation stack
of it's QuantumSession and uncomputed once an uncompute call with
``do_it = True`` is performed. The default is True.
recompute : bool, optional
If set to True, this QuantumVariable will be uncomputed but temporarily
recomputed, if it is required for the uncomputation of another
QuantumVariable. For more information check
:ref:`recomputation <recomputation>`. The default is False.
Raises
------
TracingModeError
If the QuantumVariable is registered in a ``TracingQuantumSession`` (i.e. while tracing),
uncomputation is not supported.
Examples
--------
We create two QuantumVariables, apply some gates and perform automatic
uncomputation:
>>> from qrisp import QuantumVariable, x, cx, h, p, mcx
>>> a = QuantumVariable(3)
>>> b = QuantumVariable(2)
>>> mcx(a, b[0])
>>> h(a[:2])
>>> x(b[0])
>>> cx(b[0], b[1])
>>> p(0.5, b[1])
>>> print(a.qs)
.. code-block:: none
QuantumCircuit:
--------------
┌───┐
a.0: ──■──┤ H ├───────────────
│ ├───┤
a.1: ──■──┤ H ├───────────────
│ └───┘
a.2: ──■──────────────────────
┌─┴─┐┌───┐
b.0: ┤ X ├┤ X ├──■────────────
└───┘└───┘┌─┴─┐┌────────┐
b.1: ──────────┤ X ├┤ P(0.5) ├
└───┘└────────┘
Live QuantumVariables:
---------------------
QuantumVariable a
QuantumVariable b
>>> b.uncompute()
>>> print(b.qs)
.. code-block:: none
QuantumCircuit:
--------------
┌────────┐ ┌────────┐┌───┐
a.0: ┤0 ├──────────────────────────────┤0 ├┤ H ├
│ │ │ │├───┤
a.1: ┤1 ├──────────────────────────────┤1 ├┤ H ├
│ pt3cx │ │ pt3cx │└───┘
a.2: ┤2 ├──────────────────────────────┤2 ├─────
│ │┌───┐ ┌───┐│ │
b.0: ┤3 ├┤ X ├──■──────────────■──┤ X ├┤3 ├─────
└────────┘└───┘┌─┴─┐┌────────┐┌─┴─┐└───┘└────────┘
b.1: ───────────────┤ X ├┤ P(0.5) ├┤ X ├────────────────────
└───┘└────────┘└───┘
Live QuantumVariables:
---------------------
QuantumVariable a
"""
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import TracingModeError, TracingQuantumSession
if isinstance(self.qs, TracingQuantumSession):
raise TracingModeError("Tried to uncompute a QuantumVariable in tracing mode")
if self.is_deleted():
raise Exception("Tried to uncompute deleted QuantumVariable")
if do_it:
uncompute(self.qs, self.qs.uncomp_stack + [self], recompute)
self.qs.uncomp_stack = []
else:
self.qs.uncomp_stack.append(self)
def get_unique_name(self, name=None):
if name is None:
# Imported locally: the qrisp package imports this module before defining these names.
from qrisp import QuantumBool, QuantumChar, QuantumFloat
if isinstance(self, QuantumBool):
name = "qbl"
elif isinstance(self, QuantumFloat):
name = "qf"
elif isinstance(self, QuantumChar):
name = "qch"
else:
name = "qv"
while True:
try:
naming_number = QuantumVariable.name_tracker[name]
QuantumVariable.name_tracker[name] += 1
name = name + "_" + str(naming_number)
except KeyError:
QuantumVariable.name_tracker[name] = 1
name = name + "_0"
i = 0
while i < len(QuantumVariable.live_qvs):
qv = QuantumVariable.live_qvs[i]()
if qv is None:
QuantumVariable.live_qvs.pop(i)
continue
if qv.name == name:
break
i += 1
else:
break
return name
def __iter__(self):
if not isinstance(self.reg, list):
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import TracingModeError
raise TracingModeError("Tried to perform a static iteration on a dynamic QuantumVariable")
else:
return self.reg.__iter__()
[docs]
def init_from(self, other):
r"""Initializes a QuantumVariable based on the state of another.
It does NOT copy the state. Much rather it performs the operation
.. math::
U_{\text{init_from}} \left( \sum_{x \in \text{labels}} a_x \ket{x}
\right) \ket{0} = \sum_{x \in \text{labels}} a_x \ket{x} \ket{x}
This is different from a state copying operation:
.. math::
U_{\text{copy}} \left( \sum_{x \in \text{labels}} a_x \ket{x} \right)
\ket{0} = \left( \sum_{x \in \text{labels}} a_x \ket{x} \right)
\left( \sum_{x \in \text{labels}} a_x \ket{x} \right)
A shorthand for initiating this way is the ``[:]`` operator.
Parameters
----------
other : QuantumVariable
The QuantumVariable from which to initiate.
Raises
------
Exception
Tried to initialize qubits which are not fresh anymore.
TracingModeError
If the QuantumVariable is registered in a ``TracingQuantumSession`` (i.e. while tracing),
initializing from another QuantumVariable is not supported.
Examples
--------
We create a QuantumFloat, and bring it into superposition.
>>> from qrisp import QuantumFloat, h, multi_measurement
>>> qf_a = QuantumFloat(8)
>>> qf_a[:] = 6
>>> h(qf_a[0])
>>> print(qf_a)
{6: 0.5, 7: 0.5}
We now duplicate and initiate the duplicate
>>> qf_b = qf_a.duplicate()
>>> print(qf_b)
{0: 1.0}
>>> qf_b.init_from(qf_a)
>>> print(multi_measurement([qf_a, qf_b]))
{(6, 6): 0.5, (7, 7): 0.5}
The slicing operator achieves the same:
>>> qf_c = qf_a.duplicate()
>>> qf_c[:] = qf_b
>>> print(multi_measurement([qf_a, qf_b, qf_c]))
{(6, 6, 6): 0.5, (7, 7, 7): 0.5}
"""
if not type(self) == type(other):
raise Exception("Tried to initialize " + str(type(self)) + " from " + str(type(other)))
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp import TracingModeError, TracingQuantumSession
if isinstance(self.qs, TracingQuantumSession):
raise TracingModeError("Tried to initialize a QuantumVariable from another in tracing mode")
if not check_if_fresh(self.reg, self.qs):
raise Exception("Tried to initialize qubits which are not fresh anymore")
self.qs.cx(other.reg, self.reg)
[docs]
@classmethod
def custom(cls, label_list, decoder=None, qs=None, name=None):
"""Creates a QuantumVariable with customized outcome labels.
Note that this is a class method, implying there is no need to create another
QuantumVariable first to call this method.
Parameters
----------
label_list : list
A list of outcome labels.
decoder : function, optional
The decoder function. If given none, the labels will be encoded according to
their placement in the ``label_list``.
qs : QuantumSession, optional
The :ref:`QuantumSession` in which to register the customized
QuantumVariable. If given none, the QuantumVariable will be registered in a
new QuantumSession.
name : string, optional
The name of the QuantumVariable. If given none, a suited name will be
generated.
Returns
-------
CustomQuantumVariable
A QuantumVariable with the desired outcome labels.
Examples
--------
We create a QuantumVariable with some examples values as outcome labels and
bring it into uniform superposition.
>>> from qrisp import QuantumVariable, h
>>> qv = QuantumVariable.custom(["lorem", "ipsum", "dolor", "sit", 42, (1,2,3)])
>>> h(qv)
>>> print(qv)
{'lorem': 0.125, 'ipsum': 0.125, 'dolor': 0.125, 'sit': 0.125, 42: 0.125,
(1, 2, 3): 0.125, 'undefined_label_6': 0.125, 'undefined_label_7': 0.125}
"""
return custom_qv(label_list, decoder=decoder, qs=qs, name=name)
def ensure_reg(self):
return self.reg
def measure(self):
return self.jdecoder(self.reg.measure())
def template(self):
# Imported locally: qrisp.jasp imports QuantumVariable from this module.
from qrisp.jasp.tracing_logic import QuantumVariableTemplate
return QuantumVariableTemplate(self)
def plot_histogram(outcome_labels, counts, filename=None):
res_list = []
for k in range(len(outcome_labels)):
try:
res_list.append(counts[outcome_labels[k]])
except KeyError:
res_list.append(0)
plt.bar(outcome_labels, res_list, width=0.8)
plt.grid()
plt.ylabel("Measurement probability")
plt.xlabel("QuantumVariable value")
if filename:
plt.savefig(filename, dpi=400, bbox_inches="tight")
else:
plt.show()