Skip to content

[open-systems] Superoperator tensor contraction #1596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 68 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
a9d5d7f
notey
mpharrigan Mar 5, 2025
62e9fee
ctrl
mpharrigan Mar 5, 2025
ac2db81
qdtype
mpharrigan Mar 5, 2025
fbca42b
ctrl spec
mpharrigan Mar 5, 2025
4dfe53a
controlled
mpharrigan Mar 5, 2025
0f3b5a1
controlled
mpharrigan Mar 5, 2025
d8bbe49
data types
mpharrigan Mar 5, 2025
a747b51
on classical vals
mpharrigan Mar 5, 2025
497d7ee
dtypes
mpharrigan Mar 5, 2025
9bba4e4
registers
mpharrigan Mar 5, 2025
efe6f0f
registers
mpharrigan Mar 5, 2025
e145433
bloq ctrl spec
mpharrigan Mar 5, 2025
dedc304
cast
mpharrigan Mar 5, 2025
9fc5300
bloq dtypes
mpharrigan Mar 5, 2025
e0f2b41
controlled
mpharrigan Mar 5, 2025
c58116c
bloq dtypes
mpharrigan Mar 5, 2025
535c8ef
ctrl
mpharrigan Mar 5, 2025
9e97cc9
bloq dtypes
mpharrigan Mar 5, 2025
f283249
bloq dtypes
mpharrigan Mar 5, 2025
a26cdab
bloq dtypes
mpharrigan Mar 5, 2025
f9bd77d
bloq dtypes
mpharrigan Mar 5, 2025
a10a331
bloq dtypes
mpharrigan Mar 5, 2025
1c758d4
bloq dtypes
mpharrigan Mar 5, 2025
f57cdcc
drawing
mpharrigan Mar 5, 2025
d0773f1
drawing
mpharrigan Mar 5, 2025
ba39913
annotations
mpharrigan Mar 5, 2025
300b8ba
bits
mpharrigan Mar 5, 2025
bad1f36
bits
mpharrigan Mar 5, 2025
d668e62
init
mpharrigan Mar 5, 2025
e699973
init
mpharrigan Mar 5, 2025
85907da
controlled docs
mpharrigan Mar 5, 2025
96f2cbb
lint
mpharrigan Mar 5, 2025
919bdd9
fixes
mpharrigan Mar 5, 2025
b05d94d
Merge remote-tracking branch 'origin/main' into 2025-03/classical-1
mpharrigan Mar 5, 2025
40ef237
format
mpharrigan Mar 5, 2025
22e3184
autogenerate notebooks
mpharrigan Mar 5, 2025
aabe689
Merge remote-tracking branch 'origin/main' into 2025-03/classical-1
mpharrigan Mar 5, 2025
70bac9a
add notebook to toc
mpharrigan Mar 5, 2025
6e93114
fix musical score serialization
mpharrigan Mar 5, 2025
8951104
phased classical sim
mpharrigan Mar 5, 2025
9e1bb8c
docstring
mpharrigan Mar 5, 2025
7492027
Merge remote-tracking branch 'origin/main' into 2025-03/classical-1
mpharrigan Mar 10, 2025
8fb94c6
Merge branch '2025-03/classical-1' into 2025-03/classical-sim-1
mpharrigan Mar 10, 2025
3a395f7
stuff
mpharrigan Mar 10, 2025
fa68016
Merge remote-tracking branch 'origin/main' into 2025-03/classical-1
mpharrigan Mar 13, 2025
63ed9b4
Merge branch '2025-03/classical-1' into 2025-03/classical-sim-1
mpharrigan Mar 13, 2025
9aad5f0
superquimb
mpharrigan Mar 14, 2025
7f40996
measz
mpharrigan Mar 14, 2025
5e499dc
discard
mpharrigan Mar 14, 2025
32485e3
gates init
mpharrigan Mar 14, 2025
34a5325
bloq
mpharrigan Mar 14, 2025
4a8b431
flatten including cbloqs
mpharrigan Mar 14, 2025
d27a5c1
new quimb api
mpharrigan Mar 14, 2025
1b22928
polish
mpharrigan Mar 14, 2025
d75ef07
lint
mpharrigan Mar 14, 2025
2d310aa
use a typealias for the "Any" quimb ind
mpharrigan Mar 14, 2025
04b5ab0
fixes
mpharrigan Mar 18, 2025
678ba7b
Merge remote-tracking branch 'origin/main' into 2025-03/classical-1
mpharrigan Apr 7, 2025
bb6b510
Merge branch '2025-03/classical-1' into 2025-03/classical-sim-1
mpharrigan Apr 7, 2025
6de6b8e
Merge branch '2025-03/classical-sim-1' into 2025-03/open-quimb-1
mpharrigan Apr 7, 2025
5c79a89
Merge remote-tracking branch 'origin/main' into 2025-03/classical-1
mpharrigan Apr 9, 2025
d0b77d6
Merge branch '2025-03/classical-1' into 2025-03/classical-sim-1
mpharrigan Apr 9, 2025
6b7fb0f
Merge branch '2025-03/classical-sim-1' into 2025-03/open-quimb-1
mpharrigan Apr 9, 2025
3d09d66
Signature.n_xbits
mpharrigan Apr 9, 2025
9e62a54
Merge branch '2025-03/classical-1' into 2025-03/classical-sim-1
mpharrigan Apr 9, 2025
615d42c
Merge branch '2025-03/classical-sim-1' into 2025-03/open-quimb-1
mpharrigan Apr 9, 2025
de0c07a
Merge remote-tracking branch 'origin/main' into 2025-03/classical-sim-1
mpharrigan Apr 9, 2025
51d9fb6
Merge branch '2025-03/classical-sim-1' into 2025-03/open-quimb-1
mpharrigan Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 59 additions & 11 deletions qualtran/_infra/bloq.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,19 @@
GeneralizerT,
SympySymbolAllocator,
)
from qualtran.simulation.classical_sim import ClassicalValT
from qualtran.simulation.classical_sim import ClassicalValRetT, ClassicalValT
from qualtran.simulation.tensor import DiscardInd


def _decompose_from_build_composite_bloq(bloq: 'Bloq') -> 'CompositeBloq':
from qualtran import BloqBuilder

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)


Expand Down Expand Up @@ -184,13 +189,13 @@ 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:
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.

Expand All @@ -215,6 +220,25 @@ 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]:
"""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.
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

def call_classically(
self, **vals: Union['sympy.Symbol', 'ClassicalValT']
) -> Tuple['ClassicalValT', ...]:
Expand All @@ -239,21 +263,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
Expand Down
2 changes: 1 addition & 1 deletion qualtran/_infra/composite_bloq.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,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))
Expand Down
16 changes: 14 additions & 2 deletions qualtran/_infra/controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down
8 changes: 3 additions & 5 deletions qualtran/_infra/controlled_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)


Expand Down
13 changes: 12 additions & 1 deletion qualtran/bloqs/basic_gates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
)
68 changes: 68 additions & 0 deletions qualtran/bloqs/basic_gates/discard.py
Original file line number Diff line number Diff line change
@@ -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))]
82 changes: 82 additions & 0 deletions qualtran/bloqs/basic_gates/discard_test.py
Original file line number Diff line number Diff line change
@@ -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
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)
Loading