Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions documentation/source/general/changelog/changelog-dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ Improvements
no longer replayed event by event in Python. The returned dictionary is
unchanged
(`PR #917 <https://github.com/eclipse-qrisp/Qrisp/pull/917>`_).
- Refactored quantum_variable.py by adding more typing, explicit attributes and delegating name
generation to QuantumSession in a central place.
(`PR #882 <https://github.com/eclipse-qrisp/Qrisp/pull/882>`_).

- Fewer qubits for :func:`reflection <qrisp.reflection>`:
:func:`reflection <qrisp.reflection>` no longer bundles its operations into a
Expand Down Expand Up @@ -424,6 +427,40 @@ Bug Fixes
without qubits
(`PR #975 <https://github.com/eclipse-qrisp/Qrisp/pull/975>`_).

* Fixed a bug in ``resolve_naming_collisions`` where in the "qv_0 is newer" conditional branch,
``qv_1.is_fixed_name`` (called ``user_given_name`` earlier) was checked
when it should have been ``qv_0``'s.
As a result, merging two sessions could rename a variable with
an explicitly given (fixed) name. Now a fixed name is never renamed.
(`PR #882 <https://github.com/eclipse-qrisp/Qrisp/pull/882>`_).

* Fixed a bug where calls of :meth:`~qrisp.QuantumVariable.duplicate` of the form ``duplicate(name="foo*")``
caused the variable being duplicated to change.
There, ``self.is_fixed_name`` (called ``user_given_name`` earlier) was set to ``False``, instead of the duplicate's ``is_fixed_name``.
This is fixed now.
(`PR #882 <https://github.com/eclipse-qrisp/Qrisp/pull/882>`_).

* Fixed a bug where a :ref:`QuantumVariable <QuantumVariable>` created at the top level of a
plain Python script (e.g. ``qv = QuantumVariable(2)``) was not named after its Python variable,
but received a generic name such as ``qv_0`` instead. ``find_calling_line`` picked the wrong
stack frame whenever the declaring line was the bottom of the call stack. Inside functions,
pytest or Jupyter the inferred name was already correct; at module level, scripts now get the
inferred name too, which changes the names shown e.g. in printed circuits.
(`PR #882 <https://github.com/eclipse-qrisp/Qrisp/pull/882>`_).

* Fixed the name inferred for a :ref:`QuantumVariable <QuantumVariable>` declared with a type
annotation or through tuple unpacking. ``x: QuantumFloat = QuantumFloat(3)`` was named ``x:``
and ``a, b = QuantumFloat(2), QuantumFloat(2)`` named its first variable ``a,``; they are now
named ``x`` and ``a`` respectively.
(`PR #882 <https://github.com/eclipse-qrisp/Qrisp/pull/882>`_).

* Fixed a bug where deleting a :ref:`QuantumVariable <QuantumVariable>` could remove a different
variable with the same name, living in another QuantumSession, from ``QuantumVariable.live_qvs``.
Names are only unique within a single QuantumSession, so variables are now matched by their
creation time instead. Since the ``auto_uncompute`` decorator uses ``live_qvs`` to find the
variables a function created, it could previously miss such a variable.
(`PR #882 <https://github.com/eclipse-qrisp/Qrisp/pull/882>`_).

Compatibility
-------------

Expand Down
2 changes: 1 addition & 1 deletion documentation/source/reference/Jasp/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@ If you are interested in how the QIR representation looks like, try calling
MLIR Interface
Optimization Tools/Optimization Tools
Scalable Integer Type
Ported Features
Ported Features
2 changes: 1 addition & 1 deletion src/qrisp/alg_primitives/mcx_algs/jones.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def jones_toffoli(ctrl, target, ctrl_state="11"):
jones_ancilla = QuantumVariable(
2,
qs=ctrl[0].qs(),
name="jones_ancilla_" + str(int(QuantumVariable.creation_counter[0])),
name="jones_ancilla_" + str(QuantumVariable.creation_counter),
)

target.qs().append(JonesToffoli(ctrl_state), list(ctrl) + [target] + list(jones_ancilla))
Expand Down
9 changes: 9 additions & 0 deletions src/qrisp/circuit/clbit.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@

"""Defines the Clbit class representing classical bits in a QuantumCircuit."""

from __future__ import annotations

from typing import TYPE_CHECKING
from weakref import ReferenceType

import numpy as np

if TYPE_CHECKING:
from qrisp.core.quantum_session import QuantumSession


class Clbit:
"""This class represents classical bits. Classical bits are created by supplying the
Expand All @@ -39,6 +47,7 @@ class Clbit:

"""

qs: ReferenceType[QuantumSession] | None = None
dtype = np.dtype("bool")
clbit_hash = np.zeros(1)

Expand Down
10 changes: 7 additions & 3 deletions src/qrisp/core/quantum_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from qrisp.core import QuantumSession, QuantumVariable, merge, qompiler
from qrisp.jasp import (
DynamicQubitArray,
TracingModeError,
TracingQuantumSession,
check_for_tracing_mode,
create_qubits,
Expand Down Expand Up @@ -616,8 +617,9 @@ def get_measurement(
circuit_preprocessor=None,
precompiled_qc=None,
):
"""Method for acquiring measurement results for the given array. The semantics are
similar to the :meth:`get_measurement <qrisp.QuantumVariable.get_measurement>`
"""Method for acquiring measurement results for the given array.

The semantics are similar to the :meth:`get_measurement <qrisp.QuantumVariable.get_measurement>`
method of QuantumVariable. The results are returned as a dictionary of another
numpy subtype called OutcomeArray.

Expand Down Expand Up @@ -646,6 +648,8 @@ def get_measurement(
------
Exception
Tried to get measurement within open environment.
TracingModeError
Tried to get measurement in tracing mode.

Returns
-------
Expand All @@ -665,7 +669,7 @@ def get_measurement(

"""
if check_for_tracing_mode():
raise Exception("Tried to get_measurement from QuantumArray in tracing mode")
raise TracingModeError("Tried to get_measurement from QuantumArray in tracing mode")

for qv in self.flatten():
if qv.is_deleted():
Expand Down
174 changes: 152 additions & 22 deletions src/qrisp/core/quantum_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

"""Defines the QuantumSession class managing QuantumVariable lifecycles, environments, and compilation."""

import itertools
import weakref
from operator import attrgetter

import numpy as np

Expand All @@ -31,7 +33,11 @@
)
from qrisp.core.quantum_variable import QuantumVariable
from qrisp.core.session_merging_tools import multi_session_merge
from qrisp.misc import get_depth_dic
from qrisp.misc import get_depth_dic, infer_python_var_name


class QuantumVariableNamingError(Exception):
Comment thread
moar55 marked this conversation as resolved.
Resolved
"""The error to be thrown when a QuantumVariable already exists in a QuantumSession"""


class QuantumSession(QuantumCircuit):
Expand Down Expand Up @@ -180,7 +186,7 @@ def __init__(self, backend=None):
# inside a function after the function finished
self.uncomp_stack = []

self.qs_tracker.append(weakref.ref(self))
QuantumSession.qs_tracker.append(weakref.ref(self))

self.will_be_uncomputed = False

Expand All @@ -189,35 +195,142 @@ def __init__(self, backend=None):
# when this session is merged into another session.
self.shadow_sessions = []

def register_qv(self, qv, size):
"""Method to register QuantumVariables
def _is_fresh_name(self, name: str):
return name not in map(attrgetter("name"), self.qv_list + self.deleted_qv_list)

def _default_name_generator(self, name: str, suffix: int):
yield name
while True:
yield f"{name}_{suffix}"
suffix += 1
Comment thread
moar55 marked this conversation as resolved.
Resolved

def _find_valid_name(self, name_generator) -> str:
name = next(name_generator)
while not self._is_fresh_name(name):
name = next(name_generator)
return name

def _generate_name_from_code_introspection(self, declaration_stack_level: int):
python_var_name = infer_python_var_name(declaration_stack_level + 1)
if python_var_name is None:
return None
valid_name = self._find_valid_name(self._default_name_generator(python_var_name, 0))
return valid_name

# Returns a pair of a name from provided param ``name`` and
# a boolean indicating if name is fixed, i.e: can't be modified later on.
# Names with a ``"*"`` suffix can be modified.
# If name is fixed and collides with an existing QuantumVariable,
# a QuantumVariableNamingError is raised. Raising here (rather than returning
# the error) keeps the return type a plain ``tuple[str, bool]``.
def _generate_name_from_provided_name(self, name: str) -> tuple[str, bool]:
allows_change_suffix = name.endswith("*")
name = name.removesuffix("*")

# If it's a fresh name return it.
if self._is_fresh_name(name):
return (name, not allows_change_suffix)

# If suffixes are not allowed with a '*' raise an error.
if not allows_change_suffix:
raise QuantumVariableNamingError(f"Variable name {name} already exists in quantum session")

# Otherwise append name with a valid numerical suffix.
valid_name = self._find_valid_name(self._default_name_generator(name, QuantumVariable.creation_counter))
return (valid_name, False)

def generate_name(
self, name: str | None, qv: QuantumVariable, declaration_stack_level: int, is_duplicated_name=False
) -> tuple[str, bool]:
"""Determine a unique name to register ``qv`` under in this QuantumSession.

If ``name`` is given, it is used as-is (raising if it collides), unless it
ends with ``"*"``, in which case the ``"*"`` is dropped and a numerical
suffix is appended on collision instead of raising. If ``name`` is None,
the name of the Python variable ``qv`` is being assigned to is inferred via
code introspection; if that also fails, a generic unique name is generated
(see ``QuantumVariable.get_unique_name``).

Parameters
----------
name : str or None
The name to register ``qv`` under, or None to infer/generate one.
qv : QuantumVariable
QuantumVariable to register.
The QuantumVariable being named.
Used for its type-dependent generic-name generation method
in the final fallback case.
declaration_stack_level : int
How many stack frames above the caller of this method the line
Comment thread
moar55 marked this conversation as resolved.
Resolved
declaring ``qv`` is expected to be found (0 being the caller's own
line), used for code introspection when ``name`` is None.
is_duplicated_name : bool, optional
If True and ``name`` is given, a ``"_dupl*"`` suffix is appended to
``name`` before resolving it, allowing the duplicate to be renamed on
collision instead of raising.
Used by :meth:`QuantumVariable.duplicate <qrisp.QuantumVariable.duplicate>`
when no explicit name was requested for the duplicate. The default is
False.

Raises
------
RuntimeError
Name of qv is already used in this QuantumSession.
ValueError
``name`` is the empty string.
QuantumVariableNamingError
``name`` (without a trailing ``"*"``) is already used in this
QuantumSession.

Returns
-------
tuple[str, bool]
The resolved name, and whether that name is fixed (see
``QuantumVariable.is_fixed_name``).

"""
if name in ["", "*"]:
raise ValueError("QuantumVariable name must not be empty")
if name is not None:
effective_name = name + "_dupl*" if is_duplicated_name else name
return self._generate_name_from_provided_name(effective_name)

# Otherwise attempt to introspect variable name:
if (generated_name := self._generate_name_from_code_introspection(declaration_stack_level + 1)) is not None:
return (generated_name, False)

# Finally, if introspection is not possible, fallback to generating a unique name.
get_unique_name_generator = (qv.get_unique_name() for _ in itertools.count())
return (self._find_valid_name(get_unique_name_generator), False)

def register_qv(self, qv: QuantumVariable, size: int | None = None):
"""Method to register QuantumVariables

Parameters
----------
qv : QuantumVariable
QuantumVariable to register.
size : int, optional
The amount of qubits to request and assign to ``qv.reg``. If None, no
qubits are requested and ``qv.reg`` is left untouched - used when
``qv.reg`` has already been assigned before calling this method (e.g. via
:meth:`duplicate <qrisp.QuantumVariable.duplicate>` with an explicit
``qubits`` argument). The default is None.

Returns
-------
None.

"""
if qv.name in [temp_qv.name for temp_qv in self.qv_list + self.deleted_qv_list]:
raise QuantumVariableNamingError("Variable name " + str(qv.name) + " already exists in quantum session")
if not self._is_fresh_name(qv.name):
Comment thread
moar55 marked this conversation as resolved.
Resolved
raise QuantumVariableNamingError(f"Variable name {qv.name} already exists in quantum session")

# Hand qubits to quantum variable
qv.reg = self.request_qubits(size, name=qv.name)
# Hand qubits to quantum variable if size is provided.
if size is not None:
Comment thread
moar55 marked this conversation as resolved.
Resolved
Comment thread
moar55 marked this conversation as resolved.
Resolved
qv.reg = self.request_qubits(size, name=qv.name)

# Register in the list of active quantum variable
self.qv_list.append(qv)

QuantumVariable.live_qvs.append(weakref.ref(qv))
qv.creation_time = int(QuantumVariable.creation_counter[0])
qv.creation_time = QuantumVariable.creation_counter
QuantumVariable.creation_counter += 1

def get_qv(self, key):
Expand Down Expand Up @@ -1200,10 +1313,16 @@ def triple_AND(a, b, c):
)

def __del__(self):
# Resolved through type(self) rather than the bare `QuantumSession` name:
# during interpreter shutdown, module globals (including the class name
# itself) may already be cleared to None by the time this runs, while
# type(self) stays valid as long as the instance does.
cls = type(self)
i = 0
while i < len(self.qs_tracker):
if self.qs_tracker[i]() is None or id(self) == id(self.qs_tracker[i]()):
self.qs_tracker.pop(i)
while i < len(cls.qs_tracker):
qs = cls.qs_tracker[i]()
if qs is None or qs is self:
cls.qs_tracker.pop(i)
continue
i += 1

Expand All @@ -1224,14 +1343,25 @@ def __setattr__(self, name, value):
QuantumCircuit.__setattr__(self, name, value)

@classmethod
def get_active_quantum_sessions(self):
# Remove potential duplicates
qs_list = list(set([qs() for qs in QuantumSession.qs_tracker if qs() is not None]))
def get_active_quantum_sessions(cls):
"""
Returns weak references to all currently active QuantumSessions.

self.qs_tracker = [weakref.ref(qs) for qs in qs_list]
Every QuantumSession registers itself in the class-level ``qs_tracker``
list upon creation (as a :class:`weakref.ref`, so that tracking a
session does not keep it alive). This method prunes ``qs_tracker`` of
references whose QuantumSession has since been deallocated,
deduplicates the remaining references, and returns the cleaned-up list.

return list(self.qs_tracker)
Returns
-------
list[weakref.ref]
A list of weak references to the active QuantumSession instances.

"""
# Remove potential duplicates
qs_list = list(set([qs() for qs in cls.qs_tracker if qs() is not None]))

class QuantumVariableNamingError(Exception):
"""The error to be thrown when a QuantumVariable already exists in a QuantumSession"""
cls.qs_tracker = [weakref.ref(qs) for qs in qs_list]

return list(cls.qs_tracker)
Loading
Loading