From a9d5d7f35f51654dd1c8e29f0e5c26ed05016f3d Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:15:08 -0800 Subject: [PATCH 01/68] notey --- qualtran/_infra/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qualtran/_infra/__init__.py b/qualtran/_infra/__init__.py index ce76a8d10a..89a9ee1e20 100644 --- a/qualtran/_infra/__init__.py +++ b/qualtran/_infra/__init__.py @@ -11,3 +11,8 @@ # 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. + +# ------------------------------------------------------------------------------ +# Note to intrepid developers: everything under _infra/ is re-exported in the +# top-level qualtran/__init__.py +# ------------------------------------------------------------------------------ From 62e9fee2ca69793fefcd9890de374eb7591b912a Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:15:33 -0800 Subject: [PATCH 02/68] ctrl --- qualtran/_infra/bloq.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index ec5e2d7350..18c4578516 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -394,14 +394,9 @@ def _my_add_controlled( add_controlled: A function with the signature documented above that the system can use to automatically wire up the new control registers. """ - from qualtran import Controlled, CtrlSpec - from qualtran.bloqs.mcmt.controlled_via_and import ControlledViaAnd + from qualtran._infra.controlled import make_ctrl_system_with_correct_metabloq - if ctrl_spec != CtrlSpec(): - # reduce controls to a single qubit - return ControlledViaAnd.make_ctrl_system(self, ctrl_spec=ctrl_spec) - - return Controlled.make_ctrl_system(self, ctrl_spec=ctrl_spec) + return make_ctrl_system_with_correct_metabloq(self, ctrl_spec=ctrl_spec) def controlled(self, ctrl_spec: Optional['CtrlSpec'] = None) -> 'Bloq': """Return a controlled version of this bloq. From ac2db81dd115dfae37bfcb858bfe36f8b690bf29 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:17:30 -0800 Subject: [PATCH 03/68] qdtype --- qualtran/_infra/composite_bloq.py | 40 ++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/qualtran/_infra/composite_bloq.py b/qualtran/_infra/composite_bloq.py index 450612c6d5..5fba9a2fc5 100644 --- a/qualtran/_infra/composite_bloq.py +++ b/qualtran/_infra/composite_bloq.py @@ -42,7 +42,7 @@ from .binst_graph_iterators import greedy_topological_sort from .bloq import Bloq, DecomposeNotImplementedError, DecomposeTypeError -from .data_types import check_dtypes_consistent, QAny, QBit, QDType +from .data_types import check_dtypes_consistent, QAny, QBit, QCDType, QDType from .quantum_graph import BloqInstance, Connection, DanglingT, LeftDangle, RightDangle, Soquet from .registers import Register, Side, Signature @@ -831,7 +831,7 @@ def __init__(self, add_registers_allowed: bool = True): self.add_register_allowed = add_registers_allowed def add_register_from_dtype( - self, reg: Union[str, Register], dtype: Optional[QDType] = None + self, reg: Union[str, Register], dtype: Optional[QCDType] = None ) -> Union[None, SoquetT]: """Add a new typed register to the composite bloq being built. @@ -863,9 +863,9 @@ def add_register_from_dtype( else: if not isinstance(reg, str): raise ValueError("`reg` must be a string register name if not a Register.") - if not isinstance(dtype, QDType): + if not isinstance(dtype, QCDType): raise ValueError( - "`dtype` must be specified and must be an QDType if `reg` is a register name." + "`dtype` must be specified and must be a QCDType if `reg` is a register name." ) reg = Register(name=reg, dtype=dtype) @@ -894,7 +894,7 @@ def add_register( this operation is not allowed. Args: - reg: Either the register or a register name. If this is a register, then `bitsize` + reg: Either the register or a register name. If this is a register name, then `bitsize` must also be provided and a default THRU register will be added. bitsize: If `reg` is a register name, this is the bitsize for the added register. Otherwise, this must not be provided. @@ -904,8 +904,22 @@ def add_register( initial, left-dangling soquets for the register. Otherwise, this is a RIGHT register and will be used for error checking in `finalize()` and nothing is returned. """ - if bitsize is not None: - return self.add_register_from_dtype(reg, QBit() if bitsize == 1 else QAny(bitsize)) + from qualtran.symbolics import is_symbolic + + if isinstance(reg, str): + if bitsize is None: + raise ValueError( + f"When calling `add_register(reg={reg!r}, bitsize=?) bitsize must be provided." + ) + if is_symbolic(bitsize) or isinstance(bitsize, int): + return self.add_register_from_dtype(reg, QBit() if bitsize == 1 else QAny(bitsize)) + if isinstance(bitsize, QCDType): + raise ValueError( + f"Invalid bitsize {bitsize!r} for `add_register({reg!r}). " + f"Consider `add_register_from_dtype`" + ) + raise ValueError(f"Invalid bitsize {bitsize!r} for `add_register({reg!r}).") + return self.add_register_from_dtype(reg) @classmethod @@ -1223,7 +1237,11 @@ def free(self, soq: Soquet, dirty: bool = False) -> None: if not isinstance(soq, Soquet): raise ValueError("`free` expects a single Soquet to free.") - self.add(Free(dtype=soq.reg.dtype, dirty=dirty), reg=soq) + qdtype = soq.reg.dtype + if not isinstance(qdtype, QDType): + raise ValueError("`free` can only free quantum registers.") + + self.add(Free(dtype=qdtype, dirty=dirty), reg=soq) def split(self, soq: Soquet) -> NDArray[Soquet]: # type: ignore[type-var] """Add a Split bloq to split up a register.""" @@ -1232,7 +1250,11 @@ def split(self, soq: Soquet) -> NDArray[Soquet]: # type: ignore[type-var] if not isinstance(soq, Soquet): raise ValueError("`split` expects a single Soquet to split.") - return self.add(Split(dtype=soq.reg.dtype), reg=soq) + qdtype = soq.reg.dtype + if not isinstance(qdtype, QDType): + raise ValueError("`split` can only split quantum registers.") + + return self.add(Split(dtype=qdtype), reg=soq) def join(self, soqs: SoquetInT, dtype: Optional[QDType] = None) -> Soquet: from qualtran.bloqs.bookkeeping import Join From fbca42ba7753ca69184d1bdf8084a3b4662b3cb2 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:18:25 -0800 Subject: [PATCH 04/68] ctrl spec --- qualtran/_infra/controlled.py | 54 ++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/qualtran/_infra/controlled.py b/qualtran/_infra/controlled.py index 59b69b05d8..7a50582caa 100644 --- a/qualtran/_infra/controlled.py +++ b/qualtran/_infra/controlled.py @@ -11,6 +11,7 @@ # 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. +import abc from collections import Counter from functools import cached_property from typing import ( @@ -18,6 +19,7 @@ Dict, Iterable, List, + Mapping, Optional, Protocol, Sequence, @@ -33,7 +35,7 @@ from ..symbolics import is_symbolic, prod, Shaped, SymbolicInt from .bloq import Bloq, DecomposeNotImplementedError, DecomposeTypeError -from .data_types import QBit, QDType +from .data_types import CDType, QBit, QCDType, QDType from .gate_with_registers import GateWithRegisters from .registers import Register, Side, Signature @@ -116,8 +118,8 @@ class CtrlSpec: of the ctrl register is implied to be `cv.shape`). """ - qdtypes: Tuple[QDType, ...] = attrs.field( - default=QBit(), converter=lambda qt: (qt,) if isinstance(qt, QDType) else tuple(qt) + qdtypes: Tuple[QCDType, ...] = attrs.field( + default=QBit(), converter=lambda qt: (qt,) if isinstance(qt, QCDType) else tuple(qt) ) cvs: Tuple[Union[NDArray[np.integer], Shaped], ...] = attrs.field( default=1, converter=_cvs_convert @@ -143,6 +145,11 @@ def concrete_shapes(self) -> tuple[tuple[int, ...], ...]: raise ValueError(f"cannot get concrete shapes: found symbolic {self.shapes}") return shapes # type: ignore + @cached_property + def num_bits(self) -> SymbolicInt: + """Total number of bits required for control registers represented by this CtrlSpec.""" + return sum(dtype.num_bits * prod(shape) for dtype, shape in zip(self.qdtypes, self.shapes)) + @cached_property def num_qubits(self) -> SymbolicInt: """Total number of qubits required for control registers represented by this CtrlSpec.""" @@ -150,10 +157,15 @@ def num_qubits(self) -> SymbolicInt: dtype.num_qubits * prod(shape) for dtype, shape in zip(self.qdtypes, self.shapes) ) + @cached_property + def num_cbits(self) -> SymbolicInt: + """Total number of classical bits required for control registers represented by this CtrlSpec.""" + return sum(dtype.num_cbits * prod(shape) for dtype, shape in zip(self.qdtypes, self.shapes)) + def is_symbolic(self): return is_symbolic(*self.qdtypes) or is_symbolic(*self.cvs) - def activation_function_dtypes(self) -> Sequence[Tuple[QDType, Tuple[SymbolicInt, ...]]]: + def activation_function_dtypes(self) -> Sequence[Tuple[QCDType, Tuple[SymbolicInt, ...]]]: """The data types that serve as input to the 'activation function'. The activation function takes in (quantum) inputs of these types and shapes and determines @@ -242,13 +254,17 @@ def to_cirq_cv(self) -> 'cirq.SumOfProducts': import cirq if self.is_symbolic(): - raise ValueError(f"Cannot convert symbolic {self} to cirq control values.") + raise ValueError(f"Cannot convert symbolic {self} to Cirq control values.") + if self.num_cbits > 0: + raise ValueError( + f"Cannot convert classical control spec {self} to Cirq control values." + ) cirq_cv = [] - for qdtype, cv in zip(self.qdtypes, self.cvs): + for dtype, cv in zip(self.qdtypes, self.cvs): assert isinstance(cv, np.ndarray) - for idx in Register('', qdtype, cv.shape).all_idxs(): - cirq_cv += [*qdtype.to_bits(cv[idx])] + for idx in Register('', dtype, cv.shape).all_idxs(): + cirq_cv += [*dtype.to_bits(cv[idx])] return cirq.SumOfProducts([tuple(cirq_cv)]) @classmethod @@ -256,7 +272,7 @@ def from_cirq_cv( cls, cirq_cv: 'cirq.ops.AbstractControlValues', *, - qdtypes: Optional[Sequence[QDType]] = None, + qdtypes: Optional[Sequence[QCDType]] = None, shapes: Optional[Sequence[Tuple[int, ...]]] = None, ) -> 'CtrlSpec': """Construct a CtrlSpec from cirq.SumOfProducts representation of control values.""" @@ -273,33 +289,31 @@ def from_cirq_cv( # Verify that the given values for qdtypes and shapes are compatible with cv. if sum(dt.num_qubits * np.prod(sh) for dt, sh in zip(qdtypes, shapes)) != len(cv): - raise ValueError( - f"Sum of qubits across {qdtypes=} and {shapes=} should match {len(cv)=}" - ) + raise ValueError(f"Sum of bits across {qdtypes=} and {shapes=} should match {len(cv)=}") # Convert the AND clause to a CtrlSpec. idx = 0 bloq_cvs = [] - for qdtype, shape in zip(qdtypes, shapes): - full_shape = shape + (qdtype.num_qubits,) + for dtype, shape in zip(qdtypes, shapes): + full_shape = shape + (dtype.num_bits,) curr_cvs_bits = np.array(cv[idx : idx + int(np.prod(full_shape))]).reshape(full_shape) - curr_cvs = np.apply_along_axis(qdtype.from_bits, -1, curr_cvs_bits) # type: ignore + curr_cvs = np.apply_along_axis(dtype.from_bits, -1, curr_cvs_bits) # type: ignore bloq_cvs.append(curr_cvs) return CtrlSpec(tuple(qdtypes), tuple(bloq_cvs)) - def get_single_ctrl_bit(self) -> ControlBit: + def get_single_ctrl_val(self) -> ControlBit: """If controlled by a single qubit, return the control bit, otherwise raise""" if self.is_symbolic(): raise ValueError(f"cannot get ctrl bit for symbolic {self}") - if self.num_qubits != 1: + if self.num_bits != 1: raise ValueError(f"expected a single qubit control, got {self.num_qubits}") - (qdtype,) = self.qdtypes + (dtype,) = self.qdtypes (cv,) = self.cvs assert isinstance(cv, np.ndarray) - (idx,) = Register('', qdtype, cv.shape).all_idxs() - (control_bit,) = qdtype.to_bits(cv[idx]) + (idx,) = Register('', dtype, cv.shape).all_idxs() + (control_bit,) = dtype.to_bits(cv[idx]) return int(control_bit) From 4dfe53a0f6e1090f23162627a061a48608bca0d1 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:20:06 -0800 Subject: [PATCH 05/68] controlled --- qualtran/_infra/controlled.py | 246 +++++++++++++++++++++++----------- 1 file changed, 170 insertions(+), 76 deletions(-) diff --git a/qualtran/_infra/controlled.py b/qualtran/_infra/controlled.py index 7a50582caa..7d0d5fa59e 100644 --- a/qualtran/_infra/controlled.py +++ b/qualtran/_infra/controlled.py @@ -362,23 +362,21 @@ def _get_nice_ctrl_reg_names(reg_names: List[str], n: int) -> Tuple[str, ...]: return tuple(names) -@attrs.frozen -class Controlled(GateWithRegisters): - """A controlled version of `subbloq`. - - This meta-bloq is part of the 'controlled' protocol. As a default fallback, - we wrap any bloq without a custom controlled version in this meta-bloq. - - Users should likely not use this class directly. Prefer using `bloq.controlled(ctrl_spec)`, - which may return a tailored Bloq that is controlled in the desired way. +class _ControlledBase(GateWithRegisters, metaclass=abc.ABCMeta): + """Base class for representing the controlled version of an arbitrary bloq. - Args: - subbloq: The bloq we are controlling. - ctrl_spec: The specification for how to control the bloq. + This meta-bloq interface is part of the 'controlled' protocol. """ - subbloq: 'Bloq' - ctrl_spec: 'CtrlSpec' + @property + @abc.abstractmethod + def subbloq(self) -> 'Bloq': + """The bloq being controlled.""" + + @property + @abc.abstractmethod + def ctrl_spec(self) -> 'CtrlSpec': + """The specification of how the `subbloq` is controlled.""" @cached_property def _thru_registers_only(self) -> bool: @@ -387,13 +385,15 @@ def _thru_registers_only(self) -> bool: return False return True - @classmethod - def make_ctrl_system(cls, bloq: 'Bloq', ctrl_spec: 'CtrlSpec') -> Tuple[Bloq, AddControlledT]: - """A factory method for creating both the Controlled and the adder function. + @staticmethod + def _make_ctrl_system(cb: '_ControlledBase') -> Tuple['_ControlledBase', 'AddControlledT']: + """A static method to create the adder function from an implementation of this class. + + Classes implementing this interface can use this static method to create a factory + class method to construct both the controlled bloq and its adder function. See `Bloq.get_ctrl_system`. """ - cb = cls(subbloq=bloq, ctrl_spec=ctrl_spec) ctrl_reg_names = cb.ctrl_reg_names def add_controlled( @@ -432,72 +432,32 @@ def signature(self) -> 'Signature': # Prepend register(s) corresponding to `ctrl_spec`. return Signature(self.ctrl_regs + tuple(self.subbloq.signature)) - def decompose_bloq(self) -> 'CompositeBloq': - return Bloq.decompose_bloq(self) - - def build_composite_bloq( - self, bb: 'BloqBuilder', **initial_soqs: 'SoquetT' - ) -> Dict[str, 'SoquetT']: - if not self._thru_registers_only: - raise DecomposeTypeError(f"Cannot handle non-thru registers in {self.subbloq}") - - # Use subbloq's decomposition but wire up the additional ctrl_soqs. - from qualtran import CompositeBloq - - if isinstance(self.subbloq, CompositeBloq): - cbloq = self.subbloq - else: - cbloq = self.subbloq.decompose_bloq() - - ctrl_soqs: List['SoquetT'] = [initial_soqs[creg_name] for creg_name in self.ctrl_reg_names] + def on_classical_vals(self, **vals: 'ClassicalValT') -> Mapping[str, 'ClassicalValT']: + """Classical action of controlled bloqs. - soq_map: List[Tuple[SoquetT, SoquetT]] = [] - for binst, in_soqs, old_out_soqs in cbloq.iter_bloqsoqs(): - in_soqs = bb.map_soqs(in_soqs, soq_map) - new_bloq, adder = binst.bloq.get_ctrl_system(self.ctrl_spec) - adder_output = adder(bb, ctrl_soqs=ctrl_soqs, in_soqs=in_soqs) - ctrl_soqs = list(adder_output[0]) - new_out_soqs = adder_output[1] - soq_map.extend(zip(old_out_soqs, new_out_soqs)) - - fsoqs = bb.map_soqs(cbloq.final_soqs(), soq_map) - fsoqs |= dict(zip(self.ctrl_reg_names, ctrl_soqs)) - return fsoqs - - def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': - try: - sub_cg = self.subbloq.build_call_graph(ssa=ssa) - except DecomposeTypeError as e1: - raise DecomposeTypeError(f"Could not build call graph for {self}: {e1}") from e1 - except DecomposeNotImplementedError as e2: - raise DecomposeNotImplementedError( - f"Could not build call graph for {self}: {e2}" - ) from e2 - - counts = Counter['Bloq']() - if isinstance(sub_cg, set): - for bloq, n in sub_cg: - counts[bloq.controlled(self.ctrl_spec)] += n - else: - for bloq, n in sub_cg.items(): - counts[bloq.controlled(self.ctrl_spec)] += n - return counts - - def on_classical_vals(self, **vals: 'ClassicalValT') -> Dict[str, 'ClassicalValT']: + This involves conditionally doing the classical action of `subbloq`. All implementers + of `_ControlledBase` should provide a decomposition that satisfies this classical action. + """ if not self._thru_registers_only: raise ValueError(f"Cannot handle non-thru registers in {self}.") ctrl_vals = [vals[reg_name] for reg_name in self.ctrl_reg_names] other_vals = {reg.name: vals[reg.name] for reg in self.subbloq.signature} if self.ctrl_spec.is_active(*ctrl_vals): - rets = self.subbloq.on_classical_vals(**other_vals) - rets |= { - reg_name: ctrl_val for reg_name, ctrl_val in zip(self.ctrl_reg_names, ctrl_vals) + rets = { + **self.subbloq.on_classical_vals(**other_vals), + **{ + reg_name: ctrl_val for reg_name, ctrl_val in zip(self.ctrl_reg_names, ctrl_vals) + }, } return rets return vals def _tensor_data(self): + """Dense tensor encoding a controlled unitary. + + This is used by Bloq.my_tensors and cirq.Gate._unitary_ to support tensor simulation. + """ if not self._thru_registers_only: raise ValueError(f"Cannot handle non-thru registers in {self}.") from qualtran.simulation.tensor._tensor_data_manipulation import ( @@ -519,6 +479,7 @@ def _tensor_data(self): return data def _unitary_(self): + """Cirq-style unitary protocol.""" if isinstance(self.subbloq, GateWithRegisters): # subbloq is a cirq gate, use the cirq-style API to derive a unitary. import cirq @@ -569,11 +530,8 @@ def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) - i = self.ctrl_reg_names.index(reg.name) return self.ctrl_spec.wire_symbol(i, reg, idx) - def adjoint(self) -> 'Bloq': - return self.subbloq.adjoint().controlled(ctrl_spec=self.ctrl_spec) - def __str__(self) -> str: - num_ctrls = self.ctrl_spec.num_qubits + num_ctrls = self.ctrl_spec.num_bits ctrl_string = 'C' if num_ctrls == 1 else f'C[{num_ctrls}]' return f'{ctrl_string}[{self.subbloq}]' @@ -607,3 +565,139 @@ def _circuit_diagram_info_( ) return _wire_symbol_to_cirq_diagram_info(self, args) + + +@attrs.frozen +class Controlled(_ControlledBase): + """A controlled version of `subbloq`. + + This bloq represents a "total control" strategy of controlling `subbloq`: the decomposition + of `Controlled(b)` uses the decomposition of `b` and controls each subbloq in that + decomposition. + + Users should likely not use this class directly. Prefer using `bloq.controlled(ctrl_spec)`, + which may return a natively-controlled Bloq or a more intelligent construction for + complex control specs. + + Args: + subbloq: The bloq we are controlling. + ctrl_spec: The specification for how to control the bloq. + """ + + subbloq: 'Bloq' + ctrl_spec: 'CtrlSpec' + + def __attrs_post_init__(self): + for qdtype in self.ctrl_spec.qdtypes: + if not isinstance(qdtype, QCDType): + raise ValueError(f"Invalid type found in `ctrl_spec`: {qdtype}") + if not isinstance(qdtype, QDType): + raise ValueError( + f"`qualtran.Controlled` requires a purely-quantum control spec for accurate resource estimation. Found {qdtype}. Consider using TODO" + ) + + @classmethod + def make_ctrl_system( + cls, bloq: 'Bloq', ctrl_spec: 'CtrlSpec' + ) -> Tuple['_ControlledBase', 'AddControlledT']: + """A factory method for creating both the Controlled and the adder function. + + See `Bloq.get_ctrl_system`. + """ + cb = cls(subbloq=bloq, ctrl_spec=ctrl_spec) + return cls._make_ctrl_system(cb) + + def decompose_bloq(self) -> 'CompositeBloq': + return Bloq.decompose_bloq(self) + + def build_composite_bloq( + self, bb: 'BloqBuilder', **initial_soqs: 'SoquetT' + ) -> Dict[str, 'SoquetT']: + if not self._thru_registers_only: + raise DecomposeTypeError(f"Cannot handle non-thru registers in {self.subbloq}") + + # Use subbloq's decomposition but wire up the additional ctrl_soqs. + from qualtran import CompositeBloq + + if isinstance(self.subbloq, CompositeBloq): + cbloq = self.subbloq + else: + cbloq = self.subbloq.decompose_bloq() + + ctrl_soqs: List['SoquetT'] = [initial_soqs[creg_name] for creg_name in self.ctrl_reg_names] + + soq_map: List[Tuple[SoquetT, SoquetT]] = [] + for binst, in_soqs, old_out_soqs in cbloq.iter_bloqsoqs(): + in_soqs = bb.map_soqs(in_soqs, soq_map) + new_bloq, adder = binst.bloq.get_ctrl_system(self.ctrl_spec) + adder_output = adder(bb, ctrl_soqs=ctrl_soqs, in_soqs=in_soqs) + ctrl_soqs = list(adder_output[0]) + new_out_soqs = adder_output[1] + soq_map.extend(zip(old_out_soqs, new_out_soqs)) + + fsoqs = bb.map_soqs(cbloq.final_soqs(), soq_map) + fsoqs |= dict(zip(self.ctrl_reg_names, ctrl_soqs)) + return fsoqs + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + try: + sub_cg = self.subbloq.build_call_graph(ssa=ssa) + except DecomposeTypeError as e1: + raise DecomposeTypeError(f"Could not build call graph for {self}: {e1}") from e1 + except DecomposeNotImplementedError as e2: + raise DecomposeNotImplementedError( + f"Could not build call graph for {self}: {e2}" + ) from e2 + + counts = Counter['Bloq']() + if isinstance(sub_cg, set): + for bloq, n in sub_cg: + counts[bloq.controlled(self.ctrl_spec)] += n + else: + for bloq, n in sub_cg.items(): + counts[bloq.controlled(self.ctrl_spec)] += n + return counts + + def adjoint(self) -> 'Bloq': + return self.subbloq.adjoint().controlled(ctrl_spec=self.ctrl_spec) + + +def make_ctrl_system_with_correct_metabloq( + bloq: 'Bloq', ctrl_spec: 'CtrlSpec' +) -> Tuple['_ControlledBase', 'AddControlledT']: + """The default fallback for `Bloq.make_ctrl_system. + + This intelligently selects the correct implemetation of `_ControlledBase` based + on the control spec. + + - A 1-qubit, positive control (i.e. `CtrlSpec()`) uses `Controlled`, which uses a + "total control" decomposition. + - Complex quantum controls (i.e. `CtrlSpec(...)` with quantum data types) uses + `ControlledViaAnd`, which computes the activation function once and re-uses it + for each subbloq in the decomposition of `bloq`. + """ + from qualtran.bloqs.mcmt.controlled_via_and import ControlledViaAnd + + if ctrl_spec == CtrlSpec(): + return Controlled.make_ctrl_system(bloq, ctrl_spec=ctrl_spec) + + qdtypes = [] + cdtypes = [] + for qcdtype in ctrl_spec.qdtypes: + if not isinstance(qcdtype, QCDType): + raise ValueError(f"Invalid data type encountered in the control spec: {qcdtype}") + if isinstance(qcdtype, QDType): + qdtypes.append(qcdtype) + if isinstance(qcdtype, CDType): + cdtypes.append(qcdtype) + + if qdtypes and cdtypes: + raise NotImplementedError( + "At present, Qualtran does not support mixing quantum and classical controls within a single CtrlSpec. As an alternative, consider first getting a quantum controlled bloq and then classically controlling it." + ) + if qdtypes: + return ControlledViaAnd.make_ctrl_system(bloq, ctrl_spec=ctrl_spec) + if cdtypes: + raise NotImplementedError("Stay tuned...") + + raise ValueError(f"Invalid control spec: {ctrl_spec}") From 0f3b5a148d421ab36a68bf0cfdfe3e15df633454 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:20:15 -0800 Subject: [PATCH 06/68] controlled --- qualtran/_infra/controlled_test.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/qualtran/_infra/controlled_test.py b/qualtran/_infra/controlled_test.py index 626201ac46..3eff66f029 100644 --- a/qualtran/_infra/controlled_test.py +++ b/qualtran/_infra/controlled_test.py @@ -18,7 +18,7 @@ import sympy import qualtran.testing as qlt_testing -from qualtran import Bloq, CompositeBloq, Controlled, CtrlSpec, QBit, QInt, QUInt, Register +from qualtran import Bloq, CBit, CompositeBloq, Controlled, CtrlSpec, QBit, QInt, QUInt, Register from qualtran._infra.gate_with_registers import get_named_qubits, merge_qubits from qualtran.bloqs.basic_gates import ( CSwap, @@ -60,6 +60,20 @@ def test_ctrl_spec(): assert cvs[tuple()] == 234234 +def test_ctrl_spec_classical(): + cspec = CtrlSpec(CBit()) + assert cspec.num_cbits == 1 + assert cspec.num_bits == 1 + assert cspec.num_qubits == 0 + assert cspec.is_active(1) + + cspec = CtrlSpec(CBit(), cvs=0) + assert cspec.num_cbits == 1 + assert cspec.num_bits == 1 + assert cspec.num_qubits == 0 + assert not cspec.is_active(1) + + def test_ctrl_spec_shape(): c1 = CtrlSpec(QBit(), cvs=1) c2 = CtrlSpec(QBit(), cvs=(1,)) @@ -84,25 +98,28 @@ def test_ctrl_spec_to_cirq_cv_roundtrip(): cirq_cv, qdtypes=ctrl_spec.qdtypes, shapes=ctrl_spec.concrete_shapes ) + with pytest.raises(ValueError): + CtrlSpec(CBit(), cvs=[0, 1, 0, 1]).to_cirq_cv() + @pytest.mark.parametrize( "ctrl_spec", [CtrlSpec(), CtrlSpec(cvs=[1]), CtrlSpec(cvs=np.atleast_2d([1]))] ) def test_ctrl_spec_single_bit_one(ctrl_spec: CtrlSpec): - assert ctrl_spec.get_single_ctrl_bit() == 1 + assert ctrl_spec.get_single_ctrl_val() == 1 @pytest.mark.parametrize( "ctrl_spec", [CtrlSpec(cvs=0), CtrlSpec(cvs=[0]), CtrlSpec(cvs=np.atleast_2d([0]))] ) def test_ctrl_spec_single_bit_zero(ctrl_spec: CtrlSpec): - assert ctrl_spec.get_single_ctrl_bit() == 0 + assert ctrl_spec.get_single_ctrl_val() == 0 @pytest.mark.parametrize("ctrl_spec", [CtrlSpec(cvs=[1, 1]), CtrlSpec(qdtypes=QUInt(2), cvs=0)]) def test_ctrl_spec_single_bit_raises(ctrl_spec: CtrlSpec): with pytest.raises(ValueError): - ctrl_spec.get_single_ctrl_bit() + ctrl_spec.get_single_ctrl_val() @pytest.mark.parametrize("shape", [(1,), (10,), (10, 10)]) @@ -110,6 +127,7 @@ def test_ctrl_spec_symbolic_cvs(shape: tuple[int, ...]): ctrl_spec = CtrlSpec(cvs=Shaped(shape)) assert ctrl_spec.is_symbolic() assert ctrl_spec.num_qubits == np.prod(shape) + assert ctrl_spec.num_bits == np.prod(shape) assert ctrl_spec.shapes == (shape,) @@ -122,6 +140,7 @@ def test_ctrl_spec_symbolic_dtype(shape: tuple[int, ...]): assert ctrl_spec.is_symbolic() assert ctrl_spec.num_qubits == n * np.prod(shape) + assert ctrl_spec.num_bits == n * np.prod(shape) assert ctrl_spec.shapes == (shape,) From d8bbe49b2cdd097a72dbeeb47f8edec60daf757a Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:20:27 -0800 Subject: [PATCH 07/68] data types --- qualtran/DataTypes.ipynb | 353 ++++++++++++++++++++++++++++++++++ qualtran/_infra/data_types.py | 136 ++++++++----- 2 files changed, 436 insertions(+), 53 deletions(-) create mode 100644 qualtran/DataTypes.ipynb diff --git a/qualtran/DataTypes.ipynb b/qualtran/DataTypes.ipynb new file mode 100644 index 0000000000..018219dc8c --- /dev/null +++ b/qualtran/DataTypes.ipynb @@ -0,0 +1,353 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc3bc886-66ec-4a5e-8042-ed84c98ae210", + "metadata": {}, + "source": [ + "# Data Types\n", + "\n", + "## Introduction to quantum data: the qubit\n", + "Qualtran lets you write quantum programs that operate on quantum data. The smallest unit of quantum data is the qubit (\"quantum bit\"). A quantum bit can be in the familiar `0` or `1` states (called computational basis states) or any combination of them, like $|+\\rangle = (|0\\rangle + |1\\rangle)/\\sqrt{2}$. Allocation-like bloqs can allocate a qubit in a particular state. Below, we create a simple program that allocates one quantum vairalbe in the `0` state and one in the `+` state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "780e26eb-b50a-46fd-aec9-a74d68201c2f", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import BloqBuilder\n", + "from qualtran.bloqs.basic_gates import ZeroState, PlusState\n", + "\n", + "bb = BloqBuilder()\n", + "zero_q = bb.add(ZeroState())\n", + "plus_q = bb.add(PlusState())\n", + "cbloq = bb.finalize(q0=zero_q, q1=plus_q)\n", + "\n", + "from qualtran.drawing import show_bloq\n", + "show_bloq(cbloq)" + ] + }, + { + "cell_type": "markdown", + "id": "a3ead17f-c2b1-4d7b-8467-deb4691ce4a2", + "metadata": {}, + "source": [ + "## Quantum variables\n", + "\n", + "When we use `BloqBuilder` to `add` these allocation operations to our program, we are given a handle to the resulting quantum data. These handles are *quantum variables*, which can be provided as inputs to subsequent operations. Quantum variables follow *linear logic*: that is, each quantum variable must be used exactly once. You cannot use the same variable twice (this would violate the *no-cloning theorem*), and you cannot leave a variable unused (this would violate the corresponding *no-deleting theorem*). In the above program, we use the `finalize` method to account for our unused quantum variables—it is presumed that the programmer will handle these piece of data with subsequent bloqs." + ] + }, + { + "cell_type": "markdown", + "id": "f51e6b35-64a9-442c-b677-b7053791e3f0", + "metadata": {}, + "source": [ + "## Bloq signatures and `QBit()`\n", + "\n", + "We write quantum programs by composing subroutines encoded as Qualtran *bloqs*. A bloq class inherits from the `qualtran.Bloq` interface, which only has one required property: `signature`. A bloq's signature declares the names and types of quantum data the bloq takes as input and output. You might think of a bloq with nothing other than its signature analogous to declaring (but not defining) a function in a C/C++ header file. \n", + "\n", + "The `Bloq.signature` property method must return a `qualtran.Signature` object, which is itself a list of `Register` objects. Each register is the name and data type of an input/output variable. In quantum computing (as a consequence of the no-deleting theorem), we often have a pattern we term *thru registers* where quantum data is used as input and returned with the same name and data type, so registers default to simultaneous input and output arguments.\n", + "\n", + "Below, we construct a signature consisting of two input-output arguments named 'arg1' and 'arg2'; and we declare that each must be a qubit using the data type specification `qualtran.QBit()`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9da1ffe1-b214-43aa-82f6-098dffec10eb", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import Signature, Register, QBit\n", + "\n", + "signature = Signature([\n", + " Register('arg1', QBit()),\n", + " Register('arg2', QBit()),\n", + "])\n", + "print(signature.n_qubits())" + ] + }, + { + "cell_type": "markdown", + "id": "e3f1675d-57af-4712-b694-62cca89440c3", + "metadata": {}, + "source": [ + "## Quantum data types\n", + "\n", + "Completely analogously to classical computation, collections of individual qubits can be used to encode a variety of data types. For example, `qualtran.QUInt(32)` represents a 32-bit unsigned, quantum integer. These data type objects are used in the definition of signatures to provide type checking for your quantum programs. \n", + "\n", + "In Qualtran, quantum variables of arbitrary type are first-class objects. You can represent a program operating on e.g. 2048-bit registers without having a unique index or an individual Python object for each underlying qubit (like in many NISQ frameworks like Cirq). \n", + "\n", + "We support statically-sized data; and do not support sum or union types. Built-in data types like `QInt`, `QFxp` (fixed-point reals), `QIntOnesComp` (signed integers using ones' complement), and others are available in the top-level `qualtran` namespace. Custom data types can be implemented by inheriting from `QDType`.\n", + "\n", + "Below, we construct a signature consisting of two input-output arguments named 'x' and 'y'; and we declare that each is a 32-bit quantum unsigned integer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "578c71e2-3b6b-43d1-bb91-88c94ae4f70e", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import QUInt\n", + "\n", + "signature = Signature([\n", + " Register('x', QUInt(32)),\n", + " Register('y', QUInt(32)),\n", + "])\n", + "print(signature.n_qubits())" + ] + }, + { + "cell_type": "markdown", + "id": "3fc9dd5c-7e05-4b4c-a423-0c58399bc46e", + "metadata": {}, + "source": [ + "### Quantum data types as bloq parameters\n", + "\n", + "By using compile-time classical attributes of bloqs, we can support *generic programming* where a single bloq class can be used with a variety of quantum data types. Many of the arithmetic operations take the data type as a compile-time classical attribute.\n", + "\n", + "Below, we show that the `Negate` operation can handle a `QUInt` of arbitrary size; and indeed you can read the documentation to figure out that it also supports signed and other types of integers. Note: we can represent programs on large bitsize variables without any performance overhead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb259875-b438-4ee8-882c-092bd9711682", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic import Negate\n", + "\n", + "negate = Negate(dtype=QUInt(2048))\n", + "show_bloq(negate.decompose_bloq())" + ] + }, + { + "cell_type": "markdown", + "id": "5d9426cd-087a-4d7c-a5cc-8ad95ed4257e", + "metadata": {}, + "source": [ + "## Splitting\n", + "\n", + "It is great if you can express your algorithm as manipulations of quantum ints, reals, or other *high-level* data types. But, we anticipate that the gateset of a quantum computer will consist of 1-, 2- and 3-qubit operations. At some point, we need to define our operations in terms of their action on individual bits. We can use `Split` and other *bookkeeping* operations to carefully cast the data type of a quantum variable so we can write decompositions down to the architecture-supported gateset.\n", + "\n", + "As an example, we'll consider the `BitwiseNot` used in the previous snippet. We'll take a quantum unsigned integer and just do a *not* (in quantum computing: `XGate`) on each bit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b5fdb88-9a4d-4ccc-801f-76769df67214", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.bloqs.basic_gates import XGate\n", + "\n", + "dtype = QUInt(3) # 3-bit integer for demonstration purposes\n", + "\n", + "# We'll use BloqBuilder directly. In the standard library this would\n", + "# be the `build_composite_bloq` method on the `BitwiseNot` bloq class\n", + "bb = BloqBuilder()\n", + "x = bb.add_register_from_dtype('x', dtype)\n", + "\n", + "# First, we split up the bits using the `.split` helper method on BloqBuilder.\n", + "# It returns a numpy array of quantum variables.\n", + "x_bits = bb.split(x)\n", + "\n", + "# Then, we apply the XGate to each bit. Remember that each quantum variable\n", + "# must be used exactly once, so the input bits are consumed by the XGate and\n", + "# we get a new variable back that we store in our `x_bits` array.\n", + "for i in range(len(x_bits)):\n", + " x_bits[i] = bb.add(XGate(), q=x_bits[i])\n", + "\n", + "# For users calling this bloq, we want the fact that we split up all the bits\n", + "# to be an \"implementation detail\"; so we re-join our output bits back into\n", + "# a 3-bit unsigned integer\n", + "x = bb.join(x_bits, dtype=dtype)\n", + "\n", + "# Finish up and draw a diagram\n", + "cbloq = bb.finalize(x=x)\n", + "show_bloq(cbloq)" + ] + }, + { + "cell_type": "markdown", + "id": "a0c2d228-8f1e-4d7b-938e-4b59430aa27d", + "metadata": {}, + "source": [ + "## Endianness\n", + "\n", + "The Qualtran data types use a big-endian bit convention. The most significant bit is at index 0." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbe875d9-7a15-488c-ad19-02cd0487765a", + "metadata": {}, + "outputs": [], + "source": [ + "QUInt(8).to_bits(x=0x30)" + ] + }, + { + "cell_type": "markdown", + "id": "08a9d5cd-ef4e-4695-b992-e8c7f777248a", + "metadata": {}, + "source": [ + "## Casting and QAny\n", + "\n", + "In general, we can cast from one data type to another using the `Cast` bloq. The system will validate that the number of bits between the two data types match, but this operation must still be done with some care.\n", + "\n", + "When type checking is irrelevant, you can use the `QAny(n)` type to represent an arbitrary collection of qubits that doesn't necessarily encode anything. \n", + "\n", + "Below, we allocate individual qubits and then join them into a new quantum variable. Since there's no type information, the resulting variable will have the `QAny(3)` type. We can declare that this should encode a `QUInt(3)` by using a `Cast`. (There's also a `dtype` argument to `bb.join`, which you would probably use in practice)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ecf4057-9ea9-4854-8b66-d5b041a8cc49", + "metadata": {}, + "outputs": [], + "source": [ + "bb = BloqBuilder()\n", + "\n", + "# Make three |0> qubits\n", + "qs = [bb.add(ZeroState()) for _ in range(3)]\n", + "\n", + "# Join them into one quantum variable. Since\n", + "# we don't specify a type, `x` is `QAny(3)`. \n", + "x = bb.join(qs)\n", + "\n", + "# Maybe we're trying to allocate an unsigned integer.\n", + "from qualtran.bloqs.bookkeeping import Cast\n", + "from qualtran import QAny\n", + "x = bb.add(Cast(inp_dtype=QAny(3), out_dtype=QUInt(3)), reg=x)\n", + "\n", + "cbloq = bb.finalize(x=x)\n", + "show_bloq(cbloq)" + ] + }, + { + "cell_type": "markdown", + "id": "e6987d5b-1465-47dd-894e-f966cadb868f", + "metadata": {}, + "source": [ + "## Type checking\n", + "\n", + "When wiring up bloqs, the data types must be compatible. \n", + "\n", + " - When the two data types are the same, they are always compatible\n", + " - All single-qubit data types are compatible\n", + "\n", + "The consistency checking functions accept a severity parameter. If it is set to `STRICT`, then nothing outside of the above two rules are compatible. If it is set to `LOOSE` (the default), the following pairs are also compatible:\n", + "\n", + " - `QAny` is compatible with any other data type if its number of qubits match\n", + " - Integer types are mutually compatible if the number of qubits match\n", + " - An unsigned `QFxp` fixed-point with only an integer part is compatible with integer types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f4e4a13-e112-4b7e-95e1-6fee0168d989", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import QDTypeCheckingSeverity, check_dtypes_consistent\n", + "\n", + "print('same ', check_dtypes_consistent(QUInt(3), QUInt(3)))\n", + "print('1bit ', check_dtypes_consistent(QBit(), QAny(1)))\n", + "print('qany ',\n", + " check_dtypes_consistent(QAny(3), QUInt(3)),\n", + " check_dtypes_consistent(QAny(3), QUInt(3), QDTypeCheckingSeverity.STRICT)\n", + ")\n", + "from qualtran import QInt\n", + "print('qint ', \n", + " check_dtypes_consistent(QUInt(3), QInt(3)),\n", + " check_dtypes_consistent(QUInt(3), QInt(3), QDTypeCheckingSeverity.STRICT)\n", + ")\n", + "print('diff ', check_dtypes_consistent(QAny(3), QAny(4)))" + ] + }, + { + "cell_type": "markdown", + "id": "a27b83f4-44e4-4b53-a878-ecce6a100675", + "metadata": {}, + "source": [ + "## `QDType`, `CDType`, and `QCDType`\n", + "\n", + "Quantum variables are essential when authoring quantum programs, but we live in a classical world. Measuring a qubit yields a classical bit, and a program can do classical branching (choosing which quantum operations to execute based on a classical bit). Each data type we've seen so far is a quantum data type and inherits from `QDType`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06c5c158-555b-48cd-b612-e00d03b82f70", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import QDType\n", + "\n", + "print(\"QBit() is QDType:\", isinstance(QBit(), QDType), \"; num_qubits =\", QBit().num_qubits)\n", + "print(\"QUInt(4) is QDType:\", isinstance(QUInt(4), QDType), \"; num_qubits =\", QUInt(4).num_qubits)" + ] + }, + { + "cell_type": "markdown", + "id": "c434a68e-b691-4a35-bc81-49b7159728f8", + "metadata": {}, + "source": [ + "There is a more general base class: `QCDType` that includes both quantum and classical data types. Classical data types inherit from `CDType`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c983e4cb-ac84-497b-883f-3a71abf6878f", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import QCDType, QDType, CDType, CBit\n", + "\n", + "dtypes = [QBit(), QUInt(4), CBit()]\n", + "\n", + "print(f\"{'dtype':10} {'QCDType?':9s} {'QDType?':9s} {'CDType?':9s}\"\n", + " f\"{'bits':>6s} {'qubits':>6s} {'cbits':>6s}\"\n", + " )\n", + "print(\"-\"*60)\n", + "for dtype in dtypes:\n", + " print(f\"{dtype!s:10} {isinstance(dtype, QCDType)!s:9} {isinstance(dtype, QDType)!s:9} {isinstance(dtype, CDType)!s:9}\"\n", + " f\"{dtype.num_bits:6d} {dtype.num_qubits:6d} {dtype.num_cbits:6d}\"\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 4c270e04d0..7d746e7cf6 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -11,42 +11,8 @@ # 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. -"""Quantum data type definitions. - -We often wish to write algorithms which operate on quantum data. One can think -of quantum data types, similar to classical data types, where a collection of -qubits can be used to represent a specific quantum data type (eg: a quantum -integer of width 32 would comprise 32 qubits, similar to a classical uint32 -type). More generally, many current primitives and algorithms in qualtran -implicitly expect registers which represent signed or unsigned integers, -fixed-point (fp) numbers , or “classical registers” which store some classical -value. Enforcing typing helps developers and users reason about algorithms, and -will also allow better type checking. - -The basic principles we follow are: - -1. Typing should not be too invasive for the developer / user: We got pretty far -without explicitly typing registers. -2. For algorithms or bloqs which expect registers which are meant to encode -numeric types (integers, reals, etc.) then typing should be strictly enforced. -For example, a bloq multiplying two fixed point reals should be built with an -explicit QFxp dtype. -3. The smallest addressable unit is a QBit. Other types are interpretations of -collections of QBits. A QUInt(32) is intended to represent a register -encoding positive integers. -4. To avoid too much overhead we have a QAny type, which is meant to represent -an opaque bag of bits with no particular significance associated with them. A -bloq defined with a QAny register (e.g. a n-bit CSwap) will accept any other -type assuming the bitsizes match. QInt(32) == QAny(32), QInt(32) != -QFxp(32, 16). QInt(32) != QUInt(32). -5. We assume a big endian convention for addressing QBits in registers -throughout qualtran. Recall that in a big endian convention the most significant -bit is at index 0. If you iterate through the bits in a register they will be -yielded from most significant to least significant. -6. Ones' complement integers are used extensively in quantum algorithms. We have -two types QInt and QIntOnesComp for integers using two's and ones' complement -respectively. -""" +"""Quantum data type definitions.""" + import abc from enum import Enum @@ -64,14 +30,23 @@ import galois -class QDType(metaclass=abc.ABCMeta): - """This defines the abstract interface for quantum data types.""" +class QCDType(metaclass=abc.ABCMeta): + """The abstract interface for quantum/classical quantum computing data types.""" + + @property + def num_bits(self) -> int: + return self.num_qubits + self.num_cbits @property @abc.abstractmethod def num_qubits(self) -> int: """Number of qubits required to represent a single instance of this data type.""" + @property + @abc.abstractmethod + def num_cbits(self) -> int: + """Number of classical bits required to represent a single instance of this data type.""" + @abc.abstractmethod def get_classical_domain(self) -> Iterable[Any]: """Yields all possible classical (computational basis state) values representable @@ -131,7 +106,7 @@ def assert_valid_classical_val_array(self, val_array: NDArray[Any], debug_str: s @abc.abstractmethod def is_symbolic(self) -> bool: - """Returns True if this qdtype is parameterized with symbolic objects.""" + """Returns True if this dtype is parameterized with symbolic objects.""" def iteration_length_or_zero(self) -> SymbolicInt: """Safe version of iteration length. @@ -144,13 +119,43 @@ def __str__(self): return f'{self.__class__.__name__}({self.num_qubits})' -@attrs.frozen -class QBit(QDType): - """A single qubit. The smallest addressable unit of quantum data.""" +class QDType(QCDType, metaclass=abc.ABCMeta): + """The abstract interface for quantum data types.""" @property - def num_qubits(self): - return 1 + def num_cbits(self) -> int: + return 0 + + @property + @abc.abstractmethod + def num_qubits(self) -> int: + """Number of qubits required to represent a single instance of this data type.""" + + def __str__(self): + return f'{self.__class__.__name__}({self.num_qubits})' + + +class CDType(QCDType, metaclass=abc.ABCMeta): + """The abstract interface for classical data types.""" + + @property + def num_qubits(self) -> int: + return 0 + + @property + @abc.abstractmethod + def num_cbits(self) -> int: + """Number of classical bits required to represent a single instance of this data type.""" + + def __str__(self): + return f'{self.__class__.__name__}({self.num_cbits})' + + +class _Bit(metaclass=abc.ABCMeta): + """A single quantum or classical bit. The smallest addressable unit of data. + + Use either `QBit()` or `CBit()` for quantum or classical implementations, respectively. + """ def get_classical_domain(self) -> Iterable[int]: yield from (0, 1) @@ -179,7 +184,25 @@ def assert_valid_classical_val_array( raise ValueError(f"Bad {self} value array in {debug_str}") def __str__(self): - return 'QBit()' + return f'{self.__class__.__name__}()' + + +@attrs.frozen +class QBit(_Bit, QDType): + """A single qubit. The smallest addressable unit of quantum data.""" + + @property + def num_qubits(self): + return 1 + + +@attrs.frozen +class CBit(_Bit, CDType): + """A single classical bit. The smallest addressable unit of classical data.""" + + @property + def num_cbits(self) -> int: + return 1 @attrs.frozen @@ -188,6 +211,13 @@ class QAny(QDType): bitsize: SymbolicInt + def __attrs_post_init__(self): + if is_symbolic(self.bitsize): + return + + if not isinstance(self.bitsize, int): + raise ValueError() + @property def num_qubits(self): return self.bitsize @@ -1021,8 +1051,8 @@ def __str__(self): return f'QGF({self.characteristic}**{self.degree})' -QAnyInt = (QInt, QUInt, BQUInt, QMontgomeryUInt) -QAnyUInt = (QUInt, BQUInt, QMontgomeryUInt, QGF) +_QAnyInt = (QInt, QUInt, BQUInt, QMontgomeryUInt) +_QAnyUInt = (QUInt, BQUInt, QMontgomeryUInt, QGF) class QDTypeCheckingSeverity(Enum): @@ -1046,8 +1076,8 @@ def _check_uint_fxp_consistent(a: Union[QUInt, BQUInt, QMontgomeryUInt, QGF], b: def check_dtypes_consistent( - dtype_a: QDType, - dtype_b: QDType, + dtype_a: QCDType, + dtype_b: QCDType, type_checking_severity: QDTypeCheckingSeverity = QDTypeCheckingSeverity.LOOSE, ) -> bool: """Check if two types are consistent given our current definition on consistent types. @@ -1075,13 +1105,13 @@ def check_dtypes_consistent( return same_n_qubits if type_checking_severity == QDTypeCheckingSeverity.ANY: return False - if isinstance(dtype_a, QAnyInt) and isinstance(dtype_b, QAnyInt): + if isinstance(dtype_a, _QAnyInt) and isinstance(dtype_b, _QAnyInt): # A subset of the integers should be freely interchangeable. return same_n_qubits - elif isinstance(dtype_a, QAnyUInt) and isinstance(dtype_b, QFxp): - # unsigned Fxp which is wholy an integer or < 1 part is a uint. + elif isinstance(dtype_a, _QAnyUInt) and isinstance(dtype_b, QFxp): + # unsigned Fxp which is wholly an integer or < 1 part is a uint. return _check_uint_fxp_consistent(dtype_a, dtype_b) - elif isinstance(dtype_b, QAnyUInt) and isinstance(dtype_a, QFxp): + elif isinstance(dtype_b, _QAnyUInt) and isinstance(dtype_a, QFxp): # unsigned Fxp which is wholy an integer or < 1 part is a uint. return _check_uint_fxp_consistent(dtype_b, dtype_a) else: From a747b5127167e0924c70ce1095eb68b1e585597f Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:21:35 -0800 Subject: [PATCH 08/68] on classical vals --- qualtran/_infra/bloq.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 18c4578516..5c726cef77 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -170,7 +170,7 @@ def adjoint(self) -> 'Bloq': def on_classical_vals( self, **vals: Union['sympy.Symbol', 'ClassicalValT'] - ) -> Dict[str, 'ClassicalValT']: + ) -> Mapping[str, 'ClassicalValT']: """How this bloq operates on classical data. Override this method if your bloq represents classical, reversible logic. For example: @@ -182,11 +182,9 @@ def on_classical_vals( Args: **vals: The input classical values for each left (or thru) register. The data - types are guaranteed to match `self.registers`. Values for registers - with bitsize `n` will be integers of that bitsize. Values for registers with - `shape` will be an ndarray of integers of the given bitsize. Note: integers - can be either Numpy or Python integers. If they are Python integers, they - are unsigned. + types are guaranteed to match `self.signature`. Values for registers + with a particular dtype will be the corresponding classical data type. Values for + registers with `shape` will be an ndarray of values of the expected type. Returns: A dictionary mapping right (or thru) register name to output classical values. From 497d7ee0ee517de1940c094e2fa42d75d6011cf1 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:21:51 -0800 Subject: [PATCH 09/68] dtypes --- qualtran/_infra/data_types_test.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index 70494c5686..f2cc1f9ae9 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -20,13 +20,11 @@ import sympy from numpy.typing import NDArray -from qualtran.symbolics import ceil, is_symbolic, log2 - -from .data_types import ( +from qualtran import ( BQUInt, + CBit, check_dtypes_consistent, QAny, - QAnyInt, QBit, QDType, QFxp, @@ -36,11 +34,29 @@ QMontgomeryUInt, QUInt, ) +from qualtran._infra.data_types import _QAnyInt +from qualtran.symbolics import ceil, is_symbolic, log2 + + +def test_bit(): + qbit = QBit() + assert qbit.num_qubits == 1 + assert qbit.num_cbits == 0 + assert qbit.num_bits == 1 + assert str(qbit) == 'QBit()' + + cbit = CBit() + assert cbit.num_cbits == 1 + assert cbit.num_qubits == 0 + assert cbit.num_bits == 1 + assert str(CBit()) == 'CBit()' def test_qint(): qint_8 = QInt(8) assert qint_8.num_qubits == 8 + assert qint_8.num_cbits == 0 + assert qint_8.num_bits == 8 assert str(qint_8) == 'QInt(8)' n = sympy.symbols('x') qint_8 = QInt(n) @@ -254,7 +270,7 @@ def test_type_errors_fxp(): def test_type_errors_matrix(qdtype_a, qdtype_b): if qdtype_a == qdtype_b: assert check_dtypes_consistent(qdtype_a, qdtype_b) - elif isinstance(qdtype_a, QAnyInt) and isinstance(qdtype_b, QAnyInt): + elif isinstance(qdtype_a, _QAnyInt) and isinstance(qdtype_b, _QAnyInt): assert check_dtypes_consistent(qdtype_a, qdtype_b) else: assert not check_dtypes_consistent(qdtype_a, qdtype_b) From 9bba4e4ac8d196cf0980794299c46493f3b5c690 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:22:02 -0800 Subject: [PATCH 10/68] registers --- qualtran/_infra/registers.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/qualtran/_infra/registers.py b/qualtran/_infra/registers.py index 1cb09c18fa..cb466564e4 100644 --- a/qualtran/_infra/registers.py +++ b/qualtran/_infra/registers.py @@ -24,7 +24,7 @@ from qualtran.symbolics import is_symbolic, prod, smax, ssum, SymbolicInt -from .data_types import QAny, QBit, QDType +from .data_types import QAny, QBit, QCDType class Side(enum.Flag): @@ -62,15 +62,15 @@ class Register: """ name: str - dtype: QDType + dtype: QCDType _shape: Tuple[SymbolicInt, ...] = field( default=tuple(), converter=lambda v: (v,) if isinstance(v, int) else tuple(v) ) side: Side = Side.THRU def __attrs_post_init__(self): - if not isinstance(self.dtype, QDType): - raise ValueError(f'dtype must be a QDType: found {type(self.dtype)}') + if not isinstance(self.dtype, QCDType): + raise ValueError(f'dtype must be a QCDType: found {type(self.dtype)}') def is_symbolic(self) -> bool: return is_symbolic(self.dtype, *self._shape) @@ -87,7 +87,7 @@ def shape(self) -> Tuple[int, ...]: @property def bitsize(self) -> int: - return self.dtype.num_qubits + return self.dtype.num_bits def all_idxs(self) -> Iterable[Tuple[int, ...]]: """Iterate over all possible indices of a multidimensional register.""" @@ -100,6 +100,22 @@ def total_bits(self) -> int: """ return self.bitsize * prod(self.shape_symbolic) + def total_qubits(self) -> int: + """The total number of qubits in this register. + + This is the product of the register's data type's number of qubits + and each of the dimensions in `shape`. + """ + return self.dtype.num_qubits * prod(self.shape_symbolic) + + def total_cbits(self) -> int: + """The total number of classical bits in this register. + + This is the product of the register's data type's number of classical bits + and each of the dimensions in `shape`. + """ + return self.dtype.num_cbits * prod(self.shape_symbolic) + def adjoint(self) -> 'Register': """Return the 'adjoint' of this register by switching RIGHT and LEFT registers.""" if self.side is Side.THRU: @@ -154,7 +170,7 @@ def build(cls, **registers: Union[int, sympy.Expr]) -> 'Signature': ) @classmethod - def build_from_dtypes(cls, **registers: QDType) -> 'Signature': + def build_from_dtypes(cls, **registers: QCDType) -> 'Signature': """Construct a Signature comprised of simple thru registers given the register dtypes. Args: @@ -200,9 +216,11 @@ def n_qubits(self) -> int: If the signature has LEFT and RIGHT registers, the number of qubits in the signature is taken to be the greater of the number of left or right qubits. A bloq with this signature uses at least this many qubits. + + Classical registers are ignored. """ - left_size = ssum(reg.total_bits() for reg in self.lefts()) - right_size = ssum(reg.total_bits() for reg in self.rights()) + left_size = ssum(reg.total_qubits() for reg in self.lefts()) + right_size = ssum(reg.total_qubits() for reg in self.rights()) return smax(left_size, right_size) def __repr__(self): From efe6f0f460d6c17059db05fad2d3a16015f5a1f5 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:22:08 -0800 Subject: [PATCH 11/68] registers --- qualtran/_infra/registers_test.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/qualtran/_infra/registers_test.py b/qualtran/_infra/registers_test.py index f52b1af7a1..425fa9bb9e 100644 --- a/qualtran/_infra/registers_test.py +++ b/qualtran/_infra/registers_test.py @@ -16,7 +16,7 @@ import pytest import sympy -from qualtran import BQUInt, QAny, QBit, QInt, Register, Side, Signature +from qualtran import BQUInt, CBit, QAny, QBit, QInt, QUInt, Register, Side, Signature from qualtran._infra.gate_with_registers import get_named_qubits from qualtran.symbolics import is_symbolic @@ -32,6 +32,14 @@ def test_register(): assert r == r.adjoint() +def test_classical_register(): + r = Register('c', CBit()) + assert r.bitsize == 1 + assert r.total_qubits() == 0 + assert r.total_cbits() == 1 + assert r.total_bits() == 1 + + def test_multidim_register(): r = Register("my_reg", QBit(), shape=(2, 3), side=Side.RIGHT) idxs = list(r.all_idxs()) @@ -135,6 +143,13 @@ def test_signature_symbolic(): assert str(signature.n_qubits()) == 'n_x + n_y' +def test_partial_classical_signature_n_qubits(): + sig = Signature( + [Register('x', QUInt(5)), Register('y', QUInt(5), side=Side.RIGHT), Register('c', CBit())] + ) + assert sig.n_qubits() == 10 + + def test_signature_build(): sig1 = Signature([Register("r1", QAny(5)), Register("r2", QAny(2))]) sig2 = Signature.build(r1=5, r2=2) From e1454335f1af51bb68257c5008f76e3d9f9e2cb8 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:22:49 -0800 Subject: [PATCH 12/68] bloq ctrl spec --- qualtran/bloqs/basic_gates/identity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qualtran/bloqs/basic_gates/identity.py b/qualtran/bloqs/basic_gates/identity.py index c5eeafa1ac..d8c2a57e76 100644 --- a/qualtran/bloqs/basic_gates/identity.py +++ b/qualtran/bloqs/basic_gates/identity.py @@ -100,6 +100,9 @@ def __str__(self) -> str: return 'I' def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']: + if ctrl_spec.num_cbits > 0: + return super().get_ctrl_system(ctrl_spec=ctrl_spec) + ctrl_I = Identity(ctrl_spec.num_qubits + self.bitsize) def ctrl_adder( From dedc3042334aa4b37d7ee15f34e330e28b571d4d Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:25:26 -0800 Subject: [PATCH 13/68] cast --- qualtran/bloqs/bookkeeping/cast.py | 50 ++++++++++++++++++------- qualtran/bloqs/bookkeeping/cast_test.py | 14 ++++++- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/qualtran/bloqs/bookkeeping/cast.py b/qualtran/bloqs/bookkeeping/cast.py index 7b5e0a774a..fb0725e9d5 100644 --- a/qualtran/bloqs/bookkeeping/cast.py +++ b/qualtran/bloqs/bookkeeping/cast.py @@ -22,15 +22,18 @@ Bloq, bloq_example, BloqDocSpec, + CDType, CompositeBloq, ConnectionT, DecomposeTypeError, + QCDType, QDType, Register, Side, Signature, ) from qualtran.bloqs.bookkeeping._bookkeeping_bloq import _BookkeepingBloq +from qualtran.symbolics import is_symbolic if TYPE_CHECKING: import quimb.tensor as qtn @@ -41,30 +44,51 @@ @frozen class Cast(_BookkeepingBloq): - """Cast a register from one n-bit QDType to another QDType. + """Cast a register from one n-bit QCDType to another QCDType. - This re-interprets the register's data type from `inp_dtype` to `out_dtype`. + This simply re-interprets the register's data, and is a bookkeeping operation. Args: - inp_dtype: Input QDType to cast from. - out_dtype: Output QDType to cast to. - shape: shape of the register to cast. + inp_dtype: Input QCDType to cast from. + out_dtype: Output QCDType to cast to. + shape: Optional multidimensional shape of the register to cast. + allow_quantum_to_classical: Whether to allow (potentially dangerous) casting a quantum + value to a classical value and vice versa. If you cast a classical bit to a qubit + that was originally obtained by casting a qubit to a classical bit, the program + will model unphysical quantum coherences that can give you fundamentally incorrect + resource estimates. Use a measurement operation to convert a qubit to a classical + bit (and correctly model the decoherence that results). Registers: - in: input register to cast from. - out: input register to cast to. + reg: The quantum variable to cast """ - inp_dtype: QDType - out_dtype: QDType + inp_dtype: QCDType + out_dtype: QCDType shape: Tuple[int, ...] = attrs.field( default=tuple(), converter=lambda v: (v,) if isinstance(v, int) else tuple(v) ) + allow_quantum_to_classical: bool = attrs.field(default=False, kw_only=True) def __attrs_post_init__(self): - if isinstance(self.inp_dtype.num_qubits, int): - if self.inp_dtype.num_qubits != self.out_dtype.num_qubits: - raise ValueError("Casting only permitted between same sized registers.") + q2q = isinstance(self.inp_dtype, QDType) and isinstance(self.out_dtype, QDType) + c2c = isinstance(self.inp_dtype, CDType) and isinstance(self.out_dtype, CDType) + + if not self.allow_quantum_to_classical and not (q2q or c2c): + raise ValueError( + f"Casting {self.inp_dtype} to {self.out_dtype} is potentially dangerous. " + f"If you are sure, set `Cast(..., allow_quantum_to_classical=True)`." + ) + + if is_symbolic(self.inp_dtype.num_bits): + return + + if self.inp_dtype.num_bits != self.out_dtype.num_bits: + raise ValueError( + f"Casting must preserve the number of bits in the data. " + f"Cannot cast {self.inp_dtype} to {self.out_dtype}: " + f"{self.inp_dtype.num_bits} != {self.out_dtype.num_bits}." + ) def decompose_bloq(self) -> 'CompositeBloq': raise DecomposeTypeError(f'{self} is atomic') @@ -90,7 +114,7 @@ def my_tensors( qtn.Tensor( data=np.eye(2), inds=[(outgoing['reg'], j), (incoming['reg'], j)], tags=[str(self)] ) - for j in range(self.inp_dtype.num_qubits) + for j in range(self.out_dtype.num_bits) ] def on_classical_vals(self, reg: int) -> Dict[str, 'ClassicalValT']: diff --git a/qualtran/bloqs/bookkeeping/cast_test.py b/qualtran/bloqs/bookkeeping/cast_test.py index 7dc034a713..abded64baa 100644 --- a/qualtran/bloqs/bookkeeping/cast_test.py +++ b/qualtran/bloqs/bookkeeping/cast_test.py @@ -11,8 +11,9 @@ # 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. +import pytest -from qualtran import QFxp, QInt, QUInt +from qualtran import CBit, QBit, QFxp, QInt, QUInt from qualtran.bloqs.bookkeeping import Cast from qualtran.bloqs.bookkeeping.cast import _cast from qualtran.bloqs.for_testing import TestCastToFrom @@ -57,3 +58,14 @@ def test_cast_unsiged_signed(): c = Cast(QInt(5), QUInt(5)) assert c.call_classically(reg=-1) == (31,) + + +def test_cast_classical(): + with pytest.raises(ValueError): + Cast(QBit(), CBit()) + + c = Cast(QBit(), CBit(), allow_quantum_to_classical=True) + assert c.call_classically(reg=1) == (1,) + + c = Cast(CBit(), QBit(), allow_quantum_to_classical=True) + assert c.call_classically(reg=1) == (1,) From 9fc530082c86ba4ad551b228f70bb4af87808df5 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:25:47 -0800 Subject: [PATCH 14/68] bloq dtypes --- qualtran/bloqs/data_loading/qroam_clean.py | 15 ++++++++++++--- qualtran/bloqs/data_loading/select_swap_qrom.py | 7 ++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/qualtran/bloqs/data_loading/qroam_clean.py b/qualtran/bloqs/data_loading/qroam_clean.py index 7aa83dab37..3f276cc0da 100644 --- a/qualtran/bloqs/data_loading/qroam_clean.py +++ b/qualtran/bloqs/data_loading/qroam_clean.py @@ -21,7 +21,16 @@ import sympy from numpy.typing import ArrayLike -from qualtran import Bloq, bloq_example, BloqDocSpec, GateWithRegisters, Register, Side, Signature +from qualtran import ( + Bloq, + bloq_example, + BloqDocSpec, + GateWithRegisters, + QAny, + Register, + Side, + Signature, +) from qualtran.bloqs.basic_gates import Toffoli from qualtran.bloqs.data_loading.qrom_base import QROMBase from qualtran.drawing import Circle, LarrowTextBox, RarrowTextBox, Text, TextBox, WireSymbol @@ -490,10 +499,10 @@ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str qrom_targets = [] for reg in self.target_registers: qrom_target = _alloc_anc_for_reg_except_first( - bb, reg.dtype, block_sizes, self.use_dirty_ancilla + bb, QAny(reg.dtype.num_qubits), block_sizes, self.use_dirty_ancilla ) qrom_target[np.unravel_index(0, block_sizes)] = _alloc_anc_for_reg( # type: ignore[index] - bb, reg.dtype, reg.shape, dirty=False + bb, QAny(reg.dtype.num_qubits), reg.shape, dirty=False ) qrom_targets.append(qrom_target) # Assert that all registers have been used by now. diff --git a/qualtran/bloqs/data_loading/select_swap_qrom.py b/qualtran/bloqs/data_loading/select_swap_qrom.py index 47eabc7043..6d1d5407d7 100644 --- a/qualtran/bloqs/data_loading/select_swap_qrom.py +++ b/qualtran/bloqs/data_loading/select_swap_qrom.py @@ -28,6 +28,7 @@ BQUInt, DecomposeTypeError, GateWithRegisters, + QAny, Register, Signature, ) @@ -283,7 +284,7 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': ret[self.qrom_bloq] += 1 ret[self.qrom_bloq.adjoint()] += 1 for reg in self.target_registers: - ret[Xor(reg.dtype)] += toggle_overhead * np.prod(reg.shape, dtype=int) + ret[Xor(QAny(reg.dtype.num_qubits))] += toggle_overhead * np.prod(reg.shape, dtype=int) for swz in self.swap_with_zero_bloqs: if any(is_symbolic(s) or s > 0 for s in swz.selection_bitsizes): ret[swz] += toggle_overhead @@ -332,7 +333,7 @@ def _add_cnot( assert isinstance(qrom_reg, np.ndarray) # Make mypy happy. idx = np.unravel_index(0, qrom_reg.shape) qrom_reg[idx], target[i] = bb.add( - Xor(self.target_registers[i].dtype), x=qrom_reg[idx], y=target[i] + Xor(QAny(self.target_registers[i].dtype.num_qubits)), x=qrom_reg[idx], y=target[i] ) return qrom_targets, target @@ -421,7 +422,7 @@ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str ) block_sizes = cast(Tuple[int, ...], self.block_sizes) qrom_targets = [ - _alloc_anc_for_reg(bb, reg.dtype, block_sizes, self.use_dirty_ancilla) + _alloc_anc_for_reg(bb, QAny(reg.dtype.num_qubits), block_sizes, self.use_dirty_ancilla) for reg in self.target_registers ] # Verify some of the assumptions are correct. From e0f2b414342eee72010d548387e4e45943952383 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:26:20 -0800 Subject: [PATCH 15/68] controlled --- qualtran/bloqs/mcmt/controlled_via_and.py | 40 ++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/qualtran/bloqs/mcmt/controlled_via_and.py b/qualtran/bloqs/mcmt/controlled_via_and.py index a0ae58dee1..3947ddf86b 100644 --- a/qualtran/bloqs/mcmt/controlled_via_and.py +++ b/qualtran/bloqs/mcmt/controlled_via_and.py @@ -13,12 +13,21 @@ # limitations under the License. from collections import Counter from functools import cached_property -from typing import Iterable, Sequence, TYPE_CHECKING +from typing import Iterable, Sequence, Tuple, TYPE_CHECKING import numpy as np from attrs import frozen -from qualtran import Bloq, bloq_example, BloqDocSpec, Controlled, CtrlSpec +from qualtran import ( + _ControlledBase, + Bloq, + bloq_example, + BloqDocSpec, + CompositeBloq, + CtrlSpec, + QCDType, + QDType, +) from qualtran.bloqs.basic_gates import XGate from qualtran.bloqs.mcmt.ctrl_spec_and import CtrlSpecAnd @@ -28,7 +37,7 @@ @frozen -class ControlledViaAnd(Controlled): +class ControlledViaAnd(_ControlledBase): """Reduces a generic controlled bloq to a singly-controlled bloq using an And ladder. Implements a generic controlled version of the subbloq, by first reducing the @@ -45,12 +54,35 @@ class ControlledViaAnd(Controlled): subbloq: Bloq ctrl_spec: CtrlSpec + def __attrs_post_init__(self): + for qdtype in self.ctrl_spec.qdtypes: + if not isinstance(qdtype, QCDType): + raise ValueError(f"Invalid type found in `ctrl_spec`: {qdtype}") + if not isinstance(qdtype, QDType): + raise ValueError( + f"`qualtran.Controlled` requires a purely-quantum control spec for accurate resource estimation. Found {qdtype}. Consider using TODO" + ) + + @classmethod + def make_ctrl_system( + cls, bloq: 'Bloq', ctrl_spec: 'CtrlSpec' + ) -> Tuple['_ControlledBase', 'AddControlledT']: + """A factory method for creating both the Controlled and the adder function. + + See `Bloq.get_ctrl_system`. + """ + cb = cls(subbloq=bloq, ctrl_spec=ctrl_spec) + return cls._make_ctrl_system(cb) + + def decompose_bloq(self) -> 'CompositeBloq': + return Bloq.decompose_bloq(self) + def _is_single_bit_control(self) -> bool: return self.ctrl_spec.num_qubits == 1 @cached_property def _single_control_value(self) -> int: - return self.ctrl_spec.get_single_ctrl_bit() + return self.ctrl_spec.get_single_ctrl_val() def adjoint(self) -> 'ControlledViaAnd': return ControlledViaAnd(self.subbloq.adjoint(), self.ctrl_spec) From c58116c787e2c375e726ed3869b06a7f94c8f03a Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:26:43 -0800 Subject: [PATCH 16/68] bloq dtypes --- qualtran/bloqs/mcmt/ctrl_spec_and.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/mcmt/ctrl_spec_and.py b/qualtran/bloqs/mcmt/ctrl_spec_and.py index 713a940477..3fa7edd03c 100644 --- a/qualtran/bloqs/mcmt/ctrl_spec_and.py +++ b/qualtran/bloqs/mcmt/ctrl_spec_and.py @@ -117,7 +117,7 @@ def n_ctrl_qubits(self) -> SymbolicInt: @cached_property def _ctrl_partition_bloq(self) -> Partition: - return Partition(self.ctrl_spec.num_qubits, self.control_registers) + return Partition(self.n_ctrl_qubits, self.control_registers) @property def _flat_cvs(self) -> Union[tuple[int, ...], HasLength]: From 535c8ef46f209d8d8f3bf55ed8ebed7fb6d5d64c Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:27:05 -0800 Subject: [PATCH 17/68] ctrl --- qualtran/bloqs/mcmt/specialized_ctrl.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qualtran/bloqs/mcmt/specialized_ctrl.py b/qualtran/bloqs/mcmt/specialized_ctrl.py index ce1bd3362b..98b6499501 100644 --- a/qualtran/bloqs/mcmt/specialized_ctrl.py +++ b/qualtran/bloqs/mcmt/specialized_ctrl.py @@ -82,7 +82,7 @@ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlled if ctrl_spec.num_qubits != 1: return super().get_ctrl_system(ctrl_spec=ctrl_spec) - ctrl_bloq = attrs.evolve(self, cvs=(ctrl_spec.get_single_ctrl_bit(),) + self.cvs) + ctrl_bloq = attrs.evolve(self, cvs=(ctrl_spec.get_single_ctrl_val(),) + self.cvs) def _adder(bb, ctrl_soqs, in_soqs): in_soqs[self.ctrl_reg_name] = np.concatenate(ctrl_soqs, in_soqs[self.ctrl_reg_name]) @@ -121,16 +121,15 @@ def _get_ctrl_system_1bit_cv( and returns the controlled variant of this bloq and the name of the control register. If the callable returns `None`, then the default fallback is used. """ - from qualtran import Soquet - from qualtran.bloqs.mcmt import ControlledViaAnd + from qualtran import make_ctrl_system_with_correct_metabloq, Soquet def _get_default_fallback(): - return ControlledViaAnd.make_ctrl_system(bloq=bloq, ctrl_spec=ctrl_spec) + return make_ctrl_system_with_correct_metabloq(bloq=bloq, ctrl_spec=ctrl_spec) if ctrl_spec.num_qubits != 1: return _get_default_fallback() - ctrl_bit = ctrl_spec.get_single_ctrl_bit() + ctrl_bit = ctrl_spec.get_single_ctrl_val() if current_ctrl_bit is None: # the easy case: use the controlled bloq @@ -323,7 +322,7 @@ def _specialize_control(self, ctrl_spec: 'CtrlSpec') -> bool: if ctrl_spec.num_qubits != 1: return False - cv = ctrl_spec.get_single_ctrl_bit() + cv = ctrl_spec.get_single_ctrl_val() cv_flag = SpecializeOnCtrlBit.ONE if cv == 1 else SpecializeOnCtrlBit.ZERO return cv_flag in self.specialize_on_ctrl From 9e97cc96400917efdd0e3e0c9b8ca702c8216571 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:27:16 -0800 Subject: [PATCH 18/68] bloq dtypes --- qualtran/bloqs/mcmt/specialized_ctrl_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/mcmt/specialized_ctrl_test.py b/qualtran/bloqs/mcmt/specialized_ctrl_test.py index 3c1ae249e3..8ed8dd515d 100644 --- a/qualtran/bloqs/mcmt/specialized_ctrl_test.py +++ b/qualtran/bloqs/mcmt/specialized_ctrl_test.py @@ -111,7 +111,7 @@ def test_custom_controlled(ctrl_specs: Sequence[CtrlSpec], ctrl_reg_name: str): bloq: Bloq = AtomWithSpecializedControl(ctrl_reg_name=ctrl_reg_name) for ctrl_spec in ctrl_specs: bloq = bloq.controlled(ctrl_spec) - n_ctrls = sum(ctrl_spec.num_qubits for ctrl_spec in ctrl_specs) + n_ctrls = sum(ctrl_spec.num_bits for ctrl_spec in ctrl_specs) gc = get_cost_value(bloq, QECGatesCost()) assert gc == GateCounts( From f2832497f77c574c546659252655ec234b8f4faf Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:27:35 -0800 Subject: [PATCH 19/68] bloq dtypes --- qualtran/bloqs/phase_estimation/lp_resource_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/phase_estimation/lp_resource_state.py b/qualtran/bloqs/phase_estimation/lp_resource_state.py index 53cadba44e..55466291ab 100644 --- a/qualtran/bloqs/phase_estimation/lp_resource_state.py +++ b/qualtran/bloqs/phase_estimation/lp_resource_state.py @@ -141,7 +141,7 @@ def m_bits(self) -> SymbolicInt: return self.bitsize def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str, 'SoquetT']: - qpe_reg = bb.allocate(dtype=self.m_register.dtype) + qpe_reg = bb.allocate(dtype=self.m_qdtype) anc, flag = bb.allocate(dtype=QBit()), bb.allocate(dtype=QBit()) flag_angle = np.arccos(1 / (1 + 2**self.bitsize)) From a26cdab166580c703b538b96bb574b1a8342ec80 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:27:41 -0800 Subject: [PATCH 20/68] bloq dtypes --- qualtran/bloqs/phase_estimation/qpe_window_state.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qualtran/bloqs/phase_estimation/qpe_window_state.py b/qualtran/bloqs/phase_estimation/qpe_window_state.py index 5d6f0c3733..075147c04e 100644 --- a/qualtran/bloqs/phase_estimation/qpe_window_state.py +++ b/qualtran/bloqs/phase_estimation/qpe_window_state.py @@ -17,7 +17,7 @@ import attrs -from qualtran import Bloq, bloq_example, BloqDocSpec, QFxp, Register, Side, Signature +from qualtran import Bloq, bloq_example, BloqDocSpec, QDType, QFxp, Register, Side, Signature from qualtran.bloqs.basic_gates import Hadamard, OnEach from qualtran.symbolics import ceil, log2, pi, SymbolicFloat, SymbolicInt @@ -29,9 +29,13 @@ class QPEWindowStateBase(Bloq, metaclass=abc.ABCMeta): """Base class to construct window states""" + @cached_property + def m_qdtype(self) -> QDType: + return QFxp(self.m_bits, self.m_bits) + @cached_property def m_register(self) -> 'Register': - return Register('qpe_reg', QFxp(self.m_bits, self.m_bits), side=Side.RIGHT) + return Register('qpe_reg', self.m_qdtype, side=Side.RIGHT) @property @abc.abstractmethod @@ -96,7 +100,7 @@ def from_standard_deviation_eps(cls, eps: SymbolicFloat): return cls(ceil(2 * log2(pi(eps) / eps))) def build_composite_bloq(self, bb: 'BloqBuilder') -> Dict[str, 'SoquetT']: - qpe_reg = bb.allocate(dtype=self.m_register.dtype) + qpe_reg = bb.allocate(dtype=self.m_qdtype) qpe_reg = bb.add(OnEach(self.m_bits, Hadamard()), q=qpe_reg) return {'qpe_reg': qpe_reg} From f9bd77d0e4fcddb19e1e788ad05bdc14a6a449b6 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:28:30 -0800 Subject: [PATCH 21/68] bloq dtypes --- qualtran/bloqs/rotations/quantum_variable_rotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/rotations/quantum_variable_rotation.py b/qualtran/bloqs/rotations/quantum_variable_rotation.py index 5a254a4b43..8177ea80e8 100644 --- a/qualtran/bloqs/rotations/quantum_variable_rotation.py +++ b/qualtran/bloqs/rotations/quantum_variable_rotation.py @@ -195,7 +195,7 @@ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str power_of_two = i - self.num_frac_rotations exp = (2**power_of_two) * self.gamma * 2 out[-(i + offset)] = bb.add(ZPowGate(exponent=exp, eps=eps), q=out[-(i + offset)]) - return {self.cost_reg.name: bb.join(out, self.cost_reg.dtype)} + return {self.cost_reg.name: bb.join(out, self.cost_dtype)} def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': zpow = ZPowGate( From a10a331b68ad68aba6d8d25bcf286c65753dce3b Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:30:21 -0800 Subject: [PATCH 22/68] bloq dtypes --- qualtran/cirq_interop/_bloq_to_cirq.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qualtran/cirq_interop/_bloq_to_cirq.py b/qualtran/cirq_interop/_bloq_to_cirq.py index a7a8fa0ae9..a723b99628 100644 --- a/qualtran/cirq_interop/_bloq_to_cirq.py +++ b/qualtran/cirq_interop/_bloq_to_cirq.py @@ -28,6 +28,7 @@ DecomposeNotImplementedError, DecomposeTypeError, LeftDangle, + QDType, Register, RightDangle, Side, @@ -229,6 +230,8 @@ def _bloq_to_cirq_op( soq = cxn.left assert soq.reg.name in out_quregs, f"{soq=} should exist in {out_quregs=}." if soq.reg.side == Side.RIGHT: + if not isinstance(soq.reg.dtype, QDType): + raise ValueError(f"Output classical wires are not supported in Cirq. {soq=}") qvar_to_qreg[soq] = _QReg(out_quregs[soq.reg.name][soq.idx], dtype=soq.reg.dtype) return op From 1c758d4fc8541b28f808f5c73be70ab65e6a3b24 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:30:28 -0800 Subject: [PATCH 23/68] bloq dtypes --- qualtran/drawing/graphviz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qualtran/drawing/graphviz.py b/qualtran/drawing/graphviz.py index b6e70050dd..7f23b959d8 100644 --- a/qualtran/drawing/graphviz.py +++ b/qualtran/drawing/graphviz.py @@ -27,7 +27,7 @@ DanglingT, LeftDangle, QBit, - QDType, + QCDType, Register, RightDangle, Side, @@ -405,7 +405,7 @@ def cxn_edge(self, left_id: str, right_id: str, cxn: Connection) -> pydot.Edge: class TypedGraphDrawer(PrettyGraphDrawer): @staticmethod - def _fmt_dtype(dtype: QDType): + def _fmt_dtype(dtype: QCDType): return str(dtype) def cxn_label(self, cxn: Connection) -> str: From f57cdcc6cbe2ab83245a119b2981e727f91438c0 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:30:39 -0800 Subject: [PATCH 24/68] drawing --- qualtran/drawing/musical_score.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/qualtran/drawing/musical_score.py b/qualtran/drawing/musical_score.py index 5aecc02305..5c3f0dbaa4 100644 --- a/qualtran/drawing/musical_score.py +++ b/qualtran/drawing/musical_score.py @@ -21,6 +21,7 @@ import abc import heapq import json +from enum import Enum from typing import Any, Callable, cast, Dict, Iterable, List, Optional, Set, Tuple, Union import attrs @@ -33,9 +34,12 @@ from qualtran import ( Bloq, BloqInstance, + CDType, Connection, DanglingT, LeftDangle, + QCDType, + QDType, Register, RightDangle, Side, @@ -67,6 +71,21 @@ def json_dict(self): return attrs.asdict(self) +class HLineFlavor(Enum): + QUANTUM = 1 + CLASSICAL = 2 + + @classmethod + def from_qcdtype(cls, qcdtype: QCDType) -> 'HLineFlavor': + if isinstance(qcdtype, QDType): + return cls.QUANTUM + if isinstance(qcdtype, CDType): + return cls.CLASSICAL + + # Fallback + return cls.QUANTUM + + @frozen(order=True) class HLine: """Dataclass representing a horizontal line segment at a given vertical position `x`. @@ -74,11 +93,14 @@ class HLine: It runs from (sequential) x positions `seq_x_start` to `seq_x_end`, inclusive. If `seq_x_end` is `None`, that indicates we've started a line (by allocating a new qubit perhaps) but we don't know where it ends yet. + + The horizontal line can be of a particular `flavor`, e.g. a quantum wire or a classical wire. """ y: int seq_x_start: int seq_x_end: Optional[int] = None + flavor: HLineFlavor = HLineFlavor.QUANTUM def json_dict(self): return attrs.asdict(self) @@ -145,16 +167,17 @@ def new( `seq_x` and `topo_gen` are passed through. """ self.unreserve(binst, reg) + flavor = HLineFlavor.from_qcdtype(reg.dtype) if not reg.shape: y = self.new_y(binst, reg) - self.hlines.add(HLine(y=y, seq_x_start=seq_x)) + self.hlines.add(HLine(y=y, seq_x_start=seq_x, flavor=flavor)) self.maybe_reserve(binst, reg, idx=tuple()) return RegPosition(y=y, seq_x=seq_x, topo_gen=topo_gen) arg = np.zeros(reg.shape, dtype=object) for idx in reg.all_idxs(): y = self.new_y(binst, reg, idx) - self.hlines.add(HLine(y=y, seq_x_start=seq_x)) + self.hlines.add(HLine(y=y, seq_x_start=seq_x, flavor=flavor)) arg[idx] = RegPosition(y=y, seq_x=seq_x, topo_gen=topo_gen) self.maybe_reserve(binst, reg, idx) return arg @@ -693,7 +716,8 @@ def draw_musical_score( for hline in msd.hlines: assert hline.seq_x_end is not None, hline - ax.hlines(-hline.y, hline.seq_x_start, hline.seq_x_end, color='k', zorder=-1) + color = 'b' if hline.flavor is HLineFlavor.CLASSICAL else 'k' + ax.hlines(-hline.y, hline.seq_x_start, hline.seq_x_end, color=color, zorder=-1) for vline in msd.vlines: ax.vlines(vline.x, -vline.top_y, -vline.bottom_y, color='k', zorder=-1) From d0773f122132ab505a7e35d158a63bb59837e154 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:32:03 -0800 Subject: [PATCH 25/68] drawing --- qualtran/drawing/qpic_diagram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qualtran/drawing/qpic_diagram.py b/qualtran/drawing/qpic_diagram.py index befd4f1f1e..389876fa3f 100644 --- a/qualtran/drawing/qpic_diagram.py +++ b/qualtran/drawing/qpic_diagram.py @@ -39,7 +39,7 @@ ) if TYPE_CHECKING: - from qualtran import Bloq, Connection, QDType, Signature + from qualtran import Bloq, Connection, QCDType, Signature def _wire_name_prefix_for_soq(soq: Soquet) -> str: @@ -200,7 +200,7 @@ def _dealloc_wire_for_soq(self, soq: Soquet) -> None: self.soq_map.pop(soq) @classmethod - def _dtype_label_for_wire(cls, wire_name: str, dtype: 'QDType') -> List[str]: + def _dtype_label_for_wire(cls, wire_name: str, dtype: 'QCDType') -> List[str]: if dtype != QBit(): dtype_str = _format_label_text(str(dtype), scale=0.5) return [f'{wire_name} / {dtype_str}'] From ba399135ef8f279083923d72d6ef367c329a6533 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:32:38 -0800 Subject: [PATCH 26/68] annotations --- qualtran/serialization/data_types.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/qualtran/serialization/data_types.py b/qualtran/serialization/data_types.py index 6badc90450..9f7aebe00f 100644 --- a/qualtran/serialization/data_types.py +++ b/qualtran/serialization/data_types.py @@ -11,12 +11,23 @@ # 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 qualtran import BQUInt, QAny, QBit, QDType, QFxp, QInt, QIntOnesComp, QMontgomeryUInt, QUInt +from qualtran import ( + BQUInt, + QAny, + QBit, + QCDType, + QDType, + QFxp, + QInt, + QIntOnesComp, + QMontgomeryUInt, + QUInt, +) from qualtran.protos import data_types_pb2 from qualtran.serialization.args import int_or_sympy_from_proto, int_or_sympy_to_proto -def data_type_to_proto(data: QDType) -> data_types_pb2.QDataType: +def data_type_to_proto(data: QCDType) -> data_types_pb2.QDataType: if isinstance(data, QBit): return data_types_pb2.QDataType(qbit=data_types_pb2.QBit()) From 300b8baf22064b2ce54061b805dd444872ea2344 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:34:17 -0800 Subject: [PATCH 27/68] bits --- qualtran/simulation/tensor/_dense.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/qualtran/simulation/tensor/_dense.py b/qualtran/simulation/tensor/_dense.py index bcfa9105a2..ea4a35bbed 100644 --- a/qualtran/simulation/tensor/_dense.py +++ b/qualtran/simulation/tensor/_dense.py @@ -13,11 +13,11 @@ # limitations under the License. import logging -from typing import Dict, List, Tuple, TYPE_CHECKING +from typing import Any, Dict, List, Tuple, TYPE_CHECKING from numpy.typing import NDArray -from qualtran import Bloq, Connection, ConnectionT, LeftDangle, RightDangle, Signature, Soquet +from qualtran import Bloq, Connection, ConnectionT, LeftDangle, RightDangle, Signature from ._flattening import flatten_for_tensor_contraction from ._quimb import cbloq_to_quimb @@ -48,14 +48,14 @@ def _order_incoming_outgoing_indices( # j: each qubit (sub-)index for a given data type for reg in signature.rights(): for idx in reg.all_idxs(): - for j in range(reg.dtype.num_qubits): + for j in range(reg.dtype.num_bits): if idx: inds.append((outgoing[reg.name][idx], j)) # type: ignore[index] else: inds.append((outgoing[reg.name], j)) # type: ignore[arg-type] for reg in signature.lefts(): for idx in reg.all_idxs(): - for j in range(reg.dtype.num_qubits): + for j in range(reg.dtype.num_bits): if idx: inds.append((incoming[reg.name][idx], j)) # type: ignore[index] else: @@ -64,7 +64,7 @@ def _order_incoming_outgoing_indices( return inds -def get_right_and_left_inds(tn: 'qtn.TensorNetwork', signature: Signature) -> List[List[Soquet]]: +def get_right_and_left_inds(tn: 'qtn.TensorNetwork', signature: Signature) -> List[List[Any]]: """Return right and left tensor indices. In general, this will be returned as a list of length-2 corresponding @@ -80,8 +80,11 @@ def get_right_and_left_inds(tn: 'qtn.TensorNetwork', signature: Signature) -> Li """ left_inds = {} right_inds = {} + + # Each index is a (cxn: Connection, j: int) tuple. cxn: Connection j: int + for ind in tn.outer_inds(): cxn, j = ind if cxn.left.binst is LeftDangle: @@ -99,13 +102,13 @@ def get_right_and_left_inds(tn: 'qtn.TensorNetwork', signature: Signature) -> Li left_ordered_inds = [] for reg in signature.lefts(): for idx in reg.all_idxs(): - for j in range(reg.dtype.num_qubits): + for j in range(reg.dtype.num_bits): left_ordered_inds.append(left_inds[reg, idx, j]) right_ordered_inds = [] for reg in signature.rights(): for idx in reg.all_idxs(): - for j in range(reg.dtype.num_qubits): + for j in range(reg.dtype.num_bits): right_ordered_inds.append(right_inds[reg, idx, j]) inds = [] From bad1f36bfcf57b6e1b6b7601ffd1966daeea6af2 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:35:06 -0800 Subject: [PATCH 28/68] bits --- qualtran/simulation/classical_sim.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qualtran/simulation/classical_sim.py b/qualtran/simulation/classical_sim.py index 8af0af0387..60a064f805 100644 --- a/qualtran/simulation/classical_sim.py +++ b/qualtran/simulation/classical_sim.py @@ -13,6 +13,7 @@ # limitations under the License. """Functionality for the `Bloq.call_classically(...)` protocol.""" +import abc import itertools from typing import ( Any, @@ -28,6 +29,7 @@ Union, ) +import attrs import networkx as nx import numpy as np import sympy @@ -47,13 +49,14 @@ from qualtran._infra.composite_bloq import _binst_to_cxns if TYPE_CHECKING: - from qualtran import QDType + from qualtran import CompositeBloq, QCDType ClassicalValT = Union[int, np.integer, NDArray[np.integer]] -def _numpy_dtype_from_qdtype(dtype: 'QDType') -> Type: - from qualtran._infra.data_types import QBit, QInt, QUInt +def _numpy_dtype_from_qlt_dtype(dtype: 'QCDType') -> Type: + # TODO: Move to a method on QCDType. https://github.com/quantumlib/Qualtran/issues/1437. + from qualtran._infra.data_types import CBit, QBit, QInt, QUInt if isinstance(dtype, QUInt): if dtype.bitsize <= 8: @@ -75,7 +78,7 @@ def _numpy_dtype_from_qdtype(dtype: 'QDType') -> Type: elif dtype.bitsize <= 64: return np.int64 - if isinstance(dtype, QBit): + if isinstance(dtype, (QBit, CBit)): return np.uint8 return object @@ -87,7 +90,7 @@ def _empty_ndarray_from_reg(reg: Register) -> np.ndarray: if isinstance(reg.dtype, QGF): return reg.dtype.gf_type.Zeros(reg.shape) - return np.empty(reg.shape, dtype=_numpy_dtype_from_qdtype(reg.dtype)) + return np.empty(reg.shape, dtype=_numpy_dtype_from_qlt_dtype(reg.dtype)) def _get_in_vals( From d668e62a5b6c85cdb2057ffcf5bcfdb6555097c9 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:35:41 -0800 Subject: [PATCH 29/68] init --- qualtran/__init__.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/qualtran/__init__.py b/qualtran/__init__.py index 745842a5b4..31879580ab 100644 --- a/qualtran/__init__.py +++ b/qualtran/__init__.py @@ -51,16 +51,21 @@ ) from ._infra.data_types import ( + QCDType, + CDType, QDType, - QInt, - QBit, QAny, - QFxp, + QBit, + CBit, + QInt, QIntOnesComp, QUInt, BQUInt, + QFxp, QMontgomeryUInt, QGF, + QDTypeCheckingSeverity, + check_dtypes_consistent, ) # Internal imports: none @@ -83,7 +88,13 @@ from ._infra.adjoint import Adjoint -from ._infra.controlled import Controlled, CtrlSpec, AddControlledT +from ._infra.controlled import ( + Controlled, + CtrlSpec, + AddControlledT, + _ControlledBase, + make_ctrl_system_with_correct_metabloq, +) from ._infra.bloq_example import BloqExample, bloq_example, BloqDocSpec From e699973efb6f2bffd6d8b153799074855c146c5b Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:35:51 -0800 Subject: [PATCH 30/68] init --- qualtran/_infra/bloq.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 5c726cef77..12d6de8b83 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -16,7 +16,18 @@ """Contains the main interface for defining `Bloq`s.""" import abc -from typing import Callable, Dict, List, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union +from typing import ( + Callable, + Dict, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + TYPE_CHECKING, + Union, +) if TYPE_CHECKING: import cirq From 85907da865e652c1345f9763be385289edbe305a Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:36:01 -0800 Subject: [PATCH 31/68] controlled docs --- qualtran/Controlled.ipynb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/qualtran/Controlled.ipynb b/qualtran/Controlled.ipynb index 9a495093de..069c6045a1 100644 --- a/qualtran/Controlled.ipynb +++ b/qualtran/Controlled.ipynb @@ -7,11 +7,13 @@ "source": [ "# Controlled\n", "\n", - "The controlled protocol lets you request a 'controlled' version of a bloq.\n", + "The controlled protocol lets you request a *controlled* version of a bloq: a bloq that is only active based on the status of another quantum bit or register.\n", "\n", - "A key feature of any program is the ability to do branching. That is, choose what operations to do based on input. The primitive branching feature in quantum algorithms is the idea of a controlled bloq.\n", + "A key feature of any program is the ability to do branching. That is, choosing what operations to do based on input. Controlled bloqs give the quantum programmer access to branching.\n", "\n", - "In its simplest form, a control bit is specified as an (additional) input to a bloq and the bloq is active when the control input is in the $|1\\rangle$ state. Otherwise, the bloq's operation is not performed (said another way: the Identity bloq is performed). The control input can be in superposition. " + "In its simplest form, a control bit is specified as an (additional) input to a bloq and the bloq is active when the control input is in the $|1\\rangle$ state. Otherwise, the bloq's operation is not performed. Equivalently, the control bit toggles between the bloq and the `Identity` operation.\n", + "\n", + "Control bits can be in quantum superposition. The result is a wavefunction containaing simultaneous branches where the controlled operation was performed and not performed. " ] }, { @@ -37,11 +39,11 @@ "source": [ "## Interface\n", "\n", - "The method for accessing the controlled version of a bloq is calling `Bloq.controlled(ctrl_spec)`. `ctrl_spec` is an instance of `CtrlSpec` which specifies how to control the bloq. \n", + "The method for accessing the controlled version of a bloq is calling `Bloq.controlled(ctrl_spec)`. `ctrl_spec` is an instance of `qualtran.CtrlSpec` which specifies the condition under which the bloq is active. \n", "\n", - "`CtrlSpec` supports additional control specifications:\n", + "`CtrlSpec` supports $|1\\rangle$-state individual control bits, as well as:\n", " 1. 'negative' controls where the bloq is active if the input is |0>.\n", - " 2. integer-equality controls where a `bitsize`-sized input must match an integer control value.\n", + " 2. integer-equality controls where an input of an arbitrary `QCDType` must match a compile-time control value.\n", " 3. ndarrays of control values, where the bloq is active if **all** inputs are active.\n", " 4. Multiple control registers, control values for each of which can be specified\n", " using 1-3 above.\n", From 96f2cbb8c6a26f3391b67f02360bb06931371b60 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 16:39:42 -0800 Subject: [PATCH 32/68] lint --- qualtran/simulation/classical_sim.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qualtran/simulation/classical_sim.py b/qualtran/simulation/classical_sim.py index 60a064f805..288dd0548e 100644 --- a/qualtran/simulation/classical_sim.py +++ b/qualtran/simulation/classical_sim.py @@ -13,7 +13,6 @@ # limitations under the License. """Functionality for the `Bloq.call_classically(...)` protocol.""" -import abc import itertools from typing import ( Any, @@ -29,7 +28,6 @@ Union, ) -import attrs import networkx as nx import numpy as np import sympy @@ -49,7 +47,7 @@ from qualtran._infra.composite_bloq import _binst_to_cxns if TYPE_CHECKING: - from qualtran import CompositeBloq, QCDType + from qualtran import QCDType ClassicalValT = Union[int, np.integer, NDArray[np.integer]] From 919bdd91a955ecddfc981b0ca5ad927853f8cc86 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Tue, 4 Mar 2025 17:06:14 -0800 Subject: [PATCH 33/68] fixes --- qualtran/_infra/bloq.py | 2 +- .../bloqs/qubitization/qubitization_walk_operator_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 12d6de8b83..9c175f9dc3 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -403,7 +403,7 @@ def _my_add_controlled( add_controlled: A function with the signature documented above that the system can use to automatically wire up the new control registers. """ - from qualtran._infra.controlled import make_ctrl_system_with_correct_metabloq + from qualtran import make_ctrl_system_with_correct_metabloq return make_ctrl_system_with_correct_metabloq(self, ctrl_spec=ctrl_spec) diff --git a/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py b/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py index 37cf3dab09..8e1d4ded53 100644 --- a/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py +++ b/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py @@ -16,7 +16,7 @@ import numpy as np import pytest -from qualtran import Adjoint, Controlled +from qualtran import Adjoint, _ControlledBase from qualtran.bloqs.basic_gates import Power, XGate, ZGate from qualtran.bloqs.chemistry.ising.walk_operator import get_walk_operator_for_1d_ising_model from qualtran.bloqs.multiplexers.select_pauli_lcu import SelectPauliLCU @@ -177,7 +177,7 @@ def pred(binst): keep = binst.bloq_is(bloqs_to_keep) if binst.bloq_is(Adjoint): keep |= isinstance(binst.bloq.subbloq, bloqs_to_keep) - if binst.bloq_is(Controlled) and isinstance(binst.bloq.subbloq, (XGate, ZGate)): + if binst.bloq_is(_ControlledBase) and isinstance(binst.bloq.subbloq, (XGate, ZGate)): keep = True return not keep From 40ef237fed98139c6c8c1b234841686cae001952 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 5 Mar 2025 11:17:25 -0800 Subject: [PATCH 34/68] format --- qualtran/bloqs/qubitization/qubitization_walk_operator_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py b/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py index 8e1d4ded53..b34b1227b6 100644 --- a/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py +++ b/qualtran/bloqs/qubitization/qubitization_walk_operator_test.py @@ -16,7 +16,7 @@ import numpy as np import pytest -from qualtran import Adjoint, _ControlledBase +from qualtran import _ControlledBase, Adjoint from qualtran.bloqs.basic_gates import Power, XGate, ZGate from qualtran.bloqs.chemistry.ising.walk_operator import get_walk_operator_for_1d_ising_model from qualtran.bloqs.multiplexers.select_pauli_lcu import SelectPauliLCU From 22e3184054b6adc3920ab820e8bfc5cfd5ac3332 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 5 Mar 2025 11:18:02 -0800 Subject: [PATCH 35/68] autogenerate notebooks --- qualtran/bloqs/bookkeeping/bookkeeping.ipynb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qualtran/bloqs/bookkeeping/bookkeeping.ipynb b/qualtran/bloqs/bookkeeping/bookkeeping.ipynb index a439c2b61d..f5f330779e 100644 --- a/qualtran/bloqs/bookkeeping/bookkeeping.ipynb +++ b/qualtran/bloqs/bookkeeping/bookkeeping.ipynb @@ -614,18 +614,18 @@ }, "source": [ "## `Cast`\n", - "Cast a register from one n-bit QDType to another QDType.\n", + "Cast a register from one n-bit QCDType to another QCDType.\n", "\n", - "This re-interprets the register's data type from `inp_dtype` to `out_dtype`.\n", + "This simply re-interprets the register's data, and is a bookkeeping operation.\n", "\n", "#### Parameters\n", - " - `inp_dtype`: Input QDType to cast from.\n", - " - `out_dtype`: Output QDType to cast to.\n", - " - `shape`: shape of the register to cast. \n", + " - `inp_dtype`: Input QCDType to cast from.\n", + " - `out_dtype`: Output QCDType to cast to.\n", + " - `shape`: Optional multidimensional shape of the register to cast.\n", + " - `allow_quantum_to_classical`: Whether to allow (potentially dangerous) casting a quantum value to a classical value and vice versa. If you cast a classical bit to a qubit that was originally obtained by casting a qubit to a classical bit, the program will model unphysical quantum coherences that can give you fundamentally incorrect resource estimates. Use a measurement operation to convert a qubit to a classical bit (and correctly model the decoherence that results). \n", "\n", "#### Registers\n", - " - `in`: input register to cast from.\n", - " - `out`: input register to cast to.\n" + " - `reg`: The quantum variable to cast\n" ] }, { From 70bac9af47759f24a5748b1ab6baab5e4b786b5b Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 5 Mar 2025 11:36:45 -0800 Subject: [PATCH 36/68] add notebook to toc --- docs/bloq_infra.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/bloq_infra.rst b/docs/bloq_infra.rst index 82d18b9104..1ad58e8c1c 100644 --- a/docs/bloq_infra.rst +++ b/docs/bloq_infra.rst @@ -13,6 +13,7 @@ types (``Register``), and algorithms (``CompositeBloq``). _infra/Bloqs-Tutorial.ipynb Protocols.ipynb + DataTypes.ipynb simulation/classical_sim.ipynb simulation/tensor.ipynb resource_counting/call_graph.ipynb From 6e93114a46b78c69d78dde31c2871c8394be85f8 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 5 Mar 2025 11:43:39 -0800 Subject: [PATCH 37/68] fix musical score serialization --- qualtran/drawing/musical_score.py | 6 ++++-- qualtran/drawing/musical_score_test.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/qualtran/drawing/musical_score.py b/qualtran/drawing/musical_score.py index 5c3f0dbaa4..065f2312c4 100644 --- a/qualtran/drawing/musical_score.py +++ b/qualtran/drawing/musical_score.py @@ -102,8 +102,10 @@ class HLine: seq_x_end: Optional[int] = None flavor: HLineFlavor = HLineFlavor.QUANTUM - def json_dict(self): - return attrs.asdict(self) + def json_dict(self) -> Dict[str, Any]: + d = attrs.asdict(self) + d['flavor'] = str(d['flavor']) + return d class LineManager: diff --git a/qualtran/drawing/musical_score_test.py b/qualtran/drawing/musical_score_test.py index 147aa3f9f5..db35be86a8 100644 --- a/qualtran/drawing/musical_score_test.py +++ b/qualtran/drawing/musical_score_test.py @@ -13,9 +13,25 @@ # limitations under the License. import pytest +from qualtran.bloqs.mcmt import MultiAnd +from qualtran.drawing import dump_musical_score, get_musical_score_data, HLine from qualtran.testing import execute_notebook +def test_dump_json(tmp_path): + hline = HLine(y=10, seq_x_start=5, seq_x_end=6) + assert hline.json_dict() == { + 'y': 10, + 'seq_x_start': 5, + 'seq_x_end': 6, + 'flavor': 'HLineFlavor.QUANTUM', + } + + cbloq = MultiAnd((1, 1, 0, 1)).decompose_bloq() + msd = get_musical_score_data(cbloq) + dump_musical_score(msd, name=f'{tmp_path}/musical_score_example') + + @pytest.mark.notebook def test_notebook(): execute_notebook('musical_score') From 8951104fb9839ac7ebbdaf04d6e27d13531da6a2 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 5 Mar 2025 15:24:08 -0800 Subject: [PATCH 38/68] phased classical sim --- qualtran/_infra/bloq.py | 7 +- qualtran/_infra/controlled.py | 16 +- qualtran/bloqs/basic_gates/z_basis.py | 10 + qualtran/bloqs/basic_gates/z_basis_test.py | 3 + qualtran/simulation/classical_sim.ipynb | 229 +++++++++--- qualtran/simulation/classical_sim.py | 383 ++++++++++++++++----- qualtran/simulation/classical_sim_test.py | 12 +- 7 files changed, 524 insertions(+), 136 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 9c175f9dc3..8f487bf6cb 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -57,7 +57,7 @@ GeneralizerT, SympySymbolAllocator, ) - from qualtran.simulation.classical_sim import ClassicalValT + from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT def _decompose_from_build_composite_bloq(bloq: 'Bloq') -> 'CompositeBloq': @@ -181,7 +181,7 @@ def adjoint(self) -> 'Bloq': def on_classical_vals( self, **vals: Union['sympy.Symbol', 'ClassicalValT'] - ) -> Mapping[str, 'ClassicalValT']: + ) -> Mapping[str, 'ClassicalValRetT']: """How this bloq operates on classical data. Override this method if your bloq represents classical, reversible logic. For example: @@ -212,6 +212,9 @@ def on_classical_vals( except NotImplementedError as e: raise NotImplementedError(f"{self} does not support classical simulation: {e}") from e + def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: + return None + def call_classically( self, **vals: Union['sympy.Symbol', 'ClassicalValT'] ) -> Tuple['ClassicalValT', ...]: diff --git a/qualtran/_infra/controlled.py b/qualtran/_infra/controlled.py index 7d0d5fa59e..0e99500728 100644 --- a/qualtran/_infra/controlled.py +++ b/qualtran/_infra/controlled.py @@ -47,7 +47,7 @@ from qualtran.cirq_interop import CirqQuregT from qualtran.drawing import WireSymbol from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator - from qualtran.simulation.classical_sim import ClassicalValT + from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT ControlBit: TypeAlias = int """A control bit, either 0 or 1.""" @@ -432,7 +432,7 @@ def signature(self) -> 'Signature': # Prepend register(s) corresponding to `ctrl_spec`. return Signature(self.ctrl_regs + tuple(self.subbloq.signature)) - def on_classical_vals(self, **vals: 'ClassicalValT') -> Mapping[str, 'ClassicalValT']: + def on_classical_vals(self, **vals: 'ClassicalValT') -> Mapping[str, 'ClassicalValRetT']: """Classical action of controlled bloqs. This involves conditionally doing the classical action of `subbloq`. All implementers @@ -453,6 +453,18 @@ def on_classical_vals(self, **vals: 'ClassicalValT') -> Mapping[str, 'ClassicalV return vals + def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: + """Phasing action of controlled bloqs. + + This involves conditionally doing the phasing action of `subbloq`. All implementers + of `_ControlledBase` should provide a decomposition that satisfies this phase funciton. + """ + ctrl_vals = [vals[reg_name] for reg_name in self.ctrl_reg_names] + other_vals = {reg.name: vals[reg.name] for reg in self.subbloq.signature} + if self.ctrl_spec.is_active(*ctrl_vals): + return self.subbloq.basis_state_phase(**other_vals) + return None + def _tensor_data(self): """Dense tensor encoding a controlled unitary. diff --git a/qualtran/bloqs/basic_gates/z_basis.py b/qualtran/bloqs/basic_gates/z_basis.py index 599acfc898..5736f8cfb9 100644 --- a/qualtran/bloqs/basic_gates/z_basis.py +++ b/qualtran/bloqs/basic_gates/z_basis.py @@ -50,6 +50,7 @@ from qualtran.cirq_interop import CirqQuregT from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator + from qualtran.simulation.classical_sim import ClassicalValT _ZERO = np.array([1, 0], dtype=np.complex128) _ONE = np.array([0, 1], dtype=np.complex128) @@ -347,6 +348,15 @@ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> Tuple['Bloq', 'AddControlled self, ctrl_spec, current_ctrl_bit=1, bloq_with_ctrl=self, ctrl_reg_name='q1' ) + def on_classical_vals(self, **vals: 'ClassicalValT') -> Dict[str, 'ClassicalValT']: + # Diagonal, but causes phases: see `basis_state_phase` + return vals + + def basis_state_phase(self, q1: int, q2: int) -> Optional[complex]: + if q1 == 1 and q2 == 1: + return -1 + return 1 + @bloq_example def _cz() -> CZ: diff --git a/qualtran/bloqs/basic_gates/z_basis_test.py b/qualtran/bloqs/basic_gates/z_basis_test.py index dbe83c9c39..267518a96b 100644 --- a/qualtran/bloqs/basic_gates/z_basis_test.py +++ b/qualtran/bloqs/basic_gates/z_basis_test.py @@ -232,3 +232,6 @@ def test_cz_manual(): assert ZGate().controlled() == CZ() assert t_complexity(cz) == TComplexity(clifford=1) + + with pytest.raises(ValueError, match='.*phase.*'): + cz.call_classically(q1=1, q2=1) diff --git a/qualtran/simulation/classical_sim.ipynb b/qualtran/simulation/classical_sim.ipynb index c3760b3b5b..9f99d7e8d8 100644 --- a/qualtran/simulation/classical_sim.ipynb +++ b/qualtran/simulation/classical_sim.ipynb @@ -13,28 +13,184 @@ { "cell_type": "code", "execution_count": null, - "id": "689149fc", + "id": "e8d6c2a3-ed22-459d-9bcb-959ebf9a3e0c", "metadata": {}, "outputs": [], "source": [ - "from typing import *\n", + "from qualtran.bloqs.basic_gates import CNOT\n", + "\n", + "cnot = CNOT()\n", + "cnot.call_classically(ctrl=1, target=0)" + ] + }, + { + "cell_type": "markdown", + "id": "0b3cb890-627f-4095-8970-18a891fe1fa8", + "metadata": {}, + "source": [ + "## Interface\n", + "\n", + "The primary way to simulate the classical action of a bloq is through the `Bloq.call_classically` method. This takes classical values for each left- or thru-register and returns a classical value for each right- or thru-register (in the order of the bloq's signature). \n", + "\n", + "The functionality for this method is contained in the `qualtran.simulation.classical_sim` module. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "612eeda6-11d8-4ee1-ba12-85aefe6a1e29", + "metadata": {}, + "outputs": [], + "source": [ + "import itertools\n", + "for c, t in itertools.product([0,1], repeat=2):\n", + " out_c, out_t = CNOT().call_classically(ctrl=c, target=t)\n", + " print(f'{c}{t} -> {out_c}{out_t}')" + ] + }, + { + "cell_type": "markdown", + "id": "1c9d1041-67ad-4c5b-b836-70766eb65ee7", + "metadata": {}, + "source": [ + "## Additional Functionality" + ] + }, + { + "cell_type": "markdown", + "id": "4e020b40-2bf0-47f5-86b2-96d05ee3a4d4", + "metadata": {}, + "source": [ + "### Consistent classical action\n", "\n", + "The primary method of testing the classical action of a bloq is comparing the classical action from the bloq's decomposition with its directly annotated, reference action. For example: the `Add` is annotated with a method override that directly adds two numbers. This is the reference classical action for addition\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ecc2944-21cc-4127-9f2f-fd5814472963", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import QUInt\n", + "from qualtran.bloqs.arithmetic import Add\n", + "\n", + "add = Add(QUInt(8))\n", + "add.call_classically(a=5, b=7)" + ] + }, + { + "cell_type": "markdown", + "id": "5eb82a5b-c43e-4872-a4b8-c0c5886bb547", + "metadata": {}, + "source": [ + "The `Add` bloq also is annotated with a decomposition that we would like to validate. We can check various input/output pairs against the reference implementation to gain confidence that the decomposition is correct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96f02ca2-4a33-4fa2-864f-a07a6f94c909", + "metadata": {}, + "outputs": [], + "source": [ "import numpy as np\n", - "from attrs import frozen\n", - "from numpy.typing import NDArray\n", + "rs = np.random.RandomState(seed=52)\n", "\n", - "from qualtran import Bloq, BloqBuilder, Register, Signature, Side\n", - "from qualtran.drawing import show_bloq" + "add_cbloq = add.decompose_bloq()\n", + "\n", + "for _ in range(10):\n", + " a = rs.randint(256)\n", + " b = rs.randint(256)\n", + " ref_vals = add.call_classically(a=a, b=b)\n", + " decomp_vals = add_cbloq.call_classically(a=a, b=b)\n", + " assert ref_vals == decomp_vals\n", + " print('✓', end='')" + ] + }, + { + "cell_type": "markdown", + "id": "dd945628-d120-4dbe-b84f-721a668f555e", + "metadata": {}, + "source": [ + "### `assert_consistent_classical_action`\n", + "\n", + "The idea above is encapsulated in an exhaustive testing function. You can use properties of the quantum data types involved to make the function quite general" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8caab611-4bca-409a-98b2-147cf8b8db39", + "metadata": {}, + "outputs": [], + "source": [ + "import qualtran.testing as qlt_testing\n", + "\n", + "dtype = QUInt(2)\n", + "qlt_testing.assert_consistent_classical_action(\n", + " bloq=Add(dtype), \n", + " a=dtype.get_classical_domain(),\n", + " b=dtype.get_classical_domain(),\n", + ")\n", + "print('✓')" ] }, { "cell_type": "markdown", - "id": "71193328", + "id": "c47c770e-b2c6-40fa-b355-58866195311e", + "metadata": {}, + "source": [ + "### Printing truth tables\n", + "\n", + "There are functions provided to quickly print the truth table of a bloq." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6f114c2-3a9d-40f4-83ae-f184b4b7ef02", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.simulation.classical_sim import get_classical_truth_table, format_classical_truth_table\n", + "from qualtran.bloqs.arithmetic import Add\n", + "from qualtran import QUInt\n", + "\n", + "add = Add(QUInt(2))\n", + "print(format_classical_truth_table(*get_classical_truth_table(add)))" + ] + }, + { + "cell_type": "markdown", + "id": "829aff81-c396-46a5-8918-c251fb10f38e", + "metadata": {}, + "source": [ + "## Implementation\n", + "\n", + "The `qualtran.simulation.classical_sim` functions rely on `Bloq.on_classical_vals` overrides to implement the protocol. This method encodes a bloq's classical action.\n", + "\n", + "Bloq authors should aim to always provide a `on_classical_vals` override if the bloq implements a classical function. The override on high-level bloqs can serve as a reference implementation for testing decompositions, see above.\n", + "\n", + "A simple classical gate is the controlled not. This flips the target bit if `ctrl` is set. We'll implement `on_classical_vals` to encode this behavior" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "689149fc", "metadata": {}, + "outputs": [], "source": [ - "## CNOT\n", + "from typing import *\n", "\n", - "The simplest classical gate is the controlled not. This flips the target bit if `ctrl` is set. We can implement the `on_classical_vals` method to encode this behavior." + "import numpy as np\n", + "from attrs import frozen\n", + "from numpy.typing import NDArray\n", + "\n", + "from qualtran import Bloq, BloqBuilder, Register, Signature, Side\n", + "from qualtran.drawing import show_bloq" ] }, { @@ -62,30 +218,26 @@ "id": "62a974c9", "metadata": {}, "source": [ - "We can call the Bloq on classical inputs by using `Bloq.call_classically()`. " + "We can call the Bloq on classical inputs by using `Bloq.call_classically()`. Below, we inspect the truth table." ] }, { "cell_type": "code", "execution_count": null, - "id": "84d2ee13", + "id": "1c9d022e-6169-4f7b-b424-d27cd430f4c4", "metadata": {}, "outputs": [], "source": [ - "CNOTExample().call_classically(ctrl=1, target=0)" + "my_cnot = CNOTExample()\n", + "print(format_classical_truth_table(*get_classical_truth_table(my_cnot)))" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "4d5fd4dc", + "cell_type": "markdown", + "id": "d685f136-6e5e-4d0c-83a1-e1c38ec2b498", "metadata": {}, - "outputs": [], "source": [ - "import itertools\n", - "for c, t in itertools.product([0,1], repeat=2):\n", - " out_c, out_t = CNOTExample().call_classically(ctrl=c, target=t)\n", - " print(f'{c}{t} -> {out_c}{out_t}')" + "## Properties and Relations" ] }, { @@ -107,43 +259,38 @@ "source": [ "from qualtran.drawing import ClassicalSimGraphDrawer\n", "\n", - "drawer = ClassicalSimGraphDrawer(CNOTExample(), vals=dict(ctrl=1, target=0))\n", + "drawer = ClassicalSimGraphDrawer(Add(QUInt(2)).decompose_bloq(), vals=dict(a=3, b=2))\n", "drawer.get_svg()" ] }, + { + "cell_type": "markdown", + "id": "ef352653-4b7e-4735-b8af-b78de0320426", + "metadata": {}, + "source": [ + "### Quantum Data Types\n", + "\n", + "To convert back and forth between classical values and bitstrings, we use the `QDType.to_bits` and `QDType.from_bits` functions. Qualtran uses a big-endian convention. The most significant bit is at index 0." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "8b62e282", + "id": "c0a52e07-d117-447e-b171-3e2d16cb991f", "metadata": {}, "outputs": [], "source": [ - "# Build a SWAP circuit\n", - "bb = BloqBuilder()\n", - "q0 = bb.add_register('q0', 1)\n", - "q1 = bb.add_register('q1', 1)\n", - "q0, q1 = bb.add(CNOTExample(), ctrl=q0, target=q1)\n", - "q1, q0 = bb.add(CNOTExample(), ctrl=q1, target=q0)\n", - "q0, q1 = bb.add(CNOTExample(), ctrl=q0, target=q1)\n", - "cbloq = bb.finalize(q0=q0, q1=q1)\n", - "\n", - "drawer = ClassicalSimGraphDrawer(cbloq, vals=dict(q0=1, q1=0))\n", - "drawer.get_svg()" + "QUInt(8).to_bits(254)" ] }, { "cell_type": "markdown", - "id": "10", + "id": "be896d15-f576-4113-b81c-306b0457dd23", "metadata": {}, "source": [ - "## Quantum Data Types\n", - "\n", - "To convert back and forth between classical values and bitstrings, we use the `QDType.to_bits` and `QDType.from_bits` functions.\n", - "\n", - "\n", "### QFxp classical values\n", "\n", - "Currently, QFxp classical values are represented as fixed-width integers.\n", + "Due to technical limitations, `QFxp` classical values are represented as fixed-width integers.\n", "See the class docstring for QFxp for precise details.\n", "To convert from true floating point values to this representation and vice-versa,\n", "users can use `QFxp.to_fixed_width_int` and `QFxp.float_from_fixed_width_int` respectively." @@ -200,7 +347,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/qualtran/simulation/classical_sim.py b/qualtran/simulation/classical_sim.py index 288dd0548e..a723f244bd 100644 --- a/qualtran/simulation/classical_sim.py +++ b/qualtran/simulation/classical_sim.py @@ -36,7 +36,6 @@ from qualtran import ( Bloq, BloqInstance, - Connection, DanglingT, LeftDangle, Register, @@ -47,9 +46,10 @@ from qualtran._infra.composite_bloq import _binst_to_cxns if TYPE_CHECKING: - from qualtran import QCDType + from qualtran import CompositeBloq, QCDType ClassicalValT = Union[int, np.integer, NDArray[np.integer]] +ClassicalValRetT = Union[int, np.integer, NDArray[np.integer]] def _numpy_dtype_from_qlt_dtype(dtype: 'QCDType') -> Type: @@ -106,79 +106,283 @@ def _get_in_vals( return arg -def _update_assign_from_vals( - regs: Iterable[Register], - binst: Union[DanglingT, BloqInstance], - vals: Union[Dict[str, Union[sympy.Symbol, ClassicalValT]], Dict[str, ClassicalValT]], - soq_assign: Union[ - Dict[Soquet, Union[sympy.Symbol, ClassicalValT]], Dict[Soquet, ClassicalValT] - ], -): - """Update `soq_assign` using `vals`. +class _ClassicalSimState: + """A mutable class for classically simulating composite bloqs. + + Consider using the public method `Bloq.call_classically(...)` for a simple interface + for classical simulation. + + The `.step()` and `.finalize()` methods provide fine-grained control over the progress + of the simulation; or the `.simulate()` method will step through the entire composite bloq. + + Args: + signature: The signature of the composite bloq. + binst_graph: The directed-graph form of the composite bloq. Consider constructing + this class with the `.from_cbloq` constructor method to correctly generate the + binst graph. + vals: A mapping of input register name to classical value to serve as inputs to the + procedure. + + Attributes: + soq_assign: An assignment of soquets to classical values. We store the classical state + of each soquet (wire connection point in the compute graph) for debugging and/or + visualization. After stepping through each bloq instance, the right-dangling soquet + are assigned the output classical values + last_binst: A record of the last bloq instance we processed during simulation. This + can be used in concert with `.step()` for debugging. - This helper function is responsible for error checking. We use `regs` to make sure all the - keys are present in the vals dictionary. We check the classical value shapes, types, and - ranges. """ - for reg in regs: - debug_str = f'{binst}.{reg.name}' - try: - val = vals[reg.name] - except KeyError as e: - raise ValueError(f"{binst} requires an input register named {reg.name}") from e - if reg.shape: - # `val` is an array - val = np.asanyarray(val) - if val.shape != reg.shape: - raise ValueError( - f"Incorrect shape {val.shape} received for {debug_str}. " f"Want {reg.shape}." - ) - reg.dtype.assert_valid_classical_val_array(val, debug_str) - - for idx in reg.all_idxs(): - soq = Soquet(binst, reg, idx=idx) - soq_assign[soq] = val[idx] - - elif isinstance(val, sympy.Expr): - # `val` is symbolic - soq = Soquet(binst, reg) - soq_assign[soq] = val # type: ignore[assignment] + def __init__( + self, + signature: 'Signature', + binst_graph: nx.DiGraph, + vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]], + ): + self._signature = signature + self._binst_graph = binst_graph + self._binst_iter = nx.topological_sort(self._binst_graph) + + # Keep track of each soquet's bit array. Initialize with LeftDangle + self.soq_assign: Dict[Soquet, ClassicalValT] = {} + self._update_assign_from_vals(self._signature.lefts(), LeftDangle, dict(vals)) + + self.last_binst = None + + @classmethod + def from_cbloq( + cls, cbloq: 'CompositeBloq', vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]] + ) -> '_ClassicalSimState': + """Initiate a classical simulation from a CompositeBloq. + + Args: + cbloq: The composite bloq + vals: A mapping of input register name to classical value to serve as inputs to the + procedure. + + Returns: + A new classical sim state. + + """ + return cls(signature=cbloq.signature, binst_graph=cbloq._binst_graph, vals=vals) + + def _update_assign_from_vals( + self, + regs: Iterable[Register], + binst: Union[DanglingT, BloqInstance], + vals: Union[Dict[str, Union[sympy.Symbol, ClassicalValT]], Dict[str, ClassicalValT]], + ) -> None: + """Update `self.soq_assign` using `vals`. + + This helper function is responsible for error checking. We use `regs` to make sure all the + keys are present in the vals dictionary. We check the classical value shapes, types, and + ranges. + """ + for reg in regs: + debug_str = f'{binst}.{reg.name}' + try: + val = vals[reg.name] + except KeyError as e: + raise ValueError(f"{binst} requires a {reg.side} register named {reg.name}") from e + + if reg.shape: + # `val` is an array + val = np.asanyarray(val) + if val.shape != reg.shape: + raise ValueError( + f"Incorrect shape {val.shape} received for {debug_str}. " + f"Want {reg.shape}." + ) + reg.dtype.assert_valid_classical_val_array(val, debug_str) + + for idx in reg.all_idxs(): + soq = Soquet(binst, reg, idx=idx) + self.soq_assign[soq] = val[idx] + + elif isinstance(val, sympy.Expr): + # `val` is symbolic + soq = Soquet(binst, reg) + self.soq_assign[soq] = val # type: ignore[assignment] + + else: + # `val` is one value. + reg.dtype.assert_valid_classical_val(val, debug_str) + soq = Soquet(binst, reg) + self.soq_assign[soq] = val + + def _binst_on_classical_vals(self, binst, in_vals) -> None: + """Call `on_classical_vals` on a given bloq instance.""" + bloq = binst.bloq + + out_vals = bloq.on_classical_vals(**in_vals) + if not isinstance(out_vals, dict): + raise TypeError( + f"{bloq.__class__.__name__}.on_classical_vals should return a dictionary." + ) + self._update_assign_from_vals(bloq.signature.rights(), binst, out_vals) + + def _binst_basis_state_phase(self, binst, in_vals) -> None: + """Call `basis_state_phase` on a given bloq instance. + + This base simulation class will raise an error if the bloq reports any phasing. + This method is overwritten in `_PhasedClassicalSimState` to support phasing. + """ + bloq = binst.bloq + bloq_phase = bloq.basis_state_phase(**in_vals) + if bloq_phase is not None: + raise ValueError( + f"{bloq} imparts a phase, and can't be simulated purely classically. Consider TODO" + ) + + def step(self) -> '_ClassicalSimState': + """Advance the simulation by one bloq instance. + + After calling this method, `self.last_binst` will contain the bloq instance that + was just simulated. `self.soq_assign` and any other state variables will be updated. + + Returns: + self + """ + binst = next(self._binst_iter) + self.last_binst = binst + if isinstance(binst, DanglingT): + return self + pred_cxns, succ_cxns = _binst_to_cxns(binst, binst_graph=self._binst_graph) - else: - # `val` is one value. - reg.dtype.assert_valid_classical_val(val, debug_str) - soq = Soquet(binst, reg) - soq_assign[soq] = val + # Track inter-Bloq name changes + for cxn in pred_cxns: + self.soq_assign[cxn.right] = self.soq_assign[cxn.left] + def _in_vals(reg: Register): + # close over binst and `soq_assign` + return _get_in_vals(binst, reg, soq_assign=self.soq_assign) -def _binst_on_classical_vals( - binst: BloqInstance, pred_cxns: Iterable[Connection], soq_assign: Dict[Soquet, ClassicalValT] -): - """Call `on_classical_vals` on a given binst. + bloq = binst.bloq + in_vals = {reg.name: _in_vals(reg) for reg in bloq.signature.lefts()} - Args: - binst: The bloq instance whose bloq we will call `on_classical_vals`. - pred_cxns: Predecessor connections for the bloq instance. - soq_assign: Current assignment of soquets to classical values. - """ + # Apply methods + self._binst_on_classical_vals(binst, in_vals) + self._binst_basis_state_phase(binst, in_vals) + return self + + def finalize(self) -> Dict[str, 'ClassicalValT']: + """Finish simulating a composite bloq and extract final values. + + Returns: + final_vals: The final classical values, keyed by the RIGHT register names of the + composite bloq. + + Raises: + KeyError if `.step()` has not been called for each bloq instance. + """ + + # Track bloq-to-dangle name changes + if len(list(self._signature.rights())) > 0: + final_preds, _ = _binst_to_cxns(RightDangle, binst_graph=self._binst_graph) + for cxn in final_preds: + self.soq_assign[cxn.right] = self.soq_assign[cxn.left] + + # Formulate output with expected API + def _f_vals(reg: Register): + return _get_in_vals(RightDangle, reg, soq_assign=self.soq_assign) + + final_vals = {reg.name: _f_vals(reg) for reg in self._signature.rights()} + return final_vals + + def simulate(self) -> Dict[str, 'ClassicalValT']: + """Simulate the composite bloq and return the final values.""" + try: + while True: + self.step() + except StopIteration: + return self.finalize() - # Track inter-Bloq name changes - for cxn in pred_cxns: - soq_assign[cxn.right] = soq_assign[cxn.left] - def _in_vals(reg: Register): - # close over binst and `soq_assign` - return _get_in_vals(binst, reg, soq_assign=soq_assign) +class _PhasedClassicalSimState(_ClassicalSimState): + """A mutable class for classically simulating composite bloqs with phase tracking. - bloq = binst.bloq - in_vals = {reg.name: _in_vals(reg) for reg in bloq.signature.lefts()} + Consider using TODO - # Apply function - out_vals = bloq.on_classical_vals(**in_vals) - if not isinstance(out_vals, dict): - raise TypeError(f"{bloq.__class__.__name__}.on_classical_vals should return a dictionary.") - _update_assign_from_vals(bloq.signature.rights(), binst, out_vals, soq_assign) + This simulation scheme supports a class of circuits containing only: + - classical operations corresponding to permutation matrices in the computational basis + - phase-like operations corresponding to diagonal matrices in the computational basis. + - X-basis measurement. + + In general, you cannot delete quantum data and must "uncompute" bits by adding inverse, + ("adjoint") operations to return variables to known states. Measurement based + uncomputation (MBUC) is a trick that uses X-basis measurement to _nearly_ remove quantum + data. Performing an X-basis measurement will not destroy computational-basis coherence + but it will generate phases on your remaining qubits. These phases are tracked by + this simulator; but note that they are classically stochastic. + + A bloq that generates a negative phase based on its measurement result can return a + `MeasurementPhase` object in its `Bloq.basis_state_phase` method. This simulator will + correctly apply a -1 phase only if the measurement result was 1. + + Args: + signature: The signature of the composite bloq. + binst_graph: The directed-graph form of the composite bloq. Consider constructing + this class with the `.from_cbloq` constructor method to correctly generate the + binst graph. + vals: A mapping of input register name to classical value to serve as inputs to the + procedure. + phase: The initial phase. It must be a valid phase: a complex number with unit modulus. + + Attributes: + soq_assign: An assignment of soquets to classical values. + last_binst: A record of the last bloq instance we processed during simulation. + phase: The current phase of the simulation state. + + References: + [Verifying Measurement Based Uncomputation](https://algassert.com/post/1903). + Gidney. 2019. + """ + + def __init__( + self, + signature: 'Signature', + binst_graph: nx.DiGraph, + vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]], + *, + phase: complex = 1.0, + ): + super().__init__(signature=signature, binst_graph=binst_graph, vals=vals) + _assert_valid_phase(phase) + self.phase = phase + + @classmethod + def from_cbloq( + cls, cbloq: 'CompositeBloq', vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]] + ) -> '_PhasedClassicalSimState': + """Initiate a classical simulation from a CompositeBloq. + + Args: + cbloq: The composite bloq + vals: A mapping of input register name to classical value to serve as inputs to the + procedure. + + Returns: + A new classical sim state. + + """ + + return cls(signature=cbloq.signature, binst_graph=cbloq._binst_graph, vals=vals) + + def _binst_basis_state_phase(self, binst, in_vals): + """Call `basis_state_phase` on a given bloq instance. + + If this method returns a value, the current phase will be updated. Otherwise, we + leave the phase as-is. If the method returns `MeasurementPhase`, we employ the + special case described in TODO. + """ + bloq = binst.bloq + bloq_phase = bloq.basis_state_phase(**in_vals) + if bloq_phase is not None: + _assert_valid_phase(bloq_phase) + self.phase *= bloq_phase + else: + # Purely classical bloq; phase of 1 + pass def call_cbloq_classically( @@ -202,29 +406,36 @@ def call_cbloq_classically( corresponding to thru registers will be mapped to the *output* classical value. """ - # Keep track of each soquet's bit array. Initialize with LeftDangle - soq_assign: Dict[Soquet, ClassicalValT] = {} - _update_assign_from_vals(signature.lefts(), LeftDangle, dict(vals), soq_assign) + sim = _ClassicalSimState(signature, binst_graph, vals) + final_vals = sim.simulate() + return final_vals, sim.soq_assign - # Bloq-by-bloq application - for binst in nx.topological_sort(binst_graph): - if isinstance(binst, DanglingT): - continue - pred_cxns, succ_cxns = _binst_to_cxns(binst, binst_graph=binst_graph) - _binst_on_classical_vals(binst, pred_cxns, soq_assign) - - # Track bloq-to-dangle name changes - if len(list(signature.rights())) > 0: - final_preds, _ = _binst_to_cxns(RightDangle, binst_graph=binst_graph) - for cxn in final_preds: - soq_assign[cxn.right] = soq_assign[cxn.left] - - # Formulate output with expected API - def _f_vals(reg: Register): - return _get_in_vals(RightDangle, reg, soq_assign) - - final_vals = {reg.name: _f_vals(reg) for reg in signature.rights()} - return final_vals, soq_assign + +def _assert_valid_phase(p: complex, atol: float = 1e-8): + if np.abs(np.abs(p) - 1.0) > atol: + raise ValueError(f"Phases must have unit modulus. Found {p}.") + + +def do_phased_classical_simulation(bloq: 'Bloq', vals: Mapping[str, 'ClassicalValT']): + """Do a phased classical simulation of the bloq. + + This provides a simple interface to `_PhasedClassicalSimState`. Particularly advanced users + may wish to use that class directly. + + Args: + bloq: The bloq to simulate + vals: A mapping from input register name to initial classical values. The initial phase is + assumed to be 1.0. + + Returns: + final_vals: A mapping of output register name to final classical values. + phase: The final phase. + """ + cbloq = bloq.as_composite_bloq() + sim = _PhasedClassicalSimState.from_cbloq(cbloq, vals=vals) + final_vals = sim.simulate() + phase = sim.phase + return final_vals, phase def get_classical_truth_table( diff --git a/qualtran/simulation/classical_sim_test.py b/qualtran/simulation/classical_sim_test.py index 6073a50e4d..88bd7ecdb3 100644 --- a/qualtran/simulation/classical_sim_test.py +++ b/qualtran/simulation/classical_sim_test.py @@ -15,6 +15,7 @@ import itertools from typing import Dict +import networkx as nx import numpy as np import pytest from attrs import frozen @@ -39,7 +40,7 @@ ) from qualtran.bloqs.basic_gates import CNOT from qualtran.simulation.classical_sim import ( - _update_assign_from_vals, + _ClassicalSimState, add_ints, call_cbloq_classically, ClassicalValT, @@ -51,6 +52,7 @@ def test_dtype_validation(): # set up mocks for `_update_assign_from_vals` soq_assign: Dict[Soquet, ClassicalValT] = {} # gets assigned to; we discard in this test. binst = 'MyBinst' # binst is only used for error messages, so we can mock with a string + sim = _ClassicalSimState(Signature([]), nx.DiGraph(), {}) # set up different register dtypes regs = [ @@ -67,21 +69,21 @@ def test_dtype_validation(): 'bit_arr': np.array([1, 0, 1, 0, 1], dtype=np.uint8), 'int_arr': np.arange(5), } - _update_assign_from_vals(regs, binst, vals, soq_assign) # type: ignore[arg-type] + sim._update_assign_from_vals(regs, binst, vals) # type: ignore[arg-type] # bad integer vals2 = {**vals, 'one_bit_int': 2} with pytest.raises(ValueError, match=r'Bad QBit().*one_bit_int'): - _update_assign_from_vals(regs, binst, vals2, soq_assign) # type: ignore[arg-type] + sim._update_assign_from_vals(regs, binst, vals2) # type: ignore[arg-type] # int is a numpy int vals3 = {**vals, 'int': np.arange(5, dtype=np.uint8)[4]} - _update_assign_from_vals(regs, binst, vals3, soq_assign) # type: ignore[arg-type] + sim._update_assign_from_vals(regs, binst, vals3) # type: ignore[arg-type] # wrong shape vals4 = {**vals, 'int_arr': np.arange(6)} with pytest.raises(ValueError, match=r'Incorrect shape.*Want \(5,\)\.'): - _update_assign_from_vals(regs, binst, vals4, soq_assign) # type: ignore[arg-type] + sim._update_assign_from_vals(regs, binst, vals4) # type: ignore[arg-type] @frozen From 9e1bb8c0590e4f72309033613f5c81dd2dd8bff4 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 5 Mar 2025 15:40:45 -0800 Subject: [PATCH 39/68] docstring --- qualtran/_infra/bloq.py | 16 +++++++++++++++- qualtran/simulation/classical_sim_test.py | 7 ++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 8f487bf6cb..023cc934a2 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -187,7 +187,7 @@ def on_classical_vals( Override this method if your bloq represents classical, reversible logic. For example: quantum circuits composed of X and C^nNOT gates are classically simulable. - Bloq definers should override this method. If you already have an instance of a `Bloq`, + Bloq authors should override this method. If you already have an instance of a `Bloq`, consider calling `call_clasically(**vals)` which will do input validation before calling this function. @@ -213,6 +213,20 @@ def on_classical_vals( raise NotImplementedError(f"{self} does not support classical simulation: {e}") from e def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: + """How this bloq phases classical basis states. + + Override this method if your bloq represents classical logic with basis-state + dependent phase factors. This corresponds to bloqs whose matrix representation + (in the standard basis) is a generalized permutation matrix: a permutation matrix + where each entry can be +1, -1 or any complex number with unit absolute value. + Alternatively, this corresponds to bloqs composed from classical operations + (X, CNOT, Toffoli, ...) and diagonal operations (T, CZ, CCZ, ...). + + Bloq authors should override this method. If you are using an instantiated bloq object, + call TODO and not this method directly. + + If this method is implemented, `on_classical_vals` must also be implemented. + """ return None def call_classically( diff --git a/qualtran/simulation/classical_sim_test.py b/qualtran/simulation/classical_sim_test.py index 88bd7ecdb3..e7799e137a 100644 --- a/qualtran/simulation/classical_sim_test.py +++ b/qualtran/simulation/classical_sim_test.py @@ -13,7 +13,7 @@ # limitations under the License. import itertools -from typing import Dict +from typing import Dict, Union import networkx as nx import numpy as np @@ -100,6 +100,11 @@ def on_classical_vals(self, *, x: NDArray[np.uint8]) -> Dict[str, NDArray[np.uin return {'x': x, 'z': z} +class ApplyPhasedClassicalTest(Bloq): + def basis_state_phase(self, x: NDArray[np.uint8]) -> complex: + return np.exp(x * 1.0j / np.pi) + + def test_apply_classical(): bloq = ApplyClassicalTest() x, z = bloq.call_classically(x=np.zeros(5, dtype=np.uint8)) From 3a395f7525b5a914f72183786da36737594aed2c Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Mon, 10 Mar 2025 11:26:47 -0700 Subject: [PATCH 40/68] stuff --- qualtran/_infra/bloq.py | 2 + qualtran/bloqs/basic_gates/z_basis_test.py | 28 ++++++++++ qualtran/simulation/classical_sim.py | 46 +++++------------ qualtran/simulation/classical_sim_test.py | 60 +++++++++++++++++++--- 4 files changed, 96 insertions(+), 40 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 023cc934a2..a454e1e2fc 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -226,6 +226,8 @@ def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: call TODO and not this method directly. If this method is implemented, `on_classical_vals` must also be implemented. + If `on_classical_vals` is implemented but this method is not implemented, it is assumed + that the bloq does not alter the phase. """ return None diff --git a/qualtran/bloqs/basic_gates/z_basis_test.py b/qualtran/bloqs/basic_gates/z_basis_test.py index 267518a96b..8536894cd3 100644 --- a/qualtran/bloqs/basic_gates/z_basis_test.py +++ b/qualtran/bloqs/basic_gates/z_basis_test.py @@ -24,6 +24,7 @@ IntState, OneEffect, OneState, + XGate, ZeroEffect, ZeroState, ZGate, @@ -235,3 +236,30 @@ def test_cz_manual(): with pytest.raises(ValueError, match='.*phase.*'): cz.call_classically(q1=1, q2=1) + + +def test_cz_phased_classical(): + cz = CZ() + from qualtran.simulation.classical_sim import do_phased_classical_simulation + + final_vals, phase = do_phased_classical_simulation(cz, {'q1': 0, 'q2': 1}) + assert final_vals['q1'] == 0 + assert final_vals['q2'] == 1 + assert phase == 1 + + final_vals, phase = do_phased_classical_simulation(cz, {'q1': 1, 'q2': 1}) + assert final_vals['q1'] == 1 + assert final_vals['q2'] == 1 + assert phase == -1 + + bb = BloqBuilder() + q1 = bb.add(ZeroState()) + q2 = bb.add(ZeroState()) + q1 = bb.add(XGate(), q=q1) + q2 = bb.add(XGate(), q=q2) + q1, q2 = bb.add(CZ(), q1=q1, q2=q2) + cbloq = bb.finalize(q1=q1, q2=q2) + final_vals, phase = do_phased_classical_simulation(cbloq, {}) + assert final_vals['q1'] == 1 + assert final_vals['q2'] == 1 + assert phase == -1 diff --git a/qualtran/simulation/classical_sim.py b/qualtran/simulation/classical_sim.py index a723f244bd..927c93e00a 100644 --- a/qualtran/simulation/classical_sim.py +++ b/qualtran/simulation/classical_sim.py @@ -106,7 +106,7 @@ def _get_in_vals( return arg -class _ClassicalSimState: +class ClassicalSimState: """A mutable class for classically simulating composite bloqs. Consider using the public method `Bloq.call_classically(...)` for a simple interface @@ -147,12 +147,12 @@ def __init__( self.soq_assign: Dict[Soquet, ClassicalValT] = {} self._update_assign_from_vals(self._signature.lefts(), LeftDangle, dict(vals)) - self.last_binst = None + self.last_binst: Optional['BloqInstance'] = None @classmethod def from_cbloq( cls, cbloq: 'CompositeBloq', vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]] - ) -> '_ClassicalSimState': + ) -> 'ClassicalSimState': """Initiate a classical simulation from a CompositeBloq. Args: @@ -225,16 +225,16 @@ def _binst_basis_state_phase(self, binst, in_vals) -> None: """Call `basis_state_phase` on a given bloq instance. This base simulation class will raise an error if the bloq reports any phasing. - This method is overwritten in `_PhasedClassicalSimState` to support phasing. + This method is overwritten in `PhasedClassicalSimState` to support phasing. """ bloq = binst.bloq bloq_phase = bloq.basis_state_phase(**in_vals) if bloq_phase is not None: raise ValueError( - f"{bloq} imparts a phase, and can't be simulated purely classically. Consider TODO" + f"{bloq} imparts a phase, and can't be simulated purely classically. Consider using `do_phased_classical_simulation`." ) - def step(self) -> '_ClassicalSimState': + def step(self) -> 'ClassicalSimState': """Advance the simulation by one bloq instance. After calling this method, `self.last_binst` will contain the bloq instance that @@ -298,26 +298,15 @@ def simulate(self) -> Dict[str, 'ClassicalValT']: return self.finalize() -class _PhasedClassicalSimState(_ClassicalSimState): +class PhasedClassicalSimState(ClassicalSimState): """A mutable class for classically simulating composite bloqs with phase tracking. - Consider using TODO + The convenience function `do_phased_classical_simulation` will simulate a bloq. Use this + class directly for more fine-grained control. This simulation scheme supports a class of circuits containing only: - classical operations corresponding to permutation matrices in the computational basis - phase-like operations corresponding to diagonal matrices in the computational basis. - - X-basis measurement. - - In general, you cannot delete quantum data and must "uncompute" bits by adding inverse, - ("adjoint") operations to return variables to known states. Measurement based - uncomputation (MBUC) is a trick that uses X-basis measurement to _nearly_ remove quantum - data. Performing an X-basis measurement will not destroy computational-basis coherence - but it will generate phases on your remaining qubits. These phases are tracked by - this simulator; but note that they are classically stochastic. - - A bloq that generates a negative phase based on its measurement result can return a - `MeasurementPhase` object in its `Bloq.basis_state_phase` method. This simulator will - correctly apply a -1 phase only if the measurement result was 1. Args: signature: The signature of the composite bloq. @@ -332,10 +321,6 @@ class _PhasedClassicalSimState(_ClassicalSimState): soq_assign: An assignment of soquets to classical values. last_binst: A record of the last bloq instance we processed during simulation. phase: The current phase of the simulation state. - - References: - [Verifying Measurement Based Uncomputation](https://algassert.com/post/1903). - Gidney. 2019. """ def __init__( @@ -353,7 +338,7 @@ def __init__( @classmethod def from_cbloq( cls, cbloq: 'CompositeBloq', vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]] - ) -> '_PhasedClassicalSimState': + ) -> 'PhasedClassicalSimState': """Initiate a classical simulation from a CompositeBloq. Args: @@ -363,17 +348,14 @@ def from_cbloq( Returns: A new classical sim state. - """ - return cls(signature=cbloq.signature, binst_graph=cbloq._binst_graph, vals=vals) def _binst_basis_state_phase(self, binst, in_vals): """Call `basis_state_phase` on a given bloq instance. If this method returns a value, the current phase will be updated. Otherwise, we - leave the phase as-is. If the method returns `MeasurementPhase`, we employ the - special case described in TODO. + leave the phase as-is. """ bloq = binst.bloq bloq_phase = bloq.basis_state_phase(**in_vals) @@ -406,7 +388,7 @@ def call_cbloq_classically( corresponding to thru registers will be mapped to the *output* classical value. """ - sim = _ClassicalSimState(signature, binst_graph, vals) + sim = ClassicalSimState(signature, binst_graph, vals) final_vals = sim.simulate() return final_vals, sim.soq_assign @@ -419,7 +401,7 @@ def _assert_valid_phase(p: complex, atol: float = 1e-8): def do_phased_classical_simulation(bloq: 'Bloq', vals: Mapping[str, 'ClassicalValT']): """Do a phased classical simulation of the bloq. - This provides a simple interface to `_PhasedClassicalSimState`. Particularly advanced users + This provides a simple interface to `PhasedClassicalSimState`. Advanced users may wish to use that class directly. Args: @@ -432,7 +414,7 @@ def do_phased_classical_simulation(bloq: 'Bloq', vals: Mapping[str, 'ClassicalVa phase: The final phase. """ cbloq = bloq.as_composite_bloq() - sim = _PhasedClassicalSimState.from_cbloq(cbloq, vals=vals) + sim = PhasedClassicalSimState.from_cbloq(cbloq, vals=vals) final_vals = sim.simulate() phase = sim.phase return final_vals, phase diff --git a/qualtran/simulation/classical_sim_test.py b/qualtran/simulation/classical_sim_test.py index e7799e137a..582f5057a1 100644 --- a/qualtran/simulation/classical_sim_test.py +++ b/qualtran/simulation/classical_sim_test.py @@ -13,7 +13,7 @@ # limitations under the License. import itertools -from typing import Dict, Union +from typing import Dict import networkx as nx import numpy as np @@ -25,6 +25,7 @@ Bloq, BloqBuilder, BQUInt, + LeftDangle, QAny, QBit, QDType, @@ -36,23 +37,21 @@ Register, Side, Signature, - Soquet, ) from qualtran.bloqs.basic_gates import CNOT from qualtran.simulation.classical_sim import ( - _ClassicalSimState, add_ints, call_cbloq_classically, - ClassicalValT, + ClassicalSimState, + do_phased_classical_simulation, ) from qualtran.testing import execute_notebook def test_dtype_validation(): # set up mocks for `_update_assign_from_vals` - soq_assign: Dict[Soquet, ClassicalValT] = {} # gets assigned to; we discard in this test. binst = 'MyBinst' # binst is only used for error messages, so we can mock with a string - sim = _ClassicalSimState(Signature([]), nx.DiGraph(), {}) + sim = ClassicalSimState(Signature([]), nx.DiGraph(), {}) # set up different register dtypes regs = [ @@ -100,9 +99,9 @@ def on_classical_vals(self, *, x: NDArray[np.uint8]) -> Dict[str, NDArray[np.uin return {'x': x, 'z': z} -class ApplyPhasedClassicalTest(Bloq): +class ApplyPhasedClassicalTest(ApplyClassicalTest): def basis_state_phase(self, x: NDArray[np.uint8]) -> complex: - return np.exp(x * 1.0j / np.pi) + return np.prod(np.exp(x * 1.0j / np.pi)) def test_apply_classical(): @@ -124,6 +123,29 @@ def test_apply_classical(): np.testing.assert_array_equal(z2, [0, 1, 0, 1, 0]) +def test_apply_phased_classical(): + bloq = ApplyPhasedClassicalTest() + final_vals, phase = do_phased_classical_simulation(bloq, dict(x=np.ones(5, dtype=np.uint8))) + np.testing.assert_array_equal(final_vals['x'], np.ones(5)) + np.testing.assert_array_equal(final_vals['z'], [0, 1, 0, 1, 0]) + assert np.abs(phase - np.exp(5.0j / np.pi)) < 1e-8 + + +def test_phased_classical_on_normal_classical(): + final_vals, phase = do_phased_classical_simulation( + ApplyClassicalTest(), dict(x=np.ones(5, dtype=np.uint8)) + ) + np.testing.assert_array_equal(final_vals['x'], np.ones(5)) + np.testing.assert_array_equal(final_vals['z'], [0, 1, 0, 1, 0]) + assert phase == 1.0 + + +def test_normal_classical_on_phased(): + bloq = ApplyPhasedClassicalTest() + with pytest.raises(ValueError, match=r'.*`do_phased_classical_simulation`.*'): + x, z = bloq.call_classically(x=np.zeros(5, dtype=np.uint8)) + + def test_cnot_assign_dict(): cbloq = CNOT().as_composite_bloq() binst_graph = cbloq._binst_graph # pylint: disable=protected-access @@ -151,6 +173,28 @@ def test_apply_classical_cbloq(): np.testing.assert_array_equal(z, xarr) +def test_step(): + bb = BloqBuilder() + x = bb.add_register(Register('x', QBit(), shape=(5,))) + assert x is not None + x, y = bb.add(ApplyClassicalTest(), x=x) + y, z = bb.add(ApplyClassicalTest(), x=y) + cbloq = bb.finalize(x=x, y=y, z=z) + + xarr = np.zeros(5, dtype=np.intc) + sim = ClassicalSimState.from_cbloq(cbloq, dict(x=xarr)) + sim.step() + assert sim.last_binst is not None and sim.last_binst is LeftDangle + sim.step() + assert sim.last_binst is not None and sim.last_binst.bloq_is(ApplyClassicalTest) + assert sim.last_binst is not None and sim.last_binst.i == 0 + + final_vals = sim.step().finalize() + np.testing.assert_array_equal(final_vals['x'], xarr) + np.testing.assert_array_equal(final_vals['y'], [1, 0, 1, 0, 1]) + np.testing.assert_array_equal(final_vals['z'], xarr) + + @pytest.mark.parametrize('n_bits', range(1, 5)) def test_add_ints_unsigned(n_bits): for x, y in itertools.product(range(1 << n_bits), repeat=2): From 9aad5f01a10d25d378ed3b160423095e997e3464 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:26:06 -0700 Subject: [PATCH 41/68] superquimb --- qualtran/simulation/supertensor.ipynb | 413 +++++++++++++++++++++++++ qualtran/simulation/tensor.ipynb | 133 +++++--- qualtran/simulation/tensor/__init__.py | 4 +- qualtran/simulation/tensor/_dense.py | 131 ++++---- qualtran/simulation/tensor/_quimb.py | 278 +++++++++++++++-- 5 files changed, 841 insertions(+), 118 deletions(-) create mode 100644 qualtran/simulation/supertensor.ipynb diff --git a/qualtran/simulation/supertensor.ipynb b/qualtran/simulation/supertensor.ipynb new file mode 100644 index 0000000000..7ab7e347c8 --- /dev/null +++ b/qualtran/simulation/supertensor.ipynb @@ -0,0 +1,413 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c90db82e-fe68-40ab-8093-0ee1b76f1cba", + "metadata": {}, + "source": [ + "# Super-Tensor Simulation\n", + "\n", + "Simulation of a quantum program involves encoding the state of the system and encoding operators that update the state according to the desired operation.\n", + "\n", + "In *closed* quantum systems, we can encode our state in a *state vector* and our operations as *unitary matrices*. For a given bloq, we can query these objects with the `Bloq.tensor_contract()` method. Bloqs with only right registers correspond to states; and bloqs with thru registers correspond to unitary matrices. The tensor simulation protocol handles composing the states and unitaries to get e.g. the final state or unitary.\n", + "\n", + "In *open* quantum systems, the state of our system is (potentially) a classically-probabalistic mixture over pure states (which are the states found in closed quantum systems). The operations map this mixture of states to a new mixture of states. The superoperator tensor protocol lets you simulate Qualtran programs that include non-unitary operations like measurement or discarding qubits. Any pure state can also be simulated using this protocol, but it is more expensive than the normal tensor simulation protocol; so we encourage you to use the ordinary tensor contraction protocol wherever feasible.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c038ddeb-185a-45f9-8304-de9e96f1b4dc", + "metadata": {}, + "source": [ + "## Density matrix\n", + "\n", + "In this section, we contract all bloqs to thier `numpy.ndarray` numerical representation. There are more indices and fewer conventions about the arrangement of these indices, so practitioners must either pay close attention to the following documentation, or deal exclusively and directly with the Quimb `qtn.TensorNetwork` objects and their named indices (see the next section).\n", + "\n", + "When dealing with open system simulation, the state of the system is no longer represented by a 1-dimensional vector of probability amplitudes, but rather **a 2-dimensional matrix** of classical probabilities along the diagonal and quantum *coherences* off-diagonal.\n", + "\n", + "\n", + "### The $|+\\rangle$ state\n", + "First, let's inspect the statevector and density matrix representation of the $|+\\rangle$ state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12c866ec-abf8-4a4f-941e-33922edf8348", + "metadata": {}, + "outputs": [], + "source": [ + "# Ordinary statevector of |+>\n", + "from qualtran.bloqs.basic_gates import PlusState\n", + "PlusState().tensor_contract()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "794c0d72-ee3a-4679-83bd-6a6b3a504dd1", + "metadata": {}, + "outputs": [], + "source": [ + "# Use the superoperator simulation machinery. States are\n", + "# now density _matrices_. \n", + "PlusState().tensor_contract(superoperator=True)" + ] + }, + { + "cell_type": "markdown", + "id": "d25c6f88-baf2-4a9f-bdfb-0f33b25ebbda", + "metadata": {}, + "source": [ + "### Incoherent states\n", + "\n", + "We know that the $|+\\rangle$ state has a 50% chance of being measured in the 0 vs. 1 state. What is the difference between this and a classical coin flip? We'll compare the density matrices between the coherent `PlusState` vs. measuring the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cab8731-7cd0-4486-93d2-69ab79853786", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import BloqBuilder\n", + "from qualtran.drawing import show_bloq\n", + "from qualtran.bloqs.basic_gates import ZeroState, Hadamard, MeasZ\n", + "\n", + "# Initialize a qubit, do a Hadamard\n", + "bb = BloqBuilder()\n", + "q = bb.add(ZeroState())\n", + "q = bb.add(Hadamard(), q=q)\n", + "c = bb.add(MeasZ(), q=q)\n", + "coin_flip = bb.finalize(c=c)\n", + "\n", + "# The circuit\n", + "show_bloq(coin_flip, 'musical_score')\n", + "\n", + "# Its density matrix\n", + "rho_coin_flip = coin_flip.tensor_contract(superoperator=True)\n", + "display(rho_coin_flip)" + ] + }, + { + "cell_type": "markdown", + "id": "3fb7e165-2be8-4c37-b7e5-08159e9c96aa", + "metadata": {}, + "source": [ + "## Superoperator evolution with matrix-vector multiplication\n", + "\n", + "Under the hood, the Quimb tensor network package can find efficient contraction orderings for performing superoperator simulation. However, for clarity we can show how the superoperator tensors can be manipulated to evolve a vector representation of the density operator with a matrix representation of the superoperator." + ] + }, + { + "cell_type": "markdown", + "id": "2dc0cc33-3150-405c-ac13-d317ed72c570", + "metadata": {}, + "source": [ + "### Superoperator tensors\n", + "\n", + "Operations (like `Hadamard` below) are encoded in 4-index tensors. You saw above that states are encoded in 2-index tensors (the density matrix)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa757980-f244-4c30-828b-82cae9253248", + "metadata": {}, + "outputs": [], + "source": [ + "super_h = Hadamard().tensor_contract(superoperator=True)\n", + "super_h.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6fc177a0-36b2-4403-95e3-fc644d505b0b", + "metadata": {}, + "outputs": [], + "source": [ + "rho_coin_flip.shape" + ] + }, + { + "cell_type": "markdown", + "id": "89bec971-0894-4e48-a016-3ca6536e33cb", + "metadata": {}, + "source": [ + "There is no standard naming scheme for these four indices, but we describe them here:\n", + "\n", + " - Unitary operations in standard, statevector evolution have two indices (i.e. they are matrices). We name the two indices **left** and **right** indices, corresponding to the input and output (resp.) basis states.\n", + " - The density matrix $\\rho$ for a pure state $|\\psi\\rangle$ is given by $|\\psi\\rangle \\langle \\psi|$.\n", + " - Evolution of a pure state by a unitary $U$ corresponds to applying $U|\\psi\\rangle$ to the first part of $\\rho$ and $\\langle \\psi | U^\\dagger$ to the latter part.\n", + " - With a bit of poetic license, we call the indices of the $|\\psi\\rangle$ part the **forward** indices and the $\\langle \\psi|$ part the **backward** indices.\n", + "\n", + "The Qualtran ordering of the superoperator tensor is:\n", + "\n", + " (right_forward, right_backward, left_forward, left_backward)\n", + "\n", + "The ordering of the density matrix indices is the familiar, textbook ordering which—following the terminoligy we set up—is either\n", + "\n", + " (right_forward, right_backward)\n", + " *or*\n", + " (left_foward, left_backward)\n", + "\n", + "depending on whether it is an initializing state or de-allocating state, respectively." + ] + }, + { + "cell_type": "markdown", + "id": "51c1dddd-99cb-4205-83ce-326be9c3d5bf", + "metadata": {}, + "source": [ + "### Reshaping\n", + "\n", + "The index ordering allows reshaping of superoperators into matrices and density matrices into vectors so evolution can be computed by the traditional matrix-vector product.\n", + "\n", + "We'll see that applying a reshaped tensor of `Hadamard` to our reshaped `rho_coin_flip` gives us a random result, but applying it to a coherent state results in a deterministic output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c33878-f71a-4ea3-b116-a1682c3a45e8", + "metadata": {}, + "outputs": [], + "source": [ + "# Applying H to our incoherent, coin-flip state\n", + "(super_h.reshape(4,4) @ rho_coin_flip.reshape(4)).reshape(2,2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ca8abb6-7156-42e4-905b-c17f2d71bf64", + "metadata": {}, + "outputs": [], + "source": [ + "# Applying H to our coherent state\n", + "rho_coherent = PlusState().tensor_contract(superoperator=True)\n", + "(super_h.reshape(4,4) @ rho_coherent.reshape(4)).reshape(2,2)" + ] + }, + { + "cell_type": "markdown", + "id": "fd627fc9-d734-4ed0-901c-d09e22480e37", + "metadata": {}, + "source": [ + "## Quimb Tensor Network\n", + "\n", + "The function `cbloq_to_superquimb` returns a `qtn.TensorNetwork` representing the composite bloq. The structure is apparent: there are effectively two pure-state evolutions occuring ('forward' and 'backward'). Non-unitary operations introduce an index coupling the forward and backward evolutions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84d7be2f-44ac-4c69-aaa0-cc6f15364530", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.simulation.tensor import cbloq_to_superquimb\n", + "tn = cbloq_to_superquimb(coin_flip, friendly_indices=True)\n", + "tn.draw(color=['|0>', 'H', 'MeasZ', 'dag'])" + ] + }, + { + "cell_type": "markdown", + "id": "6375ba0c-1b8c-4de9-9153-bc848f626730", + "metadata": {}, + "source": [ + "## System+Environment modeling\n", + "\n", + "All CPTP maps can be implemented by unitary evolution in a larger \"system + environment\" space. In this section, we show how to build a measurement operation with only standard, unitary bloqs and the ability to discard information.\n", + "\n", + "Any Hermitian operator can be \"measured\" into a fresh ancilla using a simple prescription, see e.g. Nielsen and Chuang Exercise 4.44. We build that construction below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cde1066-7e36-49b6-a665-58905250521c", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import BloqBuilder, Register, Side, CtrlSpec, QBit, CBit\n", + "from qualtran.bloqs.basic_gates import ZeroState, Hadamard, MeasZ, ZGate\n", + "from qualtran.bloqs.bookkeeping import Cast\n", + "\n", + "bb = BloqBuilder()\n", + "# Take a single qubit as input\n", + "q = bb.add_register(Register(\"q\", QBit()))\n", + "# Set up our output register: we'll return one classical bit\n", + "bb.add_register(Register(\"c\", CBit(), side=Side.RIGHT))\n", + "\n", + "# This construction works for any Hermitian operator. We'll\n", + "# use Z as a familiar first example.\n", + "op = ZGate()\n", + "\n", + "# Allocate space to record the result of our measurement operation\n", + "meas_space = bb.add(ZeroState())\n", + "meas_space = bb.add(Hadamard(), q=meas_space)\n", + "\n", + "# Do Controlled(op)\n", + "_, add_ctrled = op.get_ctrl_system(CtrlSpec())\n", + "(meas_space,), (q,) = add_ctrled(bb, ctrl_soqs=[meas_space], in_soqs={'q': q})\n", + "\n", + "# Final Hadamard, and cast our measurement register\n", + "# into a classical bit.\n", + "meas_space = bb.add(Hadamard(), q=meas_space)\n", + "meas_result = bb.add(Cast(QBit(), CBit(), allow_quantum_to_classical=True), reg=meas_space)\n", + "meas_cbloq = bb.finalize(c = meas_result, q=q)\n", + "show_bloq(meas_cbloq)" + ] + }, + { + "cell_type": "markdown", + "id": "455ab7ed-c93d-4fc3-8f81-a694d4f90a24", + "metadata": {}, + "source": [ + "Note that we've entangled our system \"q\" with a fresh register. We've used a `Cast` operation to denote that the new bit is a classical bit. Practically this means we can no longer perform quantum operations like `Hadamard` to it, and any classical processing can happen on ordinary CPUs. But at a quantum-information level, there is nothing about the tensor structure to show that \"c\" is a 'classical' index. Below, we draw the tensor network encoding of the State/Unitary composite bloq using the ordinary tensor protocol" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57f300ec-28a5-4b6f-aabd-4cc5c8cf31ed", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.simulation.tensor import cbloq_to_quimb\n", + "tn = cbloq_to_quimb(meas_cbloq, friendly_indices=True)\n", + "tn.draw(color=['CZ', 'H'])\n", + "display(tn.contract())" + ] + }, + { + "cell_type": "markdown", + "id": "6ec443b0-d7e9-4d69-89eb-a7c068ed2737", + "metadata": {}, + "source": [ + "### Making a measurement operation\n", + "\n", + "In an open system—like the world we live in—we don't have coherent access to each (qu)bit worth of information. Our measurement apparatus might have $10^{23}$ particles, each recording the result of a measurement. We can simulate the standard measurement channel where information is lost to the environment by using the previous circuit and discarding the coherent qubit wire. Now, the signature of our composite bloq takes in one `QBit()` and returns one `CBit()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "690955e1-eab3-4f90-863b-53090d13a75c", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.bloqs.basic_gates import DiscardQ\n", + "\n", + "bb = BloqBuilder()\n", + "q = bb.add_register(Register(\"q\", QBit(), side=Side.LEFT))\n", + "q, c = bb.add_from(meas_cbloq, q=q)\n", + "bb.add(DiscardQ(), q=q)\n", + "meas2_cbloq = bb.finalize(c=c)\n", + "show_bloq(meas2_cbloq)" + ] + }, + { + "cell_type": "markdown", + "id": "2842fec6-d709-488a-9d31-d8817f47dbb0", + "metadata": {}, + "source": [ + "The ordinary tensor simulation protocol is insufficient to handle discarding a qubit. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b48228f0-3ef1-4e07-b89a-65c46fd0a865", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " tn = cbloq_to_quimb(meas2_cbloq, friendly_indices=True)\n", + "except ValueError as e:\n", + " print(\"ValueError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "cbec03e4-1a88-4a2b-ae4c-32f1efb6ddc7", + "metadata": {}, + "source": [ + "To remove a qubit, we need to sum over its possible states as we would when computing a marginal probability distribution. But our probability *amplitudes* only sum to 1 when we consider their absolute value squared\n", + "$$\n", + "\\sum a^* a = 1,\n", + "$$\n", + "so our integration proceedure requires indexing into both our state $|\\psi\\rangle$ and its adjoint $\\langle \\psi|$ to remove the offending bit.\n", + "\n", + "If you're using a densitry matrix, this correspond to performing a parital trace. In Qualtran, the superoperator tensor simulation protocol sets up two simultaneous tensor networks for simulating unitary action on the circuit *and* its adjoint. That is, we simulate both $|\\psi\\rangle$ and $\\langle \\psi|$. Discarding a qubit is performed by contracting the qubit's index in the forward network with its corresponding index in the backwards, adjoint network." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca011792-af4d-463e-bd26-df7c0cdba14a", + "metadata": {}, + "outputs": [], + "source": [ + "tn = cbloq_to_superquimb(meas2_cbloq, friendly_indices=True)\n", + "tn.draw(color=['CZ', 'H', 'dag'], initial_layout='spectral')\n", + "display(tn.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "5dbadc5c-7422-4ea6-8a5d-379a58bc81b8", + "metadata": {}, + "source": [ + "The discard operation couples the two pure evolutions. The superoperator is now a rank-4 tensor.\n", + "\n", + "By the Stinespring dilation theorem, we can actually represent any superoperator (aka quantum channel, aka CPTP map) with only pure state evolution and the ability to discard qubits. This gives rise to the \"System-Environment\" representation of superoperators, which is the native representation in Qualtran. It is quite natural for the open-system operations we're most concerned with (like measurement); but practitioners may have to do some careful translation to encode superoperators traditionally expressed in another representation like the operator-sum (Kraus operator) representation." + ] + }, + { + "cell_type": "markdown", + "id": "91784324-c495-4878-afed-5597ea6782d6", + "metadata": {}, + "source": [ + "## Implementation\n", + "\n", + "The super-tensor protocol is a superset of the ordinary tensor simulation protocol. Bloqs override the same method to define their tensors for open-system simulation: the bloq author overrides `Bloq.my_tensors` and is responsible for returning a list of tensors whose indices follow the same prescribed format as documented in the [tensor protocol](.). \n", + "\n", + "All additional functionality is unlocked by the ability to return a `qualtran.simulation.tensor.DiscardInd` object amongst the ordinary `qtn.Tensor` objects. Per above, any superoperator can be expressed in this system-environment representation. This simple sentinel object flags the named index as subject to \"tracing out\" during construction of the complete network.\n", + "\n", + "When calling `cbloq_to_quimb` the indices are faithfully kept as `(cxn: Connection, j: int)` tuples. During the conversion to the superoperator tensor network, each tensor returned by `Bloq.my_tensors` is added twice:\n", + " - The 'forward' tensor is added. Its indices `(cxn, j)` are transformed to `(cxn, j, True)`\n", + " - The 'backward' tensor is added. Its indices `(cxn, j)` are transformed to `(cxn, j, False)` and we take the element-wise complex conjugate of its data ndarray.\n", + " - A `DiscardInd` will remove the booleans from the named index in tensors which have already been added. This causes the forward and backward indices to be contracted together.\n", + "\n", + "Note that index permutation operations like taking the transpose of the backwards tensor is handled by the structure of the tensor network rather than mutating the data ndarray. \n", + "\n", + "The `my_tensors` override must order its return values such that a `DiscardInd` is encountered *after* the tensor that defines the index to discard. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/qualtran/simulation/tensor.ipynb b/qualtran/simulation/tensor.ipynb index 014193182d..3628fb021b 100644 --- a/qualtran/simulation/tensor.ipynb +++ b/qualtran/simulation/tensor.ipynb @@ -5,7 +5,7 @@ "id": "8cad0714", "metadata": {}, "source": [ - "# Tensor\n", + "# Tensor Simulation\n", "\n", "The tensor protocol lets you query the tensor (vector, matrix, etc.) representation of a bloq. For example, we can easily inspect the familiar unitary matrix representing the controlled-not operation:" ] @@ -28,7 +28,7 @@ "id": "5a2f84d3", "metadata": {}, "source": [ - "Bloqs can represent states, effects, and non-unitary operations. Below, we see the vector representation of the plus state and zero effect." + "Bloqs can represent states, effects, unitary operations, and compositions of these operations. Below, we see the vector representation of the plus state and zero effect." ] }, { @@ -64,6 +64,14 @@ "And().tensor_contract().shape" ] }, + { + "cell_type": "markdown", + "id": "6f61b73c-50df-4ed3-aeab-f24e9ec2b3e5", + "metadata": {}, + "source": [ + "For a bloq with exclusively thru-registers, the returned tensor will be a matrix with shape `(n, n)` where `n` is the number of bits in the signature. For a bloq with exlusively right- or left-registers, the returned tensor will be a vector with shape `(n,)`. In general, the tensor will be an ndarray of shape `(n_right_bits, n_left_bits)`; but empty dimensions are *dropped*." + ] + }, { "cell_type": "markdown", "id": "912fb914", @@ -140,8 +148,8 @@ "source": [ "from qualtran.simulation.tensor import cbloq_to_quimb\n", "\n", - "tn = cbloq_to_quimb(cbloq)\n", - "tn.draw(show_inds=False)" + "tn = cbloq_to_quimb(cbloq, friendly_indices=True)\n", + "tn.draw()" ] }, { @@ -169,36 +177,29 @@ "source": [ "### Quimb index format\n", "\n", - "In `CompositeBloq`, we form the compute graph by storing a list of nodes and edges. Quimb uses a different strategy for representing the tensor network graphs. To form a tensor network in Quimb, we provide a list of `qtn.Tensor` which contain not only the tensor data but also a list of \"indices\" that can form connections to other tensors. Similar to the Einstein summation convention, if two tensors each have an index with the same name: an edge is created in the tensor network graph and this shared index is summed over. These indices are traditionally short strings like `\"k\"`, but can be any hashable Python object. In `CompositeBloq`, the unique object that identifies a connection between bloqs is `qualtran.Connection`, so we use these connection objects as our indices.\n", + "Above, we used the `friendly_indices=True` argument to give string names to the outer indices of the `qtn.TensorNetwork`. This can be useful for interactive manipulation of the tensor network by a human. Qualtran uses a highly-structured (but less human-readible) format for internal tensor indices, which we describe here.\n", + "\n", + "In `CompositeBloq`, we form the compute graph by storing a list of nodes and edges. Quimb uses a different strategy for representing the tensor network graphs. To form a tensor network in Quimb, we provide a list of `qtn.Tensor` which contain not only the tensor data but also a list of \"indices\" that can form connections to other tensors. Similar to the Einstein summation convention, if two tensors each have an index with the same name: an edge is created in the tensor network graph and this shared index is summed over. In the Quimb documentation (for example), these indices are traditionally short strings like `\"k\"`, but can be any hashable Python object. In `CompositeBloq`, the unique object that identifies a connection between bloqs is `qualtran.Connection`, so we use these connection objects as the first part of our indices.\n", "\n", - "Qualtran and Quimb both support \"wires\" with arbitrary bitsize. Qualtran uses bookkeeping bloqs like `Join` and `Split` to fuse and un-fuse indices. In theory, these operations should be free in the tensor contraction, as they are essentially an identity tensor. In our understanding, Quimb does not have special support for supporting these re-shaping operations within the tensor network graph. In versions of Qualtran prior to v0.5, split and join operations were tensored up to `n`-sized identity tensors. This would form a bottleneck in any contraction ordering. Therefore, we keep all the indices un-fused in the tensor network representation and use tuples of `(cxn, i)` for our tensor indices, where the second integer `i` indexes the individual bits in a register with `reg.dtype.num_qubits` > 1.\n", + "Qualtran and Quimb both support \"wires\" with arbitrary bitsize. Qualtran uses bookkeeping bloqs like `Join` and `Split` to fuse and un-fuse indices. In theory, these operations should be free in the tensor contraction, as they are essentially an identity tensor. In our understanding, Quimb does not have special support for handling these re-shaping operations within the tensor network graph. In versions of Qualtran prior to v0.5, split and join operations were tensored up to `n`-sized identity tensors. This would form a bottleneck in any contraction ordering. Therefore, we keep all the indices un-fused in the tensor network representation and use tuples of `(cxn, j)` for our tensor indices, where the second integer `j` indexes the individual bits in a register with `reg.dtype.num_bits` > 1.\n", "\n", "**In summary:**\n", - " - Each tensor index is a tuple `(cxn, i)`\n", + " - Each tensor index is a tuple `(cxn, j)`\n", " - The `cxn: qualtran.Connection` entry identifies the connection between soquets in a Qualtran compute graph.\n", - " - The second integer `i` is the bit index within high-bitsize registers, which is necessary due to technical restrictions." + " - The second integer `j` is the bit index within high-bitsize registers, which is necessary due to technical restrictions." ] }, { "cell_type": "code", "execution_count": null, - "id": "d9c0ed47-275e-417b-bbf5-b8dff2f84099", + "id": "e0831ffe-2845-4878-a80e-bbe4a533daf4", "metadata": {}, "outputs": [], "source": [ - "# Use `get_right_and_left_inds` to get the quimb indices ordered according to\n", - "# the bloq's signature.\n", - "\n", - "from qualtran.simulation.tensor import get_right_and_left_inds\n", - "left, right = get_right_and_left_inds(tn, bloq.signature)\n", - "\n", - "print(\"Left outer inds:\")\n", - "for cxn, i in left:\n", - " print(' ', cxn.left)\n", - "\n", - "print(\"Right outer inds\")\n", - "for cxn, i in right:\n", - " print(' ', cxn.right)" + "example_inner_index = tn.inner_inds()[0]\n", + "cxn, j = example_inner_index\n", + "print(\"cxn:\", cxn)\n", + "print(\"j: \", j)" ] }, { @@ -212,7 +213,20 @@ "\n", "In Qualtran, we usually avoid flattening bloqs and strongly to prefer to work with one level of decomposition at a time. This is to avoid performance issues with large, abstract algorithms. But typically if the full circuit is large enough to cause performance issues with flattening it is also too large to simulate numerically; so an exception to the general advice is made here.\n", "\n", - "All bloqs in the flattened circuit must provide their explicit tensors. If your bloq's tensors ought to be derived from its decomposition: this is achieved by the previously mentioned flattening operation. If a bloq provides tensors through overriding `Bloq.my_tensors` _and also_ defines a decomposition, the explicit tensors will not be used (by default). This is because any bloq with a decomposition will be flattened. If you would like to control the flattening operation, use the free functions to control the tensor network construction and contraction." + "All bloqs in the flattened circuit must provide their explicit tensors. If your bloq's tensors ought to be derived from its decomposition: this is achieved by the previously mentioned flattening operation. If a bloq provides tensors through overriding `Bloq.my_tensors` _and also_ defines a decomposition, the explicit tensors will not be used (by default). If you'd like to always use annotated tensors, set `bloq_to_dense(..., full_flatten=False)`. If you would like full control over flattening, use the free functions to control the tensor network construction and contraction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "def8e03b-6608-4cd3-8035-6755cf93d9cc", + "metadata": {}, + "outputs": [], + "source": [ + "# Set `full_flatten=False` to use annotated tensors even\n", + "# if there's a decomposition. (No change in this example since\n", + "# most bloqs don't define both a decomposition and tensors).\n", + "_ = bloq_to_dense(bloq, full_flatten=False)" ] }, { @@ -222,8 +236,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Stop flattening if e.g. a bloq supports explicit tensors _and_ a decomposition.\n", - "# Use the flatten predicate to control.\n", + "# Manually flatten and contract for complete control\n", "custom_flat = bloq.as_composite_bloq().flatten(lambda binst: binst.i != 2)\n", "tn = cbloq_to_quimb(custom_flat)\n", "len(tn.tensors)" @@ -285,7 +298,6 @@ " # We'll reshape our matrix into the more natural n-dimensional\n", " # tensor form.\n", " tensor = matrix.reshape((2,2,2,2))\n", - " \n", "\n", " # This is a simple case: we only need one tensor and\n", " # each register is one bit.\n", @@ -354,7 +366,7 @@ " q1 = bb.add(ZeroState())\n", "\n", " q0, q1 = bb.add(CNOT(), ctrl=q0, target=q1)\n", - " return {'q0': q0, 'q1': q1}\n" + " return {'q0': q0, 'q1': q1}" ] }, { @@ -380,7 +392,7 @@ "id": "ebbe9a2a", "metadata": {}, "source": [ - "If you don't flatten first, an error will be raised" + "If you try to directly use `cbloq_to_quimb` and bypass the flattening operation, an error will be raised." ] }, { @@ -427,13 +439,7 @@ ")\n", "\n", "cbloq = CNOT().as_composite_bloq()\n", - "tn = cbloq_to_quimb(cbloq)\n", - "\n", - "# Rename the indices to something less verbose\n", - "lefts, rights = get_right_and_left_inds(tn, cbloq.signature)\n", - "rename = {left: f'{left[0].left.reg.name}_in' for left in lefts}\n", - "rename |= {right: f'{right[0].right.reg.name}_out' for right in rights}\n", - "tn = tn.reindex(rename)\n", + "tn = cbloq_to_quimb(cbloq, friendly_indices=True)\n", "\n", "tn.draw(color=['COPY', 'XOR'], show_tags=True, initial_layout='spectral')\n", "for tensor in tn:\n", @@ -441,6 +447,63 @@ " print(tensor.data)\n", " print()" ] + }, + { + "cell_type": "markdown", + "id": "d4ef3cdf-6edc-4f1a-900c-f0d73cd2cb26", + "metadata": {}, + "source": [ + "### Final state vector from a circuit\n", + "\n", + "In Qualtran, all initial states must be explicitly specified with allocation-like bloqs. For example, if we define the circuit below, the `.tensor_contract` simulation method will only ever return a unitary matrix. If you'd like the state vector that results from applying that circuit to qubits initialized in a particular way (e.g. the all-zeros computational basis state), then you must specify that" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28effa80-e699-4478-9779-d7d5fc93eb0e", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import BloqBuilder\n", + "from qualtran.bloqs.basic_gates import Hadamard\n", + "\n", + "bb = BloqBuilder()\n", + "q1 = bb.add_register('q1', 1)\n", + "q2 = bb.add_register('q2', 1)\n", + "\n", + "q1 = bb.add(Hadamard(), q=q1)\n", + "q1, q2 = bb.add(CNOT(), ctrl=q1, target=q2)\n", + "bell_circuit = bb.finalize(q1=q1, q2=q2)\n", + "\n", + "# This circuit always corresponds to a unitary *matrix*\n", + "show_bloq(bell_circuit, 'musical_score')\n", + "print(bell_circuit.tensor_contract().round(2))" + ] + }, + { + "cell_type": "markdown", + "id": "549cb96d-162e-4112-9e62-88a4aac73a5d", + "metadata": {}, + "source": [ + "We can use the `initialize_from_zero` helper function to get the state vector corresponding to an all-zeros initial state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cf3fbba-eccf-4f15-b9ff-9b3bfbb7e65c", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.simulation.tensor import initialize_from_zero\n", + "bell_state_cbloq = initialize_from_zero(bell_circuit)\n", + "\n", + "# The new composite bloq consists of a round of intialization and\n", + "# then the circuit from above. Its tensor contraction is the state vector\n", + "show_bloq(bell_state_cbloq)\n", + "print(bell_state_cbloq.tensor_contract().round(2))" + ] } ], "metadata": { diff --git a/qualtran/simulation/tensor/__init__.py b/qualtran/simulation/tensor/__init__.py index de6cf893e0..e656cbaaea 100644 --- a/qualtran/simulation/tensor/__init__.py +++ b/qualtran/simulation/tensor/__init__.py @@ -14,9 +14,9 @@ """Functionality for the `Bloq.tensor_contract()` protocol.""" -from ._dense import bloq_to_dense, get_right_and_left_inds, quimb_to_dense +from ._dense import bloq_to_dense, quimb_to_dense from ._flattening import bloq_has_custom_tensors, flatten_for_tensor_contraction -from ._quimb import cbloq_to_quimb, initialize_from_zero +from ._quimb import cbloq_to_quimb, cbloq_to_superquimb, DiscardInd, initialize_from_zero from ._tensor_data_manipulation import ( active_space_for_ctrl_spec, eye_tensor_for_signature, diff --git a/qualtran/simulation/tensor/_dense.py b/qualtran/simulation/tensor/_dense.py index ea4a35bbed..e48f09e211 100644 --- a/qualtran/simulation/tensor/_dense.py +++ b/qualtran/simulation/tensor/_dense.py @@ -13,20 +13,23 @@ # limitations under the License. import logging -from typing import Any, Dict, List, Tuple, TYPE_CHECKING +from collections import defaultdict +from typing import Any, Dict, List, Tuple, TYPE_CHECKING, TypeAlias from numpy.typing import NDArray -from qualtran import Bloq, Connection, ConnectionT, LeftDangle, RightDangle, Signature +from qualtran import Bloq, Connection, ConnectionT, Signature from ._flattening import flatten_for_tensor_contraction -from ._quimb import cbloq_to_quimb +from ._quimb import cbloq_to_quimb, cbloq_to_superquimb if TYPE_CHECKING: import quimb.tensor as qtn logger = logging.getLogger(__name__) +_IndT: TypeAlias = Any + def _order_incoming_outgoing_indices( signature: Signature, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] @@ -64,65 +67,59 @@ def _order_incoming_outgoing_indices( return inds -def get_right_and_left_inds(tn: 'qtn.TensorNetwork', signature: Signature) -> List[List[Any]]: - """Return right and left tensor indices. - - In general, this will be returned as a list of length-2 corresponding - to the right and left indices, respectively. If there *are no* right - or left indices, that entry will be omitted from the returned list. +def _group_outer_inds( + tn: 'qtn.TensorNetwork', signature: Signature, superoperator: bool = False +) -> List[List[Any]]: + """Group outer indices of a tensor network. - Right indices come first to match the quantum computing / matrix multiplication - convention where U_tot = U_n ... U_2 U_1. + This is used by 'bloq_to_dense` and `quimb_to_dense` to return a 1-, 2-, or 4-dimensional + array depending on the quantity and type of outer indices in the tensor network. See + the docstring for `Bloq.tensor_contract()` for more informaiton. Args: tn: The tensor network to fetch the outer indices, which won't necessarily be ordered. signature: The signature of the bloq used to order the indices. + superoperator: Whether `tn` is a pure-state or open-system tensor network. """ - left_inds = {} - right_inds = {} - - # Each index is a (cxn: Connection, j: int) tuple. - cxn: Connection + reg_name: str + idx: Tuple[int, ...] j: int + group: str + _KeyT = Tuple[str, Tuple[int, ...], int] + ind_groups_d: Dict[str, Dict[_KeyT, _IndT]] = defaultdict(dict) for ind in tn.outer_inds(): - cxn, j = ind - if cxn.left.binst is LeftDangle: - soq = cxn.left - left_inds[soq.reg, soq.idx, j] = ind - elif cxn.right.binst is RightDangle: - soq = cxn.right - right_inds[soq.reg, soq.idx, j] = ind - else: - raise ValueError( - "Outer indices of a tensor network should be " - "connections to LeftDangle or RightDangle" - ) - - left_ordered_inds = [] - for reg in signature.lefts(): - for idx in reg.all_idxs(): - for j in range(reg.dtype.num_bits): - left_ordered_inds.append(left_inds[reg, idx, j]) - - right_ordered_inds = [] - for reg in signature.rights(): - for idx in reg.all_idxs(): - for j in range(reg.dtype.num_bits): - right_ordered_inds.append(right_inds[reg, idx, j]) - - inds = [] - if right_ordered_inds: - inds.append(right_ordered_inds) - if left_ordered_inds: - inds.append(left_ordered_inds) + reg_name, idx, j, group = ind + ind_groups_d[group][reg_name, idx, j] = ind + + ind_groups_l: Dict[str, List[_IndT]] = defaultdict(list) + + def _sort_group(regs, group_name): + for reg in regs: + for idx in reg.all_idxs(): + for j in range(reg.dtype.num_bits): + ind_groups_l[group_name].append(ind_groups_d[group_name][reg.name, idx, j]) + + if superoperator: + _sort_group(signature.lefts(), 'lf') + _sort_group(signature.lefts(), 'lb') + _sort_group(signature.rights(), 'rf') + _sort_group(signature.rights(), 'rb') + group_names = ['rf', 'rb', 'lf', 'lb'] + else: + _sort_group(signature.lefts(), 'l') + _sort_group(signature.rights(), 'r') + group_names = ['r', 'l'] + inds = [ind_groups_l[groupname] for groupname in group_names if ind_groups_l[groupname]] return inds -def quimb_to_dense(tn: 'qtn.TensorNetwork', signature: Signature) -> NDArray: +def quimb_to_dense( + tn: 'qtn.TensorNetwork', signature: Signature, superoperator: bool = False +) -> NDArray: """Contract a quimb tensor network `tn` to a dense matrix consistent with `signature`.""" - inds = get_right_and_left_inds(tn, signature) + inds = _group_outer_inds(tn, signature, superoperator=superoperator) if tn.contraction_width() > 8: tn.full_simplify(inplace=True) @@ -134,18 +131,34 @@ def quimb_to_dense(tn: 'qtn.TensorNetwork', signature: Signature) -> NDArray: return data -def bloq_to_dense(bloq: Bloq, full_flatten: bool = True) -> NDArray: +def bloq_to_dense(bloq: Bloq, full_flatten: bool = True, superoperator: bool = False) -> NDArray: """Return a contracted, dense ndarray representing the composite bloq. This function is also available as the `Bloq.tensor_contract()` method. This function decomposes and flattens a given bloq into a factorized CompositeBloq, turns that composite bloq into a Quimb tensor network, and contracts it into a dense - matrix. - - The returned array will be 0-, 1- or 2- dimensional with indices arranged according to the - bloq's signature. In the case of a 2-dimensional matrix, we follow the - quantum computing / matrix multiplication convention of (right, left) order of dimensions. + ndarray. + + The returned array will be 0-, 1-, 2-, or 4-dimensional with indices arranged according to the + bloq's signature and the type of simulation requested via the `superoperator` flag. + + If `superoperator` is set to False (the default), a pure-state tensor network will be + constructed. + - If `bloq` has all thru-registers, the dense tensor will be 2-dimensional with shape `(n, n)` + where `n` is the number of bits in the signature. We follow the linear algebra convention + and order the indices as (right, left) so the matrix-vector product can be used to evolve + a state vector. + - If `bloq` has all left- or all right-registers, the tensor will be 1-dimensional with + shape `(n,)`. Note that we do not distinguish between 'row' and 'column' vectors in this + function. + - If `bloq` has no external registers, the contracted form is a 0-dimensional complex number. + + If `superoperator` is set to True, an open-system tensor network will be constructed. + - States result in a 2-dimensional density matrix with indices (right_forward, right_backward) + or (left_forward, left_backward) depending on whether they're input or output states. + - Operations result in a 4-dimensional tensor with indices (right_forward, right_backward, + left_forward, left_backward). For fine-grained control over the tensor contraction, use `cbloq_to_quimb` and `TensorNetwork.to_dense` directly. @@ -154,8 +167,14 @@ def bloq_to_dense(bloq: Bloq, full_flatten: bool = True) -> NDArray: bloq: The bloq full_flatten: Whether to completely flatten the bloq into the smallest possible bloqs. Otherwise, stop flattening if custom tensors are encountered. + superoperator: If toggled to True, do an open-system simulation. This supports + non-unitary operations like measurement, but is more costly and results in + higher-dimension resultant tensors. """ logging.info("bloq_to_dense() on %s", bloq) flat_cbloq = flatten_for_tensor_contraction(bloq, full_flatten=full_flatten) - tn = cbloq_to_quimb(flat_cbloq) - return quimb_to_dense(tn, bloq.signature) + if superoperator: + tn = cbloq_to_superquimb(flat_cbloq) + else: + tn = cbloq_to_quimb(flat_cbloq) + return quimb_to_dense(tn, bloq.signature, superoperator=superoperator) diff --git a/qualtran/simulation/tensor/_quimb.py b/qualtran/simulation/tensor/_quimb.py index d4957e4778..6f067733f0 100644 --- a/qualtran/simulation/tensor/_quimb.py +++ b/qualtran/simulation/tensor/_quimb.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import cast, Dict, Iterable +from typing import Any, cast, Dict, Iterable, Tuple, Union +import attrs import numpy as np import quimb.tensor as qtn @@ -21,6 +22,7 @@ Bloq, CompositeBloq, Connection, + ConnectionT, LeftDangle, QBit, Register, @@ -33,7 +35,7 @@ logger = logging.getLogger(__name__) -def cbloq_to_quimb(cbloq: CompositeBloq) -> qtn.TensorNetwork: +def cbloq_to_quimb(cbloq: CompositeBloq, friendly_indices: bool = False) -> qtn.TensorNetwork: """Convert a composite bloq into a tensor network. This function will call `Bloq.my_tensors` on each subbloq in the composite bloq to add @@ -42,6 +44,18 @@ def cbloq_to_quimb(cbloq: CompositeBloq) -> qtn.TensorNetwork: smallest form first. The small bloqs that result from a flattening 1) likely already have their `my_tensors` method implemented; and 2) can enable a more efficient tensor contraction path. + + Args: + cbloq: The composite bloq. + friendly_indices: If set to True, the outer indices of the tensor network will be renamed + from their Qualtran-computer-readable form to human-friendly strings. This may be + useful if you plan on manually manipulating the resulting tensor network but will + preclude any further processing by Qualtran functions. The indices are named + {soq.reg.name}{soq.idx}_{j}{side}, where j is the individual bit index and side is 'l' + or 'r' for left or right, respectively. + + Returns: + The tensor network """ tn = qtn.TensorNetwork([]) @@ -57,34 +71,248 @@ def cbloq_to_quimb(cbloq: CompositeBloq) -> qtn.TensorNetwork: out_d = _cxns_to_cxn_dict(bloq.signature.rights(), succ_cxns, get_me=lambda cxn: cxn.left) for tensor in bloq.my_tensors(inc_d, out_d): + if isinstance(tensor, DiscardInd): + raise ValueError( + f"During tensor simulation, {bloq} tried to discard information. This requires using `tensor_contract(superoperator=True)` or `cbloq_to_superquimb`." + ) tn.add(tensor) - # Special case: Add variables corresponding to all registers that don't connect to any Bloq. - # This is needed because `CompositeBloq.iter_bloqnections` ignores `LeftDangle/RightDangle` - # bloqs, and therefore we never see connections that exist only b/w LeftDangle and - # RightDangle bloqs. + # Special case: Add indices corresponding to unused wires for cxn in cbloq.connections: if cxn.left.binst is LeftDangle and cxn.right.binst is RightDangle: - # This register has no Bloq acting on it, and thus it would not have a variable in - # the tensor network. Add an identity tensor acting on this register to make sure the - # tensor network has variables corresponding to all input / output registers. - - n = cxn.left.reg.bitsize - for j in range(cxn.left.reg.bitsize): - - placeholder = Soquet(None, Register('simulation_placeholder', QBit())) # type: ignore - Connection(cxn.left, placeholder) - tn.add( - qtn.Tensor( - data=np.eye(2), - inds=[ - (Connection(cxn.left, placeholder), j), - (Connection(placeholder, cxn.right), j), - ], - ) - ) + # Connections that directly tie LeftDangle to RightDangle + for id_tensor in _get_placeholder_tensors(cxn): + tn.add(id_tensor) + + return tn.reindex(_get_outer_indices(tn, friendly_indices=friendly_indices)) + + +def _get_placeholder_tensors(cxn): + """Get identity placeholder tensors to directly connect LeftDangle to RightDangle. + + This function is used in `cbloq_to_quimb` and `cbloq_to_superquimb` for the following + contingency: + + >>> for cxn in cbloq.connections: + >>> if cxn.left.binst is LeftDangle and cxn.right.binst is RightDangle: + + This is needed because `CompositeBloq.iter_bloqnections` ignores `LeftDangle/RightDangle` + bloqs, and therefore we never see connections that exist only between LeftDangle and + RightDangle sentinel values. + """ + for j in range(cxn.left.reg.bitsize): + placeholder = Soquet(None, Register('simulation_placeholder', QBit())) # type: ignore + Connection(cxn.left, placeholder) + yield qtn.Tensor( + data=np.eye(2), + inds=[(Connection(cxn.left, placeholder), j), (Connection(placeholder, cxn.right), j)], + ) + + +_OuterIndT = Tuple[str, Tuple[int, ...], int, str] + + +def _get_outer_indices( + tn: 'qtn.TensorNetwork', friendly_indices: bool = False +) -> Dict[Any, Union[str, _OuterIndT]]: + """Provide a mapping for a tensor network's outer indices. + + Internal indices effectively use `qualtran.Connection` objects as their indices. The + outer indices correspond to connections to `DanglingT` items, and you end up having to + do logic to disambiguate left dangling indices from right dangling indices. This function + facilitates re-indexing the tensor network's outer indices to better match a bloq's signature. + + In particular, we map each outer index to a tuple (reg.name, soq.idx, j, group) where + group is 'l' or 'r' for left or right indices. + + If `friendly_indices` is set to True, the tuple of items is converted to a string. + + This function is called at the end of `cbloq_to_quimb` as part of a `tn.reindex(...) operation. + """ + ind_name_map: Dict[Any, Union[str, _OuterIndT]] = {} + + # Each index is a (cxn: Connection, j: int) tuple. + cxn: Connection + j: int + + for ind in tn.outer_inds(): + cxn, j = ind + if cxn.left.binst is LeftDangle: + soq = cxn.left + group = 'l' + elif cxn.right.binst is RightDangle: + soq = cxn.right + group = 'r' + else: + raise ValueError( + f"Outer indices of a tensor network should be " + f"connections to LeftDangle or RightDangle, not {cxn}" + ) + + if friendly_indices: + # Turn everything to strings + idx_str = f'{soq.idx}' if soq.idx else '' + ind_name_map[ind] = f'{soq.reg.name}{idx_str}_{j}{group}' + else: + # Keep as tuple + ind_name_map[ind] = (soq.reg.name, soq.idx, j, group) + + return ind_name_map + + +@attrs.frozen +class DiscardInd: + """Return `DiscardInd` in `Bloq.my_tensors()` to indicate an index should be discarded. + + We cannot discard an index from a state-vector pure-state simulation, so any bloq that + returns `DiscardInd` in its `my_tensors` method will cause an error in the ordinary + tensor contraction simulator. + + We can discard indices in open-system simulations by tracing out the index. When using + `Bloq.tensor_contract(superoperator=True)`, the index contained in a `DiscardInd` will be + traced out of the superoperator tensor network. + + Args: + ind_tuple: The index to trace out, of the form (cxn, j) where `j` addresses + individual bits. + """ + + ind_tuple: Tuple['ConnectionT', int] + + +def make_forward_tensor(t: qtn.Tensor): + new_inds = [(*ind, True) for ind in t.inds] + + t2 = t.copy() + t2.modify(inds=new_inds) + return t2 + + +def make_backward_tensor(t: qtn.Tensor): + new_inds = [] + for ind in t.inds: + new_inds.append((*ind, False)) + + t2 = t.H + t2.modify(inds=new_inds, tags=t.tags | {'dag'}) + return t2 + + +def cbloq_to_superquimb(cbloq: CompositeBloq, friendly_indices: bool = False) -> qtn.TensorNetwork: + """Convert a composite bloq into a superoperator tensor network. + + This simulation strategy can handle non-unitary dynamics, but is more costly. + + This function will call `Bloq.my_tensors` on each subbloq in the composite bloq to add + tensors to a quimb tensor network. This uses ths system+environment strategy for modeling + open system dynamics. In contrast to `cbloq_to_quimb`, each bloq will have + its tensors added twice: once to the part of the network representing the "forward" + wavefunction, and its conjugate added to the part of the network representing the "backward" + part of the wavefunction. If the bloq returns a sentinel value of the `DiscardInd` class, + that particular index is *traced out*: the forward and backward copies of the index are joined. + This corresponds to removing the qubit from the computation and integrating over its possible + values. Arbitrary non-unitary dynamics can be modeled by unitary interaction of the 'system' + with an 'environment' that is traced out. + + If a bloq returns a value of type `DiscardInd` in its tensors, this function must be + used. The ordinary `cbloq_to_quimb` will raise an error. + + Args: + cbloq: The composite bloq. + friendly_indices: If set to True, the outer indices of the tensor network will be renamed + from their Qualtran-computer-readable form to human-friendly strings. This may be + useful if you plan on manually manipulating the resulting tensor network but will + preclude any further processing by Qualtran functions. The indices are named + {soq.reg.name}{soq.idx}_{j}{side}{direction}, where j is the individual bit index, + side is 'l' or 'r' for left or right (resp.), and direction is 'f' or 'b' for the + forward or backward (adjoint) wavefunctions. + """ + tn = qtn.TensorNetwork([]) + + logging.info( + "Constructing a super tensor network for composite bloq of size %d", + len(cbloq.bloq_instances), + ) + + for binst, pred_cxns, succ_cxns in cbloq.iter_bloqnections(): + bloq = binst.bloq + assert isinstance(bloq, Bloq) + + inc_d = _cxns_to_cxn_dict(bloq.signature.lefts(), pred_cxns, get_me=lambda cxn: cxn.right) + out_d = _cxns_to_cxn_dict(bloq.signature.rights(), succ_cxns, get_me=lambda cxn: cxn.left) + + for tensor in bloq.my_tensors(inc_d, out_d): + if isinstance(tensor, DiscardInd): + dind = tensor.ind_tuple + tn.reindex({(*dind, True): dind, (*dind, False): dind}, inplace=True) + else: + forward_tensor = make_forward_tensor(tensor) + backward_tensor = make_backward_tensor(tensor) + tn.add(forward_tensor) + tn.add(backward_tensor) + + # Special case: Add indices corresponding to unused wires + for cxn in cbloq.connections: + if cxn.left.binst is LeftDangle and cxn.right.binst is RightDangle: + # Connections that directly tie LeftDangle to RightDangle + for id_tensor in _get_placeholder_tensors(cxn): + forward_tensor = make_forward_tensor(id_tensor) + backward_tensor = make_backward_tensor(id_tensor) + tn.add(forward_tensor) + tn.add(backward_tensor) + + return tn.reindex(_get_outer_superindices(tn, friendly_indices=friendly_indices)) + + +_SuperOuterIndT = Tuple[str, Tuple[int, ...], int, str] + + +def _get_outer_superindices( + tn: 'qtn.TensorNetwork', friendly_indices: bool = False +) -> Dict[Any, Union[str, _SuperOuterIndT]]: + """Provide a mapping for a super-tensor network's outer indices. + + Internal indices effectively use `qualtran.Connection` objects as their indices. The + outer indices correspond to connections to `DanglingT` items, and you end up having to + do logic to disambiguate left dangling indices from right dangling indices. This function + facilitates re-indexing the tensor network's outer indices to better match a bloq's signature. + + In particular, we map each outer index to a tuple (reg.name, soq.idx, j, group) where + group is 'lf', 'lb', 'rf', or 'rb' corresponding to (left or right) x (forward or backward) + indices. + + If `friendly_indices` is set to True, the tuple of items is converted to a string. + + This function is called at the end of `cbloq_to_superquimb` as part of a `tn.reindex(...) + operation. + """ + # Each index is a (cxn: Connection, j: int, forward: bool) tuple. + cxn: Connection + j: int + forward: bool + + ind_name_map: Dict[Any, Union[str, _SuperOuterIndT]] = {} + for ind in tn.outer_inds(): + cxn, j, forward = ind + if cxn.left.binst is LeftDangle: + soq = cxn.left + group = 'lf' if forward else 'lb' + elif cxn.right.binst is RightDangle: + soq = cxn.right + group = 'rf' if forward else 'rb' + else: + raise ValueError( + f"Outer indices of a tensor network should be " + f"connections to LeftDangle or RightDangle, not {cxn}" + ) + + if friendly_indices: + idx_str = f'{soq.idx}' if soq.idx else '' + ind_name_map[ind] = f'{soq.reg.name}{idx_str}_{j}{group}' + else: + ind_name_map[ind] = (soq.reg.name, soq.idx, j, group) - return tn + return ind_name_map def _add_classical_kets(bb: BloqBuilder, registers: Iterable[Register]) -> Dict[str, 'SoquetT']: From 7f40996391faef021034fb7b9da634e6081b19ea Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:26:59 -0700 Subject: [PATCH 42/68] measz --- qualtran/bloqs/basic_gates/z_basis.py | 54 +++++++++++++++++++- qualtran/bloqs/basic_gates/z_basis_test.py | 58 +++++++++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/qualtran/bloqs/basic_gates/z_basis.py b/qualtran/bloqs/basic_gates/z_basis.py index 5736f8cfb9..71b163b6b3 100644 --- a/qualtran/bloqs/basic_gates/z_basis.py +++ b/qualtran/bloqs/basic_gates/z_basis.py @@ -13,7 +13,18 @@ # limitations under the License. from functools import cached_property -from typing import cast, Dict, Iterable, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union +from typing import ( + cast, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + TYPE_CHECKING, + Union, +) import attrs import numpy as np @@ -27,6 +38,7 @@ bloq_example, BloqBuilder, BloqDocSpec, + CBit, CompositeBloq, ConnectionT, CtrlSpec, @@ -50,7 +62,7 @@ from qualtran.cirq_interop import CirqQuregT from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator - from qualtran.simulation.classical_sim import ClassicalValT + from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT _ZERO = np.array([1, 0], dtype=np.complex128) _ONE = np.array([0, 1], dtype=np.complex128) @@ -367,6 +379,44 @@ def _cz() -> CZ: _CZ_DOC = BloqDocSpec(bloq_cls=CZ, examples=[_cz], call_graph_example=None) +@frozen +class MeasZ(Bloq): + """Measure a qubit in the Z basis. + + Registers: + q [LEFT]: The qubit to measure. + c [RIGHT]: The classical measurement result. + """ + + @cached_property + def signature(self) -> 'Signature': + return Signature( + [Register('q', QBit(), side=Side.LEFT), Register('c', CBit(), side=Side.RIGHT)] + ) + + def on_classical_vals(self, q: int) -> Mapping[str, 'ClassicalValRetT']: + return {'c': q} + + def my_tensors( + self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] + ) -> List['qtn.Tensor']: + import quimb.tensor as qtn + + from qualtran.simulation.tensor import DiscardInd + + copy = np.zeros((2, 2, 2), dtype=np.complex128) + copy[0, 0, 0] = 1 + copy[1, 1, 1] = 1 + # Tie together q, c, and meas_result with the copy tensor; throw out one of the legs. + meas_result = qtn.rand_uuid('meas_result') + t = qtn.Tensor( + data=copy, + inds=[(incoming['q'], 0), (outgoing['c'], 0), (meas_result, 0)], + tags=[str(self)], + ) + return [t, DiscardInd((meas_result, 0))] + + @frozen class _IntVector(Bloq): """Represent a classical non-negative integer vector (state or effect). diff --git a/qualtran/bloqs/basic_gates/z_basis_test.py b/qualtran/bloqs/basic_gates/z_basis_test.py index 8536894cd3..93fd3fb11f 100644 --- a/qualtran/bloqs/basic_gates/z_basis_test.py +++ b/qualtran/bloqs/basic_gates/z_basis_test.py @@ -17,13 +17,17 @@ import pytest import qualtran.testing as qlt_testing -from qualtran import Bloq, BloqBuilder +from qualtran import Bloq, BloqBuilder, QUInt from qualtran.bloqs.basic_gates import ( CZ, IntEffect, IntState, + MeasZ, + MinusState, + OnEach, OneEffect, OneState, + PlusState, XGate, ZeroEffect, ZeroState, @@ -263,3 +267,55 @@ def test_cz_phased_classical(): assert final_vals['q1'] == 1 assert final_vals['q2'] == 1 assert phase == -1 + + +def test_meas_z_supertensor(): + with pytest.raises(ValueError, match=r'.*superoperator.*'): + MeasZ().tensor_contract() + + # Zero -> Zero + bb = BloqBuilder() + q = bb.add(ZeroState()) + c = bb.add(MeasZ(), q=q) + cbloq = bb.finalize(c=c) + rho = cbloq.tensor_contract(superoperator=True) + should_be = np.outer([1, 0], [1, 0]) + np.testing.assert_allclose(rho, should_be, atol=1e-8) + + # One -> One + bb = BloqBuilder() + q = bb.add(OneState()) + c = bb.add(MeasZ(), q=q) + cbloq = bb.finalize(c=c) + rho = cbloq.tensor_contract(superoperator=True) + should_be = np.outer([0, 1], [0, 1]) + np.testing.assert_allclose(rho, should_be, atol=1e-8) + + # Plus -> mixture + bb = BloqBuilder() + q = bb.add(PlusState()) + c = bb.add(MeasZ(), q=q) + cbloq = bb.finalize(c=c) + rho = cbloq.tensor_contract(superoperator=True) + should_be = np.diag([0.5, 0.5]) + np.testing.assert_allclose(rho, should_be, atol=1e-8) + + # Minus -> mixture + bb = BloqBuilder() + q = bb.add(MinusState()) + c = bb.add(MeasZ(), q=q) + cbloq = bb.finalize(c=c) + rho = cbloq.tensor_contract(superoperator=True) + should_be = np.diag([0.5, 0.5]) + np.testing.assert_allclose(rho, should_be, atol=1e-8) + + +def test_meas_z_classical(): + bb = BloqBuilder() + q = bb.add(IntState(val=52, bitsize=8)) + qs = bb.split(q) + for i in range(8): + qs[i] = bb.add(MeasZ(), q=qs[i]) + cbloq = bb.finalize(outs=qs) + (ret,) = cbloq.call_classically() + assert list(ret) == QUInt(8).to_bits(52) # type: ignore[arg-type] From 5e499dc65582326c448bc830ffa3e6c40a9db06e Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:27:08 -0700 Subject: [PATCH 43/68] discard --- qualtran/bloqs/basic_gates/discard.py | 68 ++++++++++++++++++ qualtran/bloqs/basic_gates/discard_test.py | 82 ++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 qualtran/bloqs/basic_gates/discard.py create mode 100644 qualtran/bloqs/basic_gates/discard_test.py diff --git a/qualtran/bloqs/basic_gates/discard.py b/qualtran/bloqs/basic_gates/discard.py new file mode 100644 index 0000000000..237fced276 --- /dev/null +++ b/qualtran/bloqs/basic_gates/discard.py @@ -0,0 +1,68 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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 functools import cached_property +from typing import Dict, List, TYPE_CHECKING + +from attrs import frozen + +from qualtran import Bloq, CBit, ConnectionT, QBit, Register, Side, Signature +from qualtran.simulation.classical_sim import ClassicalValT + +if TYPE_CHECKING: + from qualtran.simulation.tensor import DiscardInd + + +@frozen +class Discard(Bloq): + """Discard a classical bit. + + This is an allowed operation. + """ + + @cached_property + def signature(self) -> 'Signature': + return Signature([Register('c', CBit(), side=Side.LEFT)]) + + def on_classical_vals(self, c: int) -> Dict[str, 'ClassicalValT']: + return {} + + def my_tensors( + self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] + ) -> List['DiscardInd']: + + from qualtran.simulation.tensor import DiscardInd + + return [DiscardInd((incoming['c'], 0))] + + +@frozen +class DiscardQ(Bloq): + """Discard a qubit. + + This is a dangerous operation that can ruin your computation. This is equivalent to + measuring the qubit and throwing out the measurement operation, so it removes any coherences + involved with the qubit. Use with care. + """ + + @cached_property + def signature(self) -> 'Signature': + return Signature([Register('q', QBit(), side=Side.LEFT)]) + + def my_tensors( + self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] + ) -> List['DiscardInd']: + + from qualtran.simulation.tensor import DiscardInd + + return [DiscardInd((incoming['q'], 0))] diff --git a/qualtran/bloqs/basic_gates/discard_test.py b/qualtran/bloqs/basic_gates/discard_test.py new file mode 100644 index 0000000000..b0d93f0591 --- /dev/null +++ b/qualtran/bloqs/basic_gates/discard_test.py @@ -0,0 +1,82 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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. +import numpy as np +import pytest + +from qualtran import BloqBuilder, QBit +from qualtran.bloqs.basic_gates import ( + CNOT, + Discard, + DiscardQ, + MeasZ, + PlusState, + ZeroEffect, + ZeroState, +) + + +def test_discard(): + bb = BloqBuilder() + q = bb.add(ZeroState()) + c = bb.add(MeasZ(), q=q) + bb.add(Discard(), c=c) + cbloq = bb.finalize() + + # We're allowed to discard classical bits in the classical simulator + ret = cbloq.call_classically() + assert ret == () + + k = cbloq.tensor_contract(superoperator=True) + np.testing.assert_allclose(k, 1.0, atol=1e-8) + + +def test_discard_vs_project(): + # Using the ZeroState effect un-physically projects us, giving trace of 0.5 + bb = BloqBuilder() + q = bb.add(PlusState()) + bb.add(ZeroEffect(), q=q) + cbloq = bb.finalize() + k = cbloq.tensor_contract(superoperator=True) + np.testing.assert_allclose(k, 0.5, atol=1e-8) + + # Measure and discard is trace preserving + bb = BloqBuilder() + q = bb.add(PlusState()) + c = bb.add(MeasZ(), q=q) + bb.add(Discard(), c=c) + cbloq = bb.finalize() + k = cbloq.tensor_contract(superoperator=True) + np.testing.assert_allclose(k, 1.0, atol=1e-8) + + +def test_discardq(): + # Completely dephasing map + # https://learning.quantum.ibm.com/course/general-formulation-of-quantum-information/quantum-channels#the-completely-dephasing-channel + bb = BloqBuilder() + q = bb.add_register('q', 1) + env = bb.add(ZeroState()) + q, env = bb.add(CNOT(), ctrl=q, target=env) + bb.add(DiscardQ(), q=env) + cbloq = bb.finalize(q=q) + ss = cbloq.tensor_contract(superoperator=True) + + should_be = np.zeros((2, 2, 2, 2)) + should_be[0, 0, 0, 0] = 1 + should_be[1, 1, 1, 1] = 1 + + np.testing.assert_allclose(ss, should_be, atol=1e-8) + + # Classical simulator will not let you throw out qubits + with pytest.raises(NotImplementedError, match=r'.*classical simulation.*'): + _ = cbloq.call_classically(q=1) From 32485e39f016b26f2a7f6fec6bcfd710d8b20f3e Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:27:45 -0700 Subject: [PATCH 44/68] gates init --- qualtran/bloqs/basic_gates/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/qualtran/bloqs/basic_gates/__init__.py b/qualtran/bloqs/basic_gates/__init__.py index 66910b89f1..d89e5080a9 100644 --- a/qualtran/bloqs/basic_gates/__init__.py +++ b/qualtran/bloqs/basic_gates/__init__.py @@ -22,6 +22,7 @@ """ from .cnot import CNOT +from .discard import Discard, DiscardQ from .global_phase import GlobalPhase from .hadamard import CHadamard, Hadamard from .identity import Identity @@ -35,4 +36,14 @@ from .toffoli import Toffoli from .x_basis import MinusEffect, MinusState, PlusEffect, PlusState, XGate from .y_gate import CYGate, YGate -from .z_basis import CZ, IntEffect, IntState, OneEffect, OneState, ZeroEffect, ZeroState, ZGate +from .z_basis import ( + CZ, + IntEffect, + IntState, + MeasZ, + OneEffect, + OneState, + ZeroEffect, + ZeroState, + ZGate, +) From 34a53253423b634942dc9895b9f5b5ca916a9fd2 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:29:57 -0700 Subject: [PATCH 45/68] bloq --- qualtran/_infra/bloq.py | 45 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index a454e1e2fc..f84f048d36 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -58,6 +58,7 @@ SympySymbolAllocator, ) from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT + from qualtran.simulation.tensor import DiscardInd def _decompose_from_build_composite_bloq(bloq: 'Bloq') -> 'CompositeBloq': @@ -65,6 +66,10 @@ def _decompose_from_build_composite_bloq(bloq: 'Bloq') -> 'CompositeBloq': bb, initial_soqs = BloqBuilder.from_signature(bloq.signature, add_registers_allowed=False) out_soqs = bloq.build_composite_bloq(bb=bb, **initial_soqs) + if not isinstance(out_soqs, dict): + raise ValueError( + f'{bloq}.build_composite_bloq must return a dictionary mapping right register names to output soquets.' + ) return bb.finalize(**out_soqs) @@ -255,21 +260,45 @@ def call_classically( res = self.as_composite_bloq().on_classical_vals(**vals) return tuple(res[reg.name] for reg in self.signature.rights()) - def tensor_contract(self) -> 'NDArray': - """Return a contracted, dense ndarray representing this bloq. + def tensor_contract(self, superoperator: bool = False) -> 'NDArray': + """Return a contracted, dense ndarray encoding of this bloq. + + This method decomposes and flattens this bloq into a factorized CompositeBloq, + turns that composite bloq into a Quimb tensor network, and contracts it into a dense + ndarray. + + The returned array will be 0-, 1-, 2-, or 4-dimensional with indices arranged according to the + bloq's signature and the type of simulation requested via the `superoperator` flag. + + If `superoperator` is set to False (the default), a pure-state tensor network will be + constructed. + - If `bloq` has all thru-registers, the dense tensor will be 2-dimensional with shape `(n, n)` + where `n` is the number of bits in the signature. We follow the linear algebra convention + and order the indices as (right, left) so the matrix-vector product can be used to evolve + a state vector. + - If `bloq` has all left- or all right-registers, the tensor will be 1-dimensional with + shape `(n,)`. Note that we do not distinguish between 'row' and 'column' vectors in this + function. + - If `bloq` has no external registers, the contracted form is a 0-dimensional complex number. + + If `superoperator` is set to True, an open-system tensor network will be constructed. + - States result in a 2-dimensional density matrix with indices (right_forward, right_backward) + or (left_forward, left_backward) depending on whether they're input or output states. + - Operations result in a 4-dimensional tensor with indices (right_forward, right_backward, + left_forward, left_backward). - This constructs a tensor network and then contracts it according to our registers, - i.e. the dangling indices. The returned array will be 0-, 1- or 2-dimensional. If it is - a 2-dimensional matrix, we follow the quantum computing / matrix multiplication convention - of (right, left) indices. + Args: + superoperator: If toggled to True, do an open-system simulation. This supports + non-unitary operations like measurement, but is more costly and results in + higher-dimension resultant tensors. """ from qualtran.simulation.tensor import bloq_to_dense - return bloq_to_dense(self) + return bloq_to_dense(self, superoperator=superoperator) def my_tensors( self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] - ) -> List['qtn.Tensor']: + ) -> List[Union['qtn.Tensor', 'DiscardInd']]: """Override this method to support native quimb simulation of this Bloq. This method is responsible for returning tensors corresponding to the unitary, state, or From 4a8b431d7f11422461b97e68b06ac4ecf81cee01 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:30:50 -0700 Subject: [PATCH 46/68] flatten including cbloqs --- qualtran/_infra/composite_bloq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/_infra/composite_bloq.py b/qualtran/_infra/composite_bloq.py index 5fba9a2fc5..4fee8d2db1 100644 --- a/qualtran/_infra/composite_bloq.py +++ b/qualtran/_infra/composite_bloq.py @@ -370,7 +370,7 @@ def flatten_once( in_soqs = _map_soqs(in_soqs, soq_map) # update `in_soqs` from old to new. if pred(binst): try: - new_out_soqs = bb.add_from(binst.bloq.decompose_bloq(), **in_soqs) + new_out_soqs = bb.add_from(binst.bloq, **in_soqs) did_work = True except (DecomposeTypeError, DecomposeNotImplementedError): new_out_soqs = tuple(soq for _, soq in bb._add_binst(binst, in_soqs=in_soqs)) From d27a5c15a74258795392dc7493c432fcb80deca3 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:32:01 -0700 Subject: [PATCH 47/68] new quimb api --- qualtran/_infra/controlled_test.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qualtran/_infra/controlled_test.py b/qualtran/_infra/controlled_test.py index 3eff66f029..0a909d79ef 100644 --- a/qualtran/_infra/controlled_test.py +++ b/qualtran/_infra/controlled_test.py @@ -33,7 +33,7 @@ from qualtran.bloqs.for_testing import TestAtom, TestParallelCombo, TestSerialCombo from qualtran.drawing import get_musical_score_data from qualtran.drawing.musical_score import Circle, SoqData, TextBox -from qualtran.simulation.tensor import cbloq_to_quimb, get_right_and_left_inds +from qualtran.simulation.tensor import cbloq_to_quimb, quimb_to_dense from qualtran.symbolics import Shaped if TYPE_CHECKING: @@ -432,10 +432,8 @@ def test_controlled_tensor_without_decompose(): cgate = cirq.ControlledGate(cirq.CSWAP, control_values=ctrl_spec.to_cirq_cv()) tn = cbloq_to_quimb(ctrl_bloq.as_composite_bloq()) - # pylint: disable=unbalanced-tuple-unpacking - right, left = get_right_and_left_inds(tn, ctrl_bloq.signature) - # pylint: enable=unbalanced-tuple-unpacking - np.testing.assert_allclose(tn.to_dense(right, left), cirq.unitary(cgate), atol=1e-8) + tn_dense = quimb_to_dense(tn, ctrl_bloq.signature) + np.testing.assert_allclose(tn_dense, cirq.unitary(cgate), atol=1e-8) np.testing.assert_allclose(ctrl_bloq.tensor_contract(), cirq.unitary(cgate), atol=1e-8) From 1b229289b0bb488abf905d4575e0a9818b443350 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:33:05 -0700 Subject: [PATCH 48/68] polish --- qualtran/cirq_interop/_cirq_to_bloq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/cirq_interop/_cirq_to_bloq.py b/qualtran/cirq_interop/_cirq_to_bloq.py index 37271106a4..3063124916 100644 --- a/qualtran/cirq_interop/_cirq_to_bloq.py +++ b/qualtran/cirq_interop/_cirq_to_bloq.py @@ -219,7 +219,7 @@ def _my_tensors_from_gate( unitary = tensor_data_from_unitary_and_signature(cirq.unitary(gate), signature) inds = _order_incoming_outgoing_indices(signature, incoming=incoming, outgoing=outgoing) unitary = unitary.reshape((2,) * len(inds)) - return [qtn.Tensor(data=unitary, inds=inds, tags=[str(gate)])] + return [qtn.Tensor(data=unitary, inds=inds, tags=[gate.__class__.__name__])] @frozen(eq=False) From d75ef07c2a13d2760ffe8152d567092cfdb035a7 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:37:15 -0700 Subject: [PATCH 49/68] lint --- qualtran/bloqs/basic_gates/discard_test.py | 2 +- qualtran/bloqs/basic_gates/z_basis_test.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/qualtran/bloqs/basic_gates/discard_test.py b/qualtran/bloqs/basic_gates/discard_test.py index b0d93f0591..2666bd44aa 100644 --- a/qualtran/bloqs/basic_gates/discard_test.py +++ b/qualtran/bloqs/basic_gates/discard_test.py @@ -14,7 +14,7 @@ import numpy as np import pytest -from qualtran import BloqBuilder, QBit +from qualtran import BloqBuilder from qualtran.bloqs.basic_gates import ( CNOT, Discard, diff --git a/qualtran/bloqs/basic_gates/z_basis_test.py b/qualtran/bloqs/basic_gates/z_basis_test.py index 93fd3fb11f..a2cfdecb9c 100644 --- a/qualtran/bloqs/basic_gates/z_basis_test.py +++ b/qualtran/bloqs/basic_gates/z_basis_test.py @@ -24,7 +24,6 @@ IntState, MeasZ, MinusState, - OnEach, OneEffect, OneState, PlusState, From 2d310aa5c39a0e5b392ea7cb1f1925a0aec7fea8 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 14 Mar 2025 14:53:27 -0700 Subject: [PATCH 50/68] use a typealias for the "Any" quimb ind --- qualtran/simulation/tensor/_dense.py | 8 +++----- qualtran/simulation/tensor/_quimb.py | 12 +++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qualtran/simulation/tensor/_dense.py b/qualtran/simulation/tensor/_dense.py index e48f09e211..55ddb4ac8b 100644 --- a/qualtran/simulation/tensor/_dense.py +++ b/qualtran/simulation/tensor/_dense.py @@ -14,22 +14,20 @@ import logging from collections import defaultdict -from typing import Any, Dict, List, Tuple, TYPE_CHECKING, TypeAlias +from typing import Any, Dict, List, Tuple, TYPE_CHECKING from numpy.typing import NDArray from qualtran import Bloq, Connection, ConnectionT, Signature from ._flattening import flatten_for_tensor_contraction -from ._quimb import cbloq_to_quimb, cbloq_to_superquimb +from ._quimb import _IndT, cbloq_to_quimb, cbloq_to_superquimb if TYPE_CHECKING: import quimb.tensor as qtn logger = logging.getLogger(__name__) -_IndT: TypeAlias = Any - def _order_incoming_outgoing_indices( signature: Signature, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] @@ -69,7 +67,7 @@ def _order_incoming_outgoing_indices( def _group_outer_inds( tn: 'qtn.TensorNetwork', signature: Signature, superoperator: bool = False -) -> List[List[Any]]: +) -> List[List[_IndT]]: """Group outer indices of a tensor network. This is used by 'bloq_to_dense` and `quimb_to_dense` to return a 1-, 2-, or 4-dimensional diff --git a/qualtran/simulation/tensor/_quimb.py b/qualtran/simulation/tensor/_quimb.py index 6f067733f0..7a4583fdc4 100644 --- a/qualtran/simulation/tensor/_quimb.py +++ b/qualtran/simulation/tensor/_quimb.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import Any, cast, Dict, Iterable, Tuple, Union +from typing import Any, cast, Dict, Iterable, Tuple, TypeAlias, Union import attrs import numpy as np @@ -34,6 +34,8 @@ logger = logging.getLogger(__name__) +_IndT: TypeAlias = Any + def cbloq_to_quimb(cbloq: CompositeBloq, friendly_indices: bool = False) -> qtn.TensorNetwork: """Convert a composite bloq into a tensor network. @@ -114,7 +116,7 @@ def _get_placeholder_tensors(cxn): def _get_outer_indices( tn: 'qtn.TensorNetwork', friendly_indices: bool = False -) -> Dict[Any, Union[str, _OuterIndT]]: +) -> Dict[_IndT, Union[str, _OuterIndT]]: """Provide a mapping for a tensor network's outer indices. Internal indices effectively use `qualtran.Connection` objects as their indices. The @@ -129,7 +131,7 @@ def _get_outer_indices( This function is called at the end of `cbloq_to_quimb` as part of a `tn.reindex(...) operation. """ - ind_name_map: Dict[Any, Union[str, _OuterIndT]] = {} + ind_name_map: Dict[_IndT, Union[str, _OuterIndT]] = {} # Each index is a (cxn: Connection, j: int) tuple. cxn: Connection @@ -269,7 +271,7 @@ def cbloq_to_superquimb(cbloq: CompositeBloq, friendly_indices: bool = False) -> def _get_outer_superindices( tn: 'qtn.TensorNetwork', friendly_indices: bool = False -) -> Dict[Any, Union[str, _SuperOuterIndT]]: +) -> Dict[_IndT, Union[str, _SuperOuterIndT]]: """Provide a mapping for a super-tensor network's outer indices. Internal indices effectively use `qualtran.Connection` objects as their indices. The @@ -291,7 +293,7 @@ def _get_outer_superindices( j: int forward: bool - ind_name_map: Dict[Any, Union[str, _SuperOuterIndT]] = {} + ind_name_map: Dict[_IndT, Union[str, _SuperOuterIndT]] = {} for ind in tn.outer_inds(): cxn, j, forward = ind if cxn.left.binst is LeftDangle: From 04b5ab085620445a31a76b4f73525598a4422bb6 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Mon, 17 Mar 2025 21:40:44 -0700 Subject: [PATCH 51/68] fixes --- qualtran/simulation/tensor.ipynb | 4 +--- qualtran/simulation/tensor/_dense.py | 2 +- qualtran/simulation/tensor/_quimb_test.py | 11 ++++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/qualtran/simulation/tensor.ipynb b/qualtran/simulation/tensor.ipynb index 3628fb021b..d91373e7d8 100644 --- a/qualtran/simulation/tensor.ipynb +++ b/qualtran/simulation/tensor.ipynb @@ -434,9 +434,7 @@ "outputs": [], "source": [ "from qualtran.bloqs.basic_gates import CNOT\n", - "from qualtran.simulation.tensor import (\n", - " cbloq_to_quimb, get_right_and_left_inds\n", - ")\n", + "from qualtran.simulation.tensor import cbloq_to_quimb\n", "\n", "cbloq = CNOT().as_composite_bloq()\n", "tn = cbloq_to_quimb(cbloq, friendly_indices=True)\n", diff --git a/qualtran/simulation/tensor/_dense.py b/qualtran/simulation/tensor/_dense.py index 55ddb4ac8b..f4e630ddb0 100644 --- a/qualtran/simulation/tensor/_dense.py +++ b/qualtran/simulation/tensor/_dense.py @@ -14,7 +14,7 @@ import logging from collections import defaultdict -from typing import Any, Dict, List, Tuple, TYPE_CHECKING +from typing import Dict, List, Tuple, TYPE_CHECKING from numpy.typing import NDArray diff --git a/qualtran/simulation/tensor/_quimb_test.py b/qualtran/simulation/tensor/_quimb_test.py index cde0e06e0e..ceb9a347a0 100644 --- a/qualtran/simulation/tensor/_quimb_test.py +++ b/qualtran/simulation/tensor/_quimb_test.py @@ -19,7 +19,7 @@ import quimb.tensor as qtn from attrs import frozen -from qualtran import Bloq, BloqBuilder, Connection, ConnectionT, DanglingT, QAny, Signature +from qualtran import Bloq, BloqBuilder, ConnectionT, QAny, Signature from qualtran.bloqs.bookkeeping import Join, Split from qualtran.simulation.tensor import cbloq_to_quimb @@ -49,10 +49,11 @@ def test_cbloq_to_quimb(): tn = cbloq_to_quimb(cbloq) assert len(tn.tensors) == 4 - for outer_ind in tn.outer_inds(): - cxn, j = outer_ind - assert isinstance(cxn, Connection) - assert isinstance(cxn.left.binst, DanglingT) or isinstance(cxn.right.binst, DanglingT) + assert sorted(tn.outer_inds()) == [ + # reg_name, idx, j, side_str + ('x', (), 0, 'l'), + ('x', (), 0, 'r'), + ] def test_cbloq_to_quimb_with_no_ops_on_register(): From 3d09d6666bac8c35125a7005f35ce39538bbf932 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 9 Apr 2025 14:25:34 -0700 Subject: [PATCH 52/68] Signature.n_xbits --- qualtran/_infra/registers.py | 26 ++++++++++++++++++++++++++ qualtran/_infra/registers_test.py | 2 ++ 2 files changed, 28 insertions(+) diff --git a/qualtran/_infra/registers.py b/qualtran/_infra/registers.py index cb466564e4..bfeb8578d0 100644 --- a/qualtran/_infra/registers.py +++ b/qualtran/_infra/registers.py @@ -223,6 +223,32 @@ def n_qubits(self) -> int: right_size = ssum(reg.total_qubits() for reg in self.rights()) return smax(left_size, right_size) + def n_cbits(self) -> int: + """The number of classical bits in the signature. + + If the signature has LEFT and RIGHT registers, the number of classical bits in the signature + is taken to be the greater of the number of left or right cbits. A bloq with this + signature uses at least this many classical bits. + """ + left_size = ssum(reg.total_cbits() for reg in self.lefts()) + right_size = ssum(reg.total_cbits() for reg in self.rights()) + return smax(left_size, right_size) + + def n_bits(self) -> int: + """The number of quantum + classical bits in the signature. + + If the signature has LEFT and RIGHT registers, the number of bits in the signature + is taken to be the greater of the number of left or right bits. A bloq with this + signature uses at least this many quantum + classical bits. + + See Also: + Signature.n_qubits() + Signature.n_cbits() + """ + left_size = ssum(reg.total_bits() for reg in self.lefts()) + right_size = ssum(reg.total_bits() for reg in self.rights()) + return smax(left_size, right_size) + def __repr__(self): return f'Signature({repr(self._registers)})' diff --git a/qualtran/_infra/registers_test.py b/qualtran/_infra/registers_test.py index 425fa9bb9e..7da20da862 100644 --- a/qualtran/_infra/registers_test.py +++ b/qualtran/_infra/registers_test.py @@ -148,6 +148,8 @@ def test_partial_classical_signature_n_qubits(): [Register('x', QUInt(5)), Register('y', QUInt(5), side=Side.RIGHT), Register('c', CBit())] ) assert sig.n_qubits() == 10 + assert sig.n_cbits() == 1 + assert sig.n_bits() == 11 def test_signature_build(): From 1a01ea9b56ce940435fa9255d0071c3ad62e0c9b Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 10:55:48 -0700 Subject: [PATCH 53/68] measphase --- qualtran/_infra/bloq.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index 8572718445..61639454d2 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -60,7 +60,7 @@ GeneralizerT, SympySymbolAllocator, ) - from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT + from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT, MeasurementPhase from qualtran.simulation.tensor import DiscardInd @@ -220,7 +220,9 @@ def on_classical_vals( except NotImplementedError as e: raise NotImplementedError(f"{self} does not support classical simulation: {e}") from e - def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: + def basis_state_phase( + self, **vals: 'ClassicalValT' + ) -> Union[complex, 'MeasurementPhase', None]: """How this bloq phases classical basis states. Override this method if your bloq represents classical logic with basis-state @@ -231,7 +233,8 @@ def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: (X, CNOT, Toffoli, ...) and diagonal operations (T, CZ, CCZ, ...). Bloq authors should override this method. If you are using an instantiated bloq object, - call TODO and not this method directly. + call `qualtran.simulation.classical_sim.do_phased_classical_simulation` or use + `qualtran.simulation.classical_sim.PhasedClassicalSimState`. If this method is implemented, `on_classical_vals` must also be implemented. If `on_classical_vals` is implemented but this method is not implemented, it is assumed From ee25cb3634d81e1c7a39dd4f3ac30a7bdc5c5691 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 10:57:07 -0700 Subject: [PATCH 54/68] measphase --- qualtran/_infra/controlled.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qualtran/_infra/controlled.py b/qualtran/_infra/controlled.py index 0e99500728..87cdbfd400 100644 --- a/qualtran/_infra/controlled.py +++ b/qualtran/_infra/controlled.py @@ -47,7 +47,7 @@ from qualtran.cirq_interop import CirqQuregT from qualtran.drawing import WireSymbol from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator - from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT + from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT, MeasurementPhase ControlBit: TypeAlias = int """A control bit, either 0 or 1.""" @@ -453,7 +453,9 @@ def on_classical_vals(self, **vals: 'ClassicalValT') -> Mapping[str, 'ClassicalV return vals - def basis_state_phase(self, **vals: 'ClassicalValT') -> Union[complex, None]: + def basis_state_phase( + self, **vals: 'ClassicalValT' + ) -> Union[complex, 'MeasurementPhase', None]: """Phasing action of controlled bloqs. This involves conditionally doing the phasing action of `subbloq`. All implementers From 4cf9501cb3b4b83a4e61fb4a0d9d616cf45e04e6 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 10:57:55 -0700 Subject: [PATCH 55/68] measx --- qualtran/bloqs/basic_gates/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/basic_gates/__init__.py b/qualtran/bloqs/basic_gates/__init__.py index d89e5080a9..a1ce524e9e 100644 --- a/qualtran/bloqs/basic_gates/__init__.py +++ b/qualtran/bloqs/basic_gates/__init__.py @@ -34,7 +34,7 @@ from .swap import CSwap, Swap, TwoBitCSwap, TwoBitSwap from .t_gate import TGate from .toffoli import Toffoli -from .x_basis import MinusEffect, MinusState, PlusEffect, PlusState, XGate +from .x_basis import MeasX, MinusEffect, MinusState, PlusEffect, PlusState, XGate from .y_gate import CYGate, YGate from .z_basis import ( CZ, From 3a7621fa0db0e054b37a5ccf8d014dbadc5ed217 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:00:07 -0700 Subject: [PATCH 56/68] measx --- qualtran/bloqs/basic_gates/x_basis.py | 57 ++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/qualtran/bloqs/basic_gates/x_basis.py b/qualtran/bloqs/basic_gates/x_basis.py index 5b50f5da8d..f6e8dafc31 100644 --- a/qualtran/bloqs/basic_gates/x_basis.py +++ b/qualtran/bloqs/basic_gates/x_basis.py @@ -24,6 +24,7 @@ bloq_example, BloqBuilder, BloqDocSpec, + CBit, ConnectionT, CtrlSpec, QBit, @@ -33,6 +34,12 @@ SoquetT, ) from qualtran.drawing import directional_text_box, Text, WireSymbol +from qualtran.simulation.classical_sim import ( + ClassicalValDistribution, + ClassicalValRetT, + ClassicalValT, + MeasurementPhase, +) if TYPE_CHECKING: import cirq @@ -41,7 +48,6 @@ from pennylane.wires import Wires from qualtran.cirq_interop import CirqQuregT - from qualtran.simulation.classical_sim import ClassicalValT _PLUS = np.ones(2, dtype=np.complex128) / np.sqrt(2) _MINUS = np.array([1, -1], dtype=np.complex128) / np.sqrt(2) @@ -85,7 +91,10 @@ def my_tensors( ] def as_cirq_op( - self, qubit_manager: 'cirq.QubitManager', **cirq_quregs: 'CirqQuregT' # type: ignore[type-var] + self, + qubit_manager: 'cirq.QubitManager', + **cirq_quregs: 'CirqQuregT', + # type: ignore[type-var] ) -> Tuple[Union['cirq.Operation', None], Dict[str, 'CirqQuregT']]: # type: ignore[type-var] if not self.state: raise ValueError(f"There is no Cirq equivalent for {self}") @@ -270,3 +279,47 @@ def wire_symbol(self, reg: Register, idx: Tuple[int, ...] = tuple()) -> 'WireSym return Text('X') return ModPlus() + + def __str__(self): + return 'X' + + +@frozen +class MeasX(Bloq): + @cached_property + def signature(self) -> 'Signature': + return Signature( + [Register('q', QBit(), side=Side.LEFT), Register('c', CBit(), side=Side.RIGHT)] + ) + + def on_classical_vals(self, q: int) -> Dict[str, 'ClassicalValRetT']: + if q not in [0, 1]: + raise ValueError(f"Invalid classical value encountered in {self}: {q}") + return {'c': ClassicalValDistribution(2)} + + def basis_state_phase(self, q: int) -> Union[complex, MeasurementPhase]: + if q == 0: + return 1 + if q == 1: + return MeasurementPhase(reg_name='c') + raise ValueError(f"Invalid classical value encountered in {self}: {q}") + + def my_tensors( + self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] + ) -> List['qtn.Tensor']: + import quimb.tensor as qtn + + from qualtran.simulation.tensor import DiscardInd + + data = np.array( + [ + [[0.5 + 0.0j, 0.5 + 0.0j], [0.5 + 0.0j, -0.5 + 0.0j]], + [[0.5 + 0.0j, -0.5 + 0.0j], [0.5 + 0.0j, 0.5 + 0.0j]], + ] + ) + + q_trace = qtn.rand_uuid('q_trace') + t = qtn.Tensor( + data=data, inds=[(incoming['q'], 0), (q_trace, 0), (outgoing['c'], 0)], tags=[str(self)] + ) + return [t, DiscardInd((q_trace, 0))] From ae6d0715871cada34c4fcffcdc35216cf071516c Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:00:15 -0700 Subject: [PATCH 57/68] measx --- qualtran/bloqs/basic_gates/x_basis_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qualtran/bloqs/basic_gates/x_basis_test.py b/qualtran/bloqs/basic_gates/x_basis_test.py index edcde41797..6404247fc0 100644 --- a/qualtran/bloqs/basic_gates/x_basis_test.py +++ b/qualtran/bloqs/basic_gates/x_basis_test.py @@ -15,7 +15,7 @@ import numpy as np from qualtran import BloqBuilder -from qualtran.bloqs.basic_gates import MinusState, PlusEffect, PlusState, XGate +from qualtran.bloqs.basic_gates import MeasX, MinusState, PlusEffect, PlusState, XGate from qualtran.resource_counting import GateCounts, get_cost_value, QECGatesCost from qualtran.simulation.classical_sim import ( format_classical_truth_table, @@ -119,3 +119,8 @@ def _keep_and(b): bloq = XGate().controlled(CtrlSpec(qdtypes=QUInt(n), cvs=1)) _, sigma = bloq.call_graph(keep=_keep_and) assert sigma == {And(): n - 1, CNOT(): 1, And().adjoint(): n - 1, XGate(): 4 * (n - 1)} + + +def test_meas_x_classical_sim(): + m = MeasX() + m.call_classically(q=0) From 4e9712d319bb72a642ca6aeecad593faf55865ab Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:00:58 -0700 Subject: [PATCH 58/68] nice --- qualtran/bloqs/mcmt/and_bloq.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qualtran/bloqs/mcmt/and_bloq.py b/qualtran/bloqs/mcmt/and_bloq.py index 486bb3e49b..50f4bc5a5a 100644 --- a/qualtran/bloqs/mcmt/and_bloq.py +++ b/qualtran/bloqs/mcmt/and_bloq.py @@ -103,7 +103,10 @@ def on_classical_vals( return {'ctrl': ctrl, 'target': out} # Uncompute - assert target == out + if target != out: + raise ValueError( + f"Inconsistent `target` found for uncomputing `And`: {ctrl=}, {target=}. Expected target={out}" + ) return {'ctrl': ctrl} def my_tensors( From 180d165ca5d54f5673fc20fe36273f8bcd27fa38 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:03:14 -0700 Subject: [PATCH 59/68] nice --- qualtran/resource_counting/_bloq_counts.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/qualtran/resource_counting/_bloq_counts.py b/qualtran/resource_counting/_bloq_counts.py index 3ed9a4a10a..d293ecd5ab 100644 --- a/qualtran/resource_counting/_bloq_counts.py +++ b/qualtran/resource_counting/_bloq_counts.py @@ -297,7 +297,15 @@ class QECGatesCost(CostKey[GateCounts]): legacy_shims: bool = False def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) -> GateCounts: - from qualtran.bloqs.basic_gates import GlobalPhase, Identity, Toffoli, TwoBitCSwap + from qualtran.bloqs.basic_gates import ( + Discard, + GlobalPhase, + Identity, + MeasX, + MeasZ, + Toffoli, + TwoBitCSwap, + ) from qualtran.bloqs.basic_gates._shims import Measure from qualtran.bloqs.bookkeeping._bookkeeping_bloq import _BookkeepingBloq from qualtran.bloqs.mcmt import And, MultiTargetCNOT @@ -326,7 +334,7 @@ def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) return GateCounts(toffoli=1) # Measurement - if isinstance(bloq, Measure): + if isinstance(bloq, (Measure, MeasZ, MeasX)): return GateCounts(measurement=1) # 'And' bloqs @@ -370,9 +378,10 @@ def compute(self, bloq: 'Bloq', get_callee_cost: Callable[['Bloq'], GateCounts]) return GateCounts() # Bookkeeping, empty bloqs - if isinstance(bloq, _BookkeepingBloq) or isinstance(bloq, (GlobalPhase, Identity)): + if isinstance(bloq, _BookkeepingBloq) or isinstance(bloq, (GlobalPhase, Identity, Discard)): return GateCounts() + # Rotations if bloq_is_rotation(bloq): return GateCounts(rotation=1) From e1c3f819d77d9f7f08dbaa83e864634ec32765cc Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:20:03 -0700 Subject: [PATCH 60/68] classically controlled --- qualtran/_infra/controlled.py | 18 ++++++--- qualtran/bloqs/mcmt/classically_controlled.py | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 qualtran/bloqs/mcmt/classically_controlled.py diff --git a/qualtran/_infra/controlled.py b/qualtran/_infra/controlled.py index 87cdbfd400..d35f69cffa 100644 --- a/qualtran/_infra/controlled.py +++ b/qualtran/_infra/controlled.py @@ -380,10 +380,7 @@ def ctrl_spec(self) -> 'CtrlSpec': @cached_property def _thru_registers_only(self) -> bool: - for reg in self.subbloq.signature: - if reg.side != Side.THRU: - return False - return True + return self.signature.thru_registers_only @staticmethod def _make_ctrl_system(cb: '_ControlledBase') -> Tuple['_ControlledBase', 'AddControlledT']: @@ -535,7 +532,15 @@ def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) - from qualtran.drawing import Text if reg is None: - return Text(f'C[{self.subbloq}]') + sub_title = self.subbloq.wire_symbol(None, idx) + if not isinstance(sub_title, Text): + raise ValueError( + f"{self.subbloq} should return a `Text` object for reg=None wire symbol." + ) + if sub_title.text == '': + return Text('') + + return Text(f'C[{sub_title.text}]') if reg.name not in self.ctrl_reg_names: # Delegate to subbloq return self.subbloq.wire_symbol(reg, idx) @@ -691,6 +696,7 @@ def make_ctrl_system_with_correct_metabloq( for each subbloq in the decomposition of `bloq`. """ from qualtran.bloqs.mcmt.controlled_via_and import ControlledViaAnd + from qualtran.bloqs.mcmt.classically_controlled import ClassicallyControlled if ctrl_spec == CtrlSpec(): return Controlled.make_ctrl_system(bloq, ctrl_spec=ctrl_spec) @@ -712,6 +718,6 @@ def make_ctrl_system_with_correct_metabloq( if qdtypes: return ControlledViaAnd.make_ctrl_system(bloq, ctrl_spec=ctrl_spec) if cdtypes: - raise NotImplementedError("Stay tuned...") + return ClassicallyControlled.make_ctrl_system(bloq, ctrl_spec=ctrl_spec) raise ValueError(f"Invalid control spec: {ctrl_spec}") diff --git a/qualtran/bloqs/mcmt/classically_controlled.py b/qualtran/bloqs/mcmt/classically_controlled.py new file mode 100644 index 0000000000..d28ea4f799 --- /dev/null +++ b/qualtran/bloqs/mcmt/classically_controlled.py @@ -0,0 +1,40 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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 typing import Tuple + +import attrs + +from qualtran import AddControlledT, Bloq, CDType, CtrlSpec, QCDType +from qualtran._infra.controlled import _ControlledBase + + +@attrs.frozen +class ClassicallyControlled( _ControlledBase): + + subbloq: 'Bloq' + ctrl_spec: 'CtrlSpec' + + def __attrs_post_init__(self): + for qcdtype in self.ctrl_spec.qdtypes: + if not isinstance(qcdtype, QCDType): + raise ValueError(f"Invalid type found in `ctrl_spec`: {qcdtype}") + if not isinstance(qcdtype, CDType): + raise ValueError(f"Invalid type found in `ctrl_spec`: {qcdtype}") + + @classmethod + def make_ctrl_system( + cls, bloq: 'Bloq', ctrl_spec: 'CtrlSpec' + ) -> Tuple['_ControlledBase', 'AddControlledT']: + cb = cls(subbloq=bloq, ctrl_spec=ctrl_spec) + return cls._make_ctrl_system(cb) \ No newline at end of file From ccf711105f2cfc815ac692ae39a3cfeef512c27f Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:20:47 -0700 Subject: [PATCH 61/68] mbuc --- qualtran/simulation/MBUC.ipynb | 384 +++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 qualtran/simulation/MBUC.ipynb diff --git a/qualtran/simulation/MBUC.ipynb b/qualtran/simulation/MBUC.ipynb new file mode 100644 index 0000000000..71d2d98eb6 --- /dev/null +++ b/qualtran/simulation/MBUC.ipynb @@ -0,0 +1,384 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44f40baf-3f87-40c5-9bf1-409ac3f86e68", + "metadata": {}, + "source": [ + "# Verifying Measurement-Based Uncomputation\n", + "\n", + "Quantum information cannot be destroyed, but during a computation we may produce intermediate values that we wish to discard. We can \"uncompute\" these values by running the computation in reverse. But, [Halving the cost of quantum addition. Gidney 2017](https://arxiv.org/abs/1709.06648) shows how measurement in the X basis can effectively discard a bit. The consequence is that the remaining states of the system will pick up phases depending on the random measurement result. [Verifying Measurement Based Uncomputation\n", + ". Gidney 2019](https://algassert.com/post/1903) provides more detail about these phases. It also describes a proceedure for using a phased-classical simulator to \"fuzz test\" measurement based uncomputation circuits.\n", + "\n", + "Here, we show how Qualtran can be used to verify measurement based uncomputation circuits following Gidney's proposal." + ] + }, + { + "cell_type": "markdown", + "id": "3c3fdd7b-a94b-4f57-bb51-eec6217018df", + "metadata": {}, + "source": [ + "## Uncomputing $\\mathrm{And}$\n", + "\n", + "As a warm-up, we can use the reference classical action of `And(uncompute=True)` to verify the truth table of the operation. First, we check the bloq over valid inputs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dddb0a5-a2b6-44cb-beb9-79b9600aae5e", + "metadata": {}, + "outputs": [], + "source": [ + "import itertools\n", + "from qualtran.bloqs.mcmt import And\n", + "\n", + "and_dag = And(uncompute=True)\n", + "for q1, q2 in itertools.product(range(2), repeat=2):\n", + " trg = int(q1==1 and q2 == 1)\n", + " print(f'{q1=}, {q2=}, {trg=}', end=' ')\n", + " (q1o, q2o), = and_dag.call_classically(ctrl=[q1,q2], target=trg)\n", + " assert q1o == q1\n", + " assert q2o == q2\n", + " print('✓')" + ] + }, + { + "cell_type": "markdown", + "id": "33c34f76-b53c-4ef7-9d72-1e7f6ebf7725", + "metadata": {}, + "source": [ + "In a quantum computer, there is no error handling; but the classical simulation will helpfully inform you if you supply invalid inputs to the bloq. Here, there is an error because the `target` register does not contain the result of a (forwards) computation of $\\mathrm{And}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dae8d49d-cc11-4d1c-84f7-70765d7c621b", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " and_dag.call_classically(ctrl=[1,1], target=0)\n", + "except ValueError as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "c82cfd5d-8482-496c-8084-0abeb315a9ca", + "metadata": {}, + "source": [ + "## Naive attempt at $\\mathrm{And}^\\dagger$\n", + "\n", + "What happens if we just measure the target bit in the X basis and throw it away? We'll build this simple circuit below so we can use the phased-classical simulator to find out." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a76a727-a742-422b-a8ac-e588f00fe765", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import BloqBuilder, Register, QBit, Side, Controlled, CtrlSpec, CBit\n", + "from qualtran.bloqs.basic_gates import MeasX, Discard, CZ\n", + "\n", + "bb = BloqBuilder()\n", + "q1 = bb.add_register('q1', 1)\n", + "q2 = bb.add_register('q2', 1)\n", + "trg = bb.add_register(Register('trg', QBit(), side=Side.LEFT))\n", + "\n", + "ctrg = bb.add(MeasX(), q=trg)\n", + "bb.add(Discard(), c=ctrg)\n", + "\n", + "cbloq = bb.finalize(q1=q1, q2=q2)\n", + "from qualtran.drawing import show_bloq\n", + "show_bloq(cbloq, 'musical_score')" + ] + }, + { + "cell_type": "markdown", + "id": "ca44fb5b-e961-434e-af6d-5deaa4fa984d", + "metadata": {}, + "source": [ + "## Fuzz testing measurement circuits\n", + "\n", + "Given a computational basis state input, the X-basis measurement operation returns a random outcome. We explicitly supply a random number generator to the phased classical simulation function to support these circuits.\n", + "\n", + "Since our simulation is now stochastic, we run it 10 times and see if we get the right answer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d123b5b4-dc1c-4957-b214-e4b9d41bb700", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qualtran.simulation.classical_sim import do_phased_classical_simulation\n", + "\n", + "rng = np.random.default_rng(seed=123)\n", + "in_vals = {'q1': 1, 'q2': 1, 'trg': 1}\n", + "for _ in range(10):\n", + " out_vals, phase = do_phased_classical_simulation(cbloq, in_vals, rng=rng)\n", + " assert out_vals['q1'] == 1\n", + " assert out_vals['q2'] == 1\n", + " assert 'trg' not in out_vals\n", + " if phase == 1:\n", + " print(\"✓\", end=' ')\n", + " else:\n", + " print(f\"Bad phase: {phase}\")" + ] + }, + { + "cell_type": "markdown", + "id": "368810bd-5ffb-46c6-8a83-6db6d09dcd78", + "metadata": {}, + "source": [ + "A phase on our computational basis state will result in *relative phases amongst* the computational basis states when this operation is called on a register in superposition, so these spurious phases must be fixed." + ] + }, + { + "cell_type": "markdown", + "id": "1dba8338-21d9-49ff-b19a-845663779039", + "metadata": {}, + "source": [ + "## MBUC circuit for $\\mathrm{And}^\\dagger$\n", + "\n", + "So simply measuring the bit in an orthogonal basis and throwing it away hasn't worked. The fix here is straightforward: a phase is encountered when the target bit is `1` and the random measurement outcome is also `1`, so we can flip it back. We flip the phase conditioned on 1) the two control qubits being `1` and 2) the classical measurement result being `1`. The first condition can be achieved with a `CZ`. We use a classically-controlled `CZ` to implement conditions (1) *and* (2) with only a Clifford operation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6717ad61-132d-4df7-9d7e-63a38ddcf233", + "metadata": {}, + "outputs": [], + "source": [ + "bb = BloqBuilder()\n", + "q1 = bb.add_register('q1', 1)\n", + "q2 = bb.add_register('q2', 1)\n", + "trg = bb.add_register(Register('trg', QBit(), side=Side.LEFT))\n", + "\n", + "ctrg = bb.add(MeasX(), q=trg)\n", + "classically_controlled_cz = CZ().controlled(CtrlSpec(qdtypes=[CBit()]))\n", + "ctrg, q1, q2 = bb.add(\n", + " classically_controlled_cz,\n", + " **{'ctrl': ctrg,\n", + " 'q1': q1,\n", + " 'q2': q2\n", + " }\n", + ")\n", + "bb.add(Discard(), c=ctrg)\n", + "\n", + "cbloq = bb.finalize(q1=q1, q2=q2)\n", + "show_bloq(cbloq, 'musical_score')" + ] + }, + { + "cell_type": "markdown", + "id": "2d2455c1-715f-4040-a7d1-a9197e9b1992", + "metadata": {}, + "source": [ + "## Fuzz testing MBUC\n", + "\n", + "We can continue to use random measurement results in simulation to \"fuzz test\" our construction. Here, all ten runs pass our check." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65b0fbf5-f341-452f-af32-74e309207e48", + "metadata": {}, + "outputs": [], + "source": [ + "rng = np.random.default_rng(seed=123)\n", + "in_vals = {'q1': 1, 'q2': 1, 'trg': 1}\n", + "for _ in range(10):\n", + " out_vals, phase = do_phased_classical_simulation(cbloq, in_vals, rng=rng)\n", + " assert out_vals['q1'] == 1\n", + " assert out_vals['q2'] == 1\n", + " assert 'trg' not in out_vals\n", + " if phase == 1:\n", + " print(\"✓\", end=' ')\n", + " else:\n", + " print(f\"Bad phase: {phase}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4c6a8409-0dff-47ec-8a44-efa7edcc157c", + "metadata": {}, + "source": [ + "## Exhaustive testing of MBUC\n", + "\n", + "With some additional work, we can inject particular patterns of measurement results to check all possible cases. For circuits with a small number of `MeasX` bloqs, this can be more valuable than fuzz testing. The exhaustive number of cases grows exponentially in the number of measured bits." + ] + }, + { + "cell_type": "markdown", + "id": "aa8e8503-242f-42c3-9f89-ae24a4dd7934", + "metadata": {}, + "source": [ + "#### Preparation: find the indices our measurement operations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0e233b4-2ec0-4c37-9b3f-101be212e673", + "metadata": {}, + "outputs": [], + "source": [ + "# Prep work: find the bloq instance indices of measurement operations.\n", + "# Here, there's only one; but this code snippet will work for MBUC circuits\n", + "# with additional MeasX bloqs\n", + "meas_binst_is = [binst.i for binst in cbloq.bloq_instances if binst.bloq_is(MeasX)]\n", + "meas_binst_is" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca79b03b-4c98-41ad-ac2c-40c58a739a30", + "metadata": {}, + "outputs": [], + "source": [ + "meas_binst_i = meas_binst_is[0]\n", + "meas_binst_i" + ] + }, + { + "cell_type": "markdown", + "id": "60ec0fd0-1b48-45d5-80da-de4cac34e9aa", + "metadata": {}, + "source": [ + "### Loop over inputs *and* measurement results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98c2a2fb-125e-4388-a5a2-f905083cbf31", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.simulation.classical_sim import PhasedClassicalSimState\n", + "import itertools\n", + "\n", + "for q1, q2 in itertools.product(range(2), repeat=2):\n", + " trg = int(q1==1 and q2 == 1)\n", + " print(f'{q1=}, {q2=}, {trg=}')\n", + "\n", + " for meas_result in [0, 1]:\n", + " print(f' meas {meas_result}', end=' ')\n", + " fixed_rnd_vals = {meas_binst_i: meas_result}\n", + " sim = PhasedClassicalSimState.from_cbloq(\n", + " cbloq, \n", + " vals={'q1': q1, 'q2': q2, 'trg': trg},\n", + " fixed_rnd_vals={meas_binst_i: meas_result}\n", + " )\n", + " out_vals = sim.simulate()\n", + " \n", + " assert out_vals['q1'] == q1\n", + " assert out_vals['q2'] == q2\n", + " assert 'trg' not in out_vals\n", + " assert phase == 1.0\n", + " print(' ✓')" + ] + }, + { + "cell_type": "markdown", + "id": "f5b50da9-cbf8-4663-9cb2-164675c079ff", + "metadata": {}, + "source": [ + "### Inspecting the phase during simulation\n", + "\n", + "For visibility into the progress of the simulation, we extend the `step` method of the simulator to print out the current phase of the system. We've also modified the exhaustive loop to use `itertools.product` so this code snippet can handle circuits with multiple `MeasX` gates (with exponential scaling). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20a732fe-74d8-43ea-80bf-f7c3d7b09e3f", + "metadata": {}, + "outputs": [], + "source": [ + "class DebugPhasedClassicalSim(PhasedClassicalSimState):\n", + " \"\"\"Phased-classical simulator that prints debug information.\"\"\"\n", + " \n", + " def step(self):\n", + " \"\"\"At each step, print a brief representation of the current phase.\"\"\"\n", + " super().step()\n", + " if sim.phase == 1.0:\n", + " print('+', end='')\n", + " elif sim.phase == -1.0:\n", + " print('-', end='')\n", + " else:\n", + " print('?', end='')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47c2db6d-b7ba-4710-9406-e1737d34cef1", + "metadata": {}, + "outputs": [], + "source": [ + "meas_binst_is = [binst.i for binst in cbloq.bloq_instances if binst.bloq_is(MeasX)]\n", + "\n", + "for q1, q2 in itertools.product(range(2), repeat=2):\n", + " trg = int(q1==1 and q2 == 1)\n", + " print(f'{q1=}, {q2=}, {trg=}')\n", + "\n", + " for meas_result in itertools.product(range(2), repeat=len(meas_binst_is)):\n", + " print(f' meas {meas_result}', end=' ')\n", + " fixed_rnd_vals = {binst_i: meas_result[j] for j, binst_i in enumerate(meas_binst_is)}\n", + "\n", + " sim = DebugPhasedClassicalSim.from_cbloq(\n", + " cbloq,\n", + " {'q1': q1, 'q2': q2, 'trg': trg},\n", + " fixed_rnd_vals=fixed_rnd_vals\n", + " )\n", + " out_vals = sim.simulate()\n", + " \n", + " assert out_vals['q1'] == q1\n", + " assert out_vals['q2'] == q2\n", + " assert 'trg' not in out_vals\n", + " assert phase == 1.0\n", + " print(' ✓')" + ] + }, + { + "cell_type": "markdown", + "id": "89b36959-ce99-4b65-a89a-8175b4849127", + "metadata": {}, + "source": [ + "Note that the phase is unaffected for all cases except when the `target` bit is `1` *and* the measurement result is `1`. Note that it is immediately fixed by the classically-controlled CZ." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From dd76ed7df57ed88850226ac260fd408616abb5d6 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:21:03 -0700 Subject: [PATCH 62/68] mbuc --- qualtran/simulation/classical_sim.py | 123 +++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 9 deletions(-) diff --git a/qualtran/simulation/classical_sim.py b/qualtran/simulation/classical_sim.py index 927c93e00a..a771984657 100644 --- a/qualtran/simulation/classical_sim.py +++ b/qualtran/simulation/classical_sim.py @@ -13,6 +13,7 @@ # limitations under the License. """Functionality for the `Bloq.call_classically(...)` protocol.""" +import abc import itertools from typing import ( Any, @@ -28,6 +29,7 @@ Union, ) +import attrs import networkx as nx import numpy as np import sympy @@ -43,13 +45,13 @@ Signature, Soquet, ) -from qualtran._infra.composite_bloq import _binst_to_cxns +from qualtran._infra.composite_bloq import _binst_to_cxns, _get_soquet if TYPE_CHECKING: from qualtran import CompositeBloq, QCDType ClassicalValT = Union[int, np.integer, NDArray[np.integer]] -ClassicalValRetT = Union[int, np.integer, NDArray[np.integer]] +ClassicalValRetT = Union[int, np.integer, NDArray[np.integer], 'ClassicalValDistribution'] def _numpy_dtype_from_qlt_dtype(dtype: 'QCDType') -> Type: @@ -106,6 +108,49 @@ def _get_in_vals( return arg +@attrs.frozen(hash=False) +class ClassicalValDistribution: + """Return this if ... + + Args: + a: An array of choices, or `np.arange` if an integer is given. This is the `a` parameter + to `np.random.Generator.choice()`. + p: An array of probabilities. If not supplied, the uniform distribution is assumed. This + is the `p` parameter to `np.random.Generator.choice()`. + """ + + a: Union[int, np.typing.ArrayLike] + p: Optional[np.typing.ArrayLike] = None + + +class _RandomValHandler(metaclass=abc.ABCMeta): + + @abc.abstractmethod + def get(self, binst: 'BloqInstance', a, p) -> Any: ... + + +class _RandomRandomValHandler(_RandomValHandler): + def __init__(self, rng): + self._gen = rng + + def get(self, binst, a, p): + return self._gen.choice(a, p=p) + + +class _FixedRandomValHandler(_RandomValHandler): + def __init__(self, binst_i_to_val: Dict[int, Any]): + self._binst_i_to_val = binst_i_to_val + + def get(self, binst, a, p): + return self._binst_i_to_val[binst.i] + + +class _BannedRandomValHandler(_RandomValHandler): + + def get(self, binst: 'BloqInstance', a, p) -> Any: + raise ValueError(f"{binst} has non-deterministic classical action. TODO: advice.") + + class ClassicalSimState: """A mutable class for classically simulating composite bloqs. @@ -138,10 +183,12 @@ def __init__( signature: 'Signature', binst_graph: nx.DiGraph, vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]], + rnd_handler: '_RandomValHandler' = _BannedRandomValHandler(), ): self._signature = signature self._binst_graph = binst_graph self._binst_iter = nx.topological_sort(self._binst_graph) + self._rnd_handler = rnd_handler # Keep track of each soquet's bit array. Initialize with LeftDangle self.soq_assign: Dict[Soquet, ClassicalValT] = {} @@ -206,6 +253,8 @@ def _update_assign_from_vals( else: # `val` is one value. + if isinstance(val, ClassicalValDistribution): + val = self._rnd_handler.get(binst, val.a, val.p) reg.dtype.assert_valid_classical_val(val, debug_str) soq = Soquet(binst, reg) self.soq_assign[soq] = val @@ -298,6 +347,17 @@ def simulate(self) -> Dict[str, 'ClassicalValT']: return self.finalize() +@attrs.frozen +class MeasurementPhase: + """Sentinel value to return from `Bloq.basis_state_phase` if a phase should be applied based on a measurement outcome. + + This can be used in special circumstances to verify measurement-based uncomputation (MBUC). + """ + + reg_name: str + idx: Tuple[int, ...] = () + + class PhasedClassicalSimState(ClassicalSimState): """A mutable class for classically simulating composite bloqs with phase tracking. @@ -328,16 +388,22 @@ def __init__( signature: 'Signature', binst_graph: nx.DiGraph, vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]], - *, + rnd_handler: '_RandomValHandler', phase: complex = 1.0, ): - super().__init__(signature=signature, binst_graph=binst_graph, vals=vals) + super().__init__( + signature=signature, binst_graph=binst_graph, vals=vals, rnd_handler=rnd_handler + ) _assert_valid_phase(phase) self.phase = phase @classmethod def from_cbloq( - cls, cbloq: 'CompositeBloq', vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]] + cls, + cbloq: 'CompositeBloq', + vals: Mapping[str, Union[sympy.Symbol, ClassicalValT]], + rng=None, + fixed_rnd_vals=None, ) -> 'PhasedClassicalSimState': """Initiate a classical simulation from a CompositeBloq. @@ -349,7 +415,23 @@ def from_cbloq( Returns: A new classical sim state. """ - return cls(signature=cbloq.signature, binst_graph=cbloq._binst_graph, vals=vals) + if rng is not None and fixed_rnd_vals is not None: + raise ValueError("Supply either `seed` or `fixed_rnd_vals`, not both.") + + rnd_handler: _RandomValHandler + if rng is not None: + rnd_handler = _RandomRandomValHandler(rng=rng) + elif fixed_rnd_vals is not None: + rnd_handler = _FixedRandomValHandler(binst_i_to_val=fixed_rnd_vals) + else: + rnd_handler = _BannedRandomValHandler() + + return cls( + signature=cbloq.signature, + binst_graph=cbloq._binst_graph, + vals=vals, + rnd_handler=rnd_handler, + ) def _binst_basis_state_phase(self, binst, in_vals): """Call `basis_state_phase` on a given bloq instance. @@ -359,7 +441,25 @@ def _binst_basis_state_phase(self, binst, in_vals): """ bloq = binst.bloq bloq_phase = bloq.basis_state_phase(**in_vals) - if bloq_phase is not None: + if isinstance(bloq_phase, MeasurementPhase): + # In this special case, there is a coupling between the classical result and the + # phase result (because the classical result is stochastic). We look up the measurement + # result and apply a phase if it is `1`. + meas_result = self.soq_assign[ + _get_soquet( + binst=binst, + reg_name=bloq_phase.reg_name, + right=True, + idx=bloq_phase.idx, + binst_graph=self._binst_graph, + ) + ] + if meas_result == 1: + self.phase *= -1.0 + else: + # Measurement result of 0, phase of +1 + pass + elif bloq_phase is not None: _assert_valid_phase(bloq_phase) self.phase *= bloq_phase else: @@ -398,7 +498,9 @@ def _assert_valid_phase(p: complex, atol: float = 1e-8): raise ValueError(f"Phases must have unit modulus. Found {p}.") -def do_phased_classical_simulation(bloq: 'Bloq', vals: Mapping[str, 'ClassicalValT']): +def do_phased_classical_simulation( + bloq: 'Bloq', vals: Mapping[str, 'ClassicalValT'], rng: Optional['np.random.Generator'] = None +): """Do a phased classical simulation of the bloq. This provides a simple interface to `PhasedClassicalSimState`. Advanced users @@ -408,13 +510,16 @@ def do_phased_classical_simulation(bloq: 'Bloq', vals: Mapping[str, 'ClassicalVa bloq: The bloq to simulate vals: A mapping from input register name to initial classical values. The initial phase is assumed to be 1.0. + rng: A numpy random generator (e.g. from `np.random.default_rng()`). This function + will use this generator to supply random values from certain phased-classical operations + like `MeasX`. If not supplied, stochastic operations will result in an error. Returns: final_vals: A mapping of output register name to final classical values. phase: The final phase. """ cbloq = bloq.as_composite_bloq() - sim = PhasedClassicalSimState.from_cbloq(cbloq, vals=vals) + sim = PhasedClassicalSimState.from_cbloq(cbloq, vals=vals, rng=rng) final_vals = sim.simulate() phase = sim.phase return final_vals, phase From 762ae8884dc937cda405d3127215f12a00efcf52 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:21:23 -0700 Subject: [PATCH 63/68] mbuc --- qualtran/_infra/composite_bloq.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/qualtran/_infra/composite_bloq.py b/qualtran/_infra/composite_bloq.py index 9832b9ffaa..20236d237b 100644 --- a/qualtran/_infra/composite_bloq.py +++ b/qualtran/_infra/composite_bloq.py @@ -512,6 +512,52 @@ def _binst_to_cxns( return pred_cxns, succ_cxns +def _get_soquet( + binst: 'BloqInstance', + reg_name: str, + right: bool = False, + idx: Tuple[int, ...] = (), + *, + binst_graph: nx.DiGraph, +) -> 'Soquet': + """Retrieve a soquet given an address. + + We can uniquely address a Soquet by the arguments to this function. + + its bloq instance `binst`; + the register name `reg_name`; which side we want: an input, left soquet if `right` is + False; otherwise the right, output soquet, and + + If you want to address the soquet + using only plain-old-data-types or don't have the bloq instance handy, you can combine + this method with `get_binst`. + + >>> cbloq.get_soquet(cbloq.get_binst(binst_i=23), reg_name='ctrl', right=False) + + Args: + binst: The bloq instance associated with the desired soquet. + reg_name: The name of the register associated with the desired soquet. + right: If False, get the input, left soquet. Otherwise: the right, output soquet + idx: The index of the soquet within a multidimensional register, or the empty + tuple for basic registers. + """ + preds, succs = _binst_to_cxns(binst, binst_graph=binst_graph) + if right: + for suc in succs: + me = suc.left + if me.reg.name == reg_name and me.idx == idx: + return me + else: + for pred in preds: + me = pred.right + if me.reg.name == reg_name and me.idx == idx: + return me + + raise ValueError( + f"Could not find the requested soquet with {binst=}, {reg_name=}, {right=}, {idx=}" + ) + + def _cxns_to_soq_dict( regs: Iterable[Register], cxns: Iterable[Connection], From 1963483b2a134b2278ee38ce008f3c7ec8f83fa4 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:22:15 -0700 Subject: [PATCH 64/68] nice --- qualtran/_infra/registers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qualtran/_infra/registers.py b/qualtran/_infra/registers.py index bfeb8578d0..ff5fc40c72 100644 --- a/qualtran/_infra/registers.py +++ b/qualtran/_infra/registers.py @@ -179,6 +179,13 @@ def build_from_dtypes(cls, **registers: QCDType) -> 'Signature': """ return cls(Register(name=k, dtype=v) for k, v in registers.items() if v.num_qubits) + @cached_property + def thru_registers_only(self) -> bool: + for reg in self: + if reg.side != Side.THRU: + return False + return True + def lefts(self) -> Iterable[Register]: """Iterable over all registers that appear on the LEFT as input.""" yield from self._lefts.values() From d16be3cd73b883c5a81f05fee83c6b73e8ed462b Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Wed, 11 Jun 2025 11:22:35 -0700 Subject: [PATCH 65/68] nice --- qualtran/_infra/registers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qualtran/_infra/registers.py b/qualtran/_infra/registers.py index ff5fc40c72..daf8ae3c9f 100644 --- a/qualtran/_infra/registers.py +++ b/qualtran/_infra/registers.py @@ -16,6 +16,7 @@ import enum import itertools from collections import defaultdict +from functools import cached_property from typing import cast, Dict, Iterable, Iterator, List, overload, Tuple, Union import attrs From b6731498792ca3b7855b64ff0356282e3de3ce40 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 11 Jul 2025 10:18:15 -0700 Subject: [PATCH 66/68] fix copyrights --- qualtran/bloqs/basic_gates/discard.py | 2 +- qualtran/bloqs/basic_gates/discard_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qualtran/bloqs/basic_gates/discard.py b/qualtran/bloqs/basic_gates/discard.py index 237fced276..90df55c367 100644 --- a/qualtran/bloqs/basic_gates/discard.py +++ b/qualtran/bloqs/basic_gates/discard.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/qualtran/bloqs/basic_gates/discard_test.py b/qualtran/bloqs/basic_gates/discard_test.py index 2666bd44aa..da90792be4 100644 --- a/qualtran/bloqs/basic_gates/discard_test.py +++ b/qualtran/bloqs/basic_gates/discard_test.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 3fb447a7709aba3130554fac97b0e62cbbd1b1b3 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 11 Jul 2025 10:23:23 -0700 Subject: [PATCH 67/68] minor fixes --- qualtran/_infra/composite_bloq.py | 12 +----------- qualtran/_infra/controlled.py | 2 +- qualtran/bloqs/mcmt/classically_controlled.py | 6 +++--- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/qualtran/_infra/composite_bloq.py b/qualtran/_infra/composite_bloq.py index 20236d237b..1d3f91e0b4 100644 --- a/qualtran/_infra/composite_bloq.py +++ b/qualtran/_infra/composite_bloq.py @@ -520,20 +520,10 @@ def _get_soquet( *, binst_graph: nx.DiGraph, ) -> 'Soquet': - """Retrieve a soquet given an address. + """Retrieve a soquet given identifying information. We can uniquely address a Soquet by the arguments to this function. - its bloq instance `binst`; - the register name `reg_name`; which side we want: an input, left soquet if `right` is - False; otherwise the right, output soquet, and - - If you want to address the soquet - using only plain-old-data-types or don't have the bloq instance handy, you can combine - this method with `get_binst`. - - >>> cbloq.get_soquet(cbloq.get_binst(binst_i=23), reg_name='ctrl', right=False) - Args: binst: The bloq instance associated with the desired soquet. reg_name: The name of the register associated with the desired soquet. diff --git a/qualtran/_infra/controlled.py b/qualtran/_infra/controlled.py index d35f69cffa..cac91d441d 100644 --- a/qualtran/_infra/controlled.py +++ b/qualtran/_infra/controlled.py @@ -695,8 +695,8 @@ def make_ctrl_system_with_correct_metabloq( `ControlledViaAnd`, which computes the activation function once and re-uses it for each subbloq in the decomposition of `bloq`. """ - from qualtran.bloqs.mcmt.controlled_via_and import ControlledViaAnd from qualtran.bloqs.mcmt.classically_controlled import ClassicallyControlled + from qualtran.bloqs.mcmt.controlled_via_and import ControlledViaAnd if ctrl_spec == CtrlSpec(): return Controlled.make_ctrl_system(bloq, ctrl_spec=ctrl_spec) diff --git a/qualtran/bloqs/mcmt/classically_controlled.py b/qualtran/bloqs/mcmt/classically_controlled.py index d28ea4f799..0d8c72e513 100644 --- a/qualtran/bloqs/mcmt/classically_controlled.py +++ b/qualtran/bloqs/mcmt/classically_controlled.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ @attrs.frozen -class ClassicallyControlled( _ControlledBase): +class ClassicallyControlled(_ControlledBase): subbloq: 'Bloq' ctrl_spec: 'CtrlSpec' @@ -37,4 +37,4 @@ def make_ctrl_system( cls, bloq: 'Bloq', ctrl_spec: 'CtrlSpec' ) -> Tuple['_ControlledBase', 'AddControlledT']: cb = cls(subbloq=bloq, ctrl_spec=ctrl_spec) - return cls._make_ctrl_system(cb) \ No newline at end of file + return cls._make_ctrl_system(cb) From 8d8a9747f22438916f493a97d7bcef8a54eb8847 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Mon, 14 Jul 2025 16:19:11 -0700 Subject: [PATCH 68/68] variable name change --- qualtran/simulation/MBUC.ipynb | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/qualtran/simulation/MBUC.ipynb b/qualtran/simulation/MBUC.ipynb index 71d2d98eb6..73fe50fc6f 100644 --- a/qualtran/simulation/MBUC.ipynb +++ b/qualtran/simulation/MBUC.ipynb @@ -7,8 +7,7 @@ "source": [ "# Verifying Measurement-Based Uncomputation\n", "\n", - "Quantum information cannot be destroyed, but during a computation we may produce intermediate values that we wish to discard. We can \"uncompute\" these values by running the computation in reverse. But, [Halving the cost of quantum addition. Gidney 2017](https://arxiv.org/abs/1709.06648) shows how measurement in the X basis can effectively discard a bit. The consequence is that the remaining states of the system will pick up phases depending on the random measurement result. [Verifying Measurement Based Uncomputation\n", - ". Gidney 2019](https://algassert.com/post/1903) provides more detail about these phases. It also describes a proceedure for using a phased-classical simulator to \"fuzz test\" measurement based uncomputation circuits.\n", + "Quantum information cannot be destroyed, but during a computation we may produce intermediate values that we wish to discard. We can \"uncompute\" these values by running the computation in reverse. The ordinary uncomputation strategy requires paying the cost of the computation twice, but [*Halving the cost of quantum addition.* Gidney 2017](https://arxiv.org/abs/1709.06648) shows how measurement in the X basis can effectively discard a bit without expensive uncomputation. The consequence is that the remaining states of the system will pick up phases depending on the random measurement result. [*Verifying Measurement Based Uncomputation.* Gidney 2019](https://algassert.com/post/1903) provides more detail about these phases. It also describes a proceedure for using a phased-classical simulator to \"fuzz test\" measurement-based uncomputation circuits.\n", "\n", "Here, we show how Qualtran can be used to verify measurement based uncomputation circuits following Gidney's proposal." ] @@ -92,9 +91,9 @@ "ctrg = bb.add(MeasX(), q=trg)\n", "bb.add(Discard(), c=ctrg)\n", "\n", - "cbloq = bb.finalize(q1=q1, q2=q2)\n", + "throw_out_target = bb.finalize(q1=q1, q2=q2)\n", "from qualtran.drawing import show_bloq\n", - "show_bloq(cbloq, 'musical_score')" + "show_bloq(throw_out_target, 'musical_score')" ] }, { @@ -122,7 +121,7 @@ "rng = np.random.default_rng(seed=123)\n", "in_vals = {'q1': 1, 'q2': 1, 'trg': 1}\n", "for _ in range(10):\n", - " out_vals, phase = do_phased_classical_simulation(cbloq, in_vals, rng=rng)\n", + " out_vals, phase = do_phased_classical_simulation(throw_out_target, in_vals, rng=rng)\n", " assert out_vals['q1'] == 1\n", " assert out_vals['q2'] == 1\n", " assert 'trg' not in out_vals\n", @@ -173,8 +172,8 @@ ")\n", "bb.add(Discard(), c=ctrg)\n", "\n", - "cbloq = bb.finalize(q1=q1, q2=q2)\n", - "show_bloq(cbloq, 'musical_score')" + "mbuc_target = bb.finalize(q1=q1, q2=q2)\n", + "show_bloq(mbuc_target, 'musical_score')" ] }, { @@ -197,7 +196,7 @@ "rng = np.random.default_rng(seed=123)\n", "in_vals = {'q1': 1, 'q2': 1, 'trg': 1}\n", "for _ in range(10):\n", - " out_vals, phase = do_phased_classical_simulation(cbloq, in_vals, rng=rng)\n", + " out_vals, phase = do_phased_classical_simulation(mbuc_target, in_vals, rng=rng)\n", " assert out_vals['q1'] == 1\n", " assert out_vals['q2'] == 1\n", " assert 'trg' not in out_vals\n", @@ -222,7 +221,7 @@ "id": "aa8e8503-242f-42c3-9f89-ae24a4dd7934", "metadata": {}, "source": [ - "#### Preparation: find the indices our measurement operations" + "#### Preparation: find the bloq index of our measurement operation" ] }, { @@ -235,17 +234,9 @@ "# Prep work: find the bloq instance indices of measurement operations.\n", "# Here, there's only one; but this code snippet will work for MBUC circuits\n", "# with additional MeasX bloqs\n", + "cbloq = mbuc_target\n", "meas_binst_is = [binst.i for binst in cbloq.bloq_instances if binst.bloq_is(MeasX)]\n", - "meas_binst_is" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ca79b03b-4c98-41ad-ac2c-40c58a739a30", - "metadata": {}, - "outputs": [], - "source": [ + "assert len(meas_binst_is) == 1, 'this circuit only has one'\n", "meas_binst_i = meas_binst_is[0]\n", "meas_binst_i" ]