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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion graphix/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,11 @@ def __hash__(self) -> int:

ExpressionOrSupportsFloat = Expression | SupportsFloat

ExpressionOrSupportsComplex = Expression | SupportsComplex
# `ExpressionOrSupportsComplex` is based on
# `ExpressionOrSupportsFloat` rather than `Expression`, because `int`
# and `float` implement `SupportsFloat` but not `SupportsComplex`,
# even though `complex(int)` and `complex(float)` are valid.
ExpressionOrSupportsComplex = ExpressionOrSupportsFloat | SupportsComplex


T = TypeVar("T")
Expand Down
5 changes: 3 additions & 2 deletions graphix/sim/density_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import math
from collections.abc import Collection, Iterable
from dataclasses import dataclass
from typing import TYPE_CHECKING, SupportsComplex
from typing import TYPE_CHECKING

import numpy as np
from typing_extensions import override
Expand All @@ -25,6 +25,7 @@

if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
from typing import SupportsComplex, SupportsFloat

from graphix.noise_models.noise_model import Noise
from graphix.parameter import ExpressionOrSupportsFloat, Parameter
Expand Down Expand Up @@ -82,7 +83,7 @@ def check_size_consistency(mat: Matrix) -> None:
if len(input_list) != 0 and isinstance(input_list[0], Iterable):

def get_row(
item: Iterable[ExpressionOrSupportsComplex] | State | Expression | SupportsComplex,
item: Iterable[ExpressionOrSupportsComplex] | State | Expression | SupportsFloat | SupportsComplex,
) -> list[ExpressionOrSupportsComplex]:
if isinstance(item, Iterable):
return list(item)
Expand Down
15 changes: 0 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,6 @@ exclude = [
'^examples/qnn\.py$',
'^examples/rotation\.py$',
'^examples/tn_simulation\.py$',
'^graphix/linalg\.py$',
'^tests/test_density_matrix\.py$',
'^tests/test_linalg\.py$',
'^tests/test_noisy_density_matrix\.py$',
'^tests/test_statevec\.py$',
'^tests/test_statevec_backend\.py$',
'^tests/test_tnsim\.py$',
]
follow_imports = "silent"
follow_untyped_imports = true # required for qiskit, requires mypy >=1.14
Expand All @@ -167,14 +160,6 @@ exclude = [
"examples/qnn.py",
"examples/rotation.py",
"examples/tn_simulation.py",
"graphix/linalg.py",
"tests/test_density_matrix.py",
"tests/test_linalg.py",
"tests/test_noisy_density_matrix.py",
"tests/test_statevec.py",
"tests/test_statevec_backend.py",
"tests/test_tnsim.py",
"tests/test_transpiler.py",
]
reportUnknownArgumentType = "information"
reportUnknownLambdaType = "information"
Expand Down
8 changes: 8 additions & 0 deletions stubs/quimb/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from collections.abc import Iterator
from typing import Generic, TypeVar

_T = TypeVar("_T")

class oset(Generic[_T]): # noqa: N801
def __iter__(self) -> Iterator[_T]: ...
def popleft(self) -> _T: ...
6 changes: 4 additions & 2 deletions stubs/quimb/tensor/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ from typing import Literal
import numpy as np
import numpy.typing as npt
from cotengra.oe import PathOptimizer
from quimb import oset
from typing_extensions import Self

class Tensor:
Expand All @@ -30,6 +31,7 @@ class Tensor:

class TensorNetwork:
tensor_map: dict[str, Tensor]
tag_map: dict[str, str]

def __init__(
self,
Expand All @@ -40,10 +42,10 @@ class TensorNetwork:
) -> None: ...
def _get_tids_from_tags(
self, tags: Sequence[str] | str | int | None, which: Literal["all", "any", "!all", "!any"] = "all"
) -> set[str]: ...
) -> oset[str]: ...
def _get_tids_from_inds(
self, inds: Sequence[str] | str | int | None, which: Literal["all", "any", "!all", "!any"] = "all"
) -> set[str]: ...
) -> oset[str]: ...
def __iter__(self) -> Iterator[Tensor]: ...
def add(
self,
Expand Down
100 changes: 44 additions & 56 deletions tests/test_density_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pytest

import graphix.random_objects as randobj
from graphix import command
from graphix.branch_selector import RandomBranchSelector
from graphix.channels import KrausChannel, dephasing_channel, depolarising_channel
from graphix.fundamentals import ANGLE_PI, Plane
Expand Down Expand Up @@ -44,16 +45,9 @@ def test_init_without_data_fail(self) -> None:
DensityMatrix(nqubit=-2)

def test_init_with_invalid_data_fail(self, fx_rng: Generator) -> None:
with pytest.raises(ValueError):
DensityMatrix("hello")
with pytest.raises(TypeError):
DensityMatrix(1)
# Length is not a power of two
with pytest.raises(ValueError):
DensityMatrix([1, 2, [3]])
# setting an array element with a sequence (numpy error)
with pytest.raises(ValueError):
DensityMatrix([1, 2, [3], 4])
DensityMatrix([1, 2, 3])

# check with hermitian dm but not unit trace
with pytest.raises(ValueError):
Expand Down Expand Up @@ -111,7 +105,9 @@ def test_init_with_state_sucess(self, fx_rng: Generator) -> None:
states = [PlanarState(plane=i, angle=j) for i, j in zip(rand_planes, rand_angles, strict=True)]
vec = Statevec(data=states)
# flattens input!
expected_dm = np.outer(vec.psi, vec.psi.conj())
expected_dm = np.outer(
vec.psi.astype(np.complex128, copy=False), vec.psi.conj().astype(np.complex128, copy=False)
)

# input with a State object
dm = DensityMatrix(data=states)
Expand Down Expand Up @@ -140,7 +136,9 @@ def test_init_with_statevec_sucess(self, fx_rng: Generator) -> None:
states = [PlanarState(plane=i, angle=j) for i, j in zip(rand_planes, rand_angles, strict=True)]
vec = Statevec(data=states)
# flattens input!
expected_dm = np.outer(vec.psi, vec.psi.conj())
expected_dm = np.outer(
vec.psi.astype(np.complex128, copy=False), vec.psi.conj().astype(np.complex128, copy=False)
)

# input with a Statevec object
dm = DensityMatrix(data=vec)
Expand All @@ -167,7 +165,9 @@ def test_init_with_densitymatrix_sucess(self, fx_rng: Generator) -> None:
print("planes", rand_planes)
states = [PlanarState(plane=i, angle=j) for i, j in zip(rand_planes, rand_angles, strict=True)]
vec = Statevec(data=states)
expected_dm = np.outer(vec.psi, vec.psi.conj())
expected_dm = np.outer(
vec.psi.astype(np.complex128, copy=False), vec.psi.conj().astype(np.complex128, copy=False)
)

# input with a huge density matrix
dm_list = [state.get_densitymatrix() for state in states]
Expand Down Expand Up @@ -202,7 +202,9 @@ def test_evolve_single_success(self, fx_rng: Generator) -> None:
for i in range(n):
sv = Statevec(nqubit=n)
sv.evolve_single(op, i)
expected_density_matrix = np.outer(sv.psi, sv.psi.conj())
expected_density_matrix = np.outer(
sv.psi.astype(np.complex128, copy=False), sv.psi.conj().astype(np.complex128, copy=False)
)
dm = DensityMatrix(nqubit=n)
dm.evolve_single(op, i)
assert np.allclose(dm.rho, expected_density_matrix)
Expand Down Expand Up @@ -250,13 +252,6 @@ def test_expectation_single_success(self, fx_rng: Generator) -> None:
# watch out ordering. Expval unitary is cpx so psi1 on the right to match DM.
assert np.allclose(np.dot(psi.conjugate(), psi1), dm.expectation_single(op, target_qubit))

def test_tensor_fail(self) -> None:
dm = DensityMatrix(nqubit=1)
with pytest.raises(ValueError):
dm.tensor("hello")
with pytest.raises(TypeError):
dm.tensor(1)

@pytest.mark.parametrize("n", range(3))
def test_tensor_without_data_success(self, n: int) -> None:
dm_a = DensityMatrix(nqubit=n)
Expand Down Expand Up @@ -502,15 +497,15 @@ def test_evolve_fail(self, fx_rng: Generator) -> None:

# check not square matrix
with pytest.raises(ValueError):
dm.evolve(fx_rng.uniform(size=(2, 3)), (0, 1))
dm.evolve(fx_rng.uniform(size=(2, 3)).astype(np.complex128), (0, 1))

# check higher dimensional matrix
with pytest.raises(ValueError):
dm.evolve(fx_rng.uniform(size=(2, 2, 3)), (0, 1))
dm.evolve(fx_rng.uniform(size=(2, 2, 3)).astype(np.complex128), (0, 1))

# check square but with incorrect dimension (non-qubit type)
with pytest.raises(ValueError):
dm.evolve(fx_rng.uniform(size=(5, 5)), (0, 1))
dm.evolve(fx_rng.uniform(size=(5, 5)).astype(np.complex128), (0, 1))

# TODO: the test for normalization is done at initialization with data.
# Now check that all operations conserve the norm.
Expand Down Expand Up @@ -920,51 +915,44 @@ def test_entangle_nodes(self) -> None:

@pytest.mark.parametrize("pr_calc", [False, True])
def test_measure(self, pr_calc: bool) -> None:
circ = Circuit(1)
circ.rx(0, ANGLE_PI / 2)
pattern = circ.transpile().pattern

measure_method = DefaultMeasureMethod()
backend = DensityMatrixBackend(branch_selector=RandomBranchSelector(pr_calc=pr_calc))
backend.add_nodes(pattern.input_nodes)
backend.add_nodes([0])
backend.add_nodes([1, 2])
backend.entangle_nodes((0, 1))
backend.entangle_nodes((1, 2))
measure_method.measure(backend, pattern[-4])
measure_method.measure(backend, command.M(0))

expected_matrix_1 = np.kron(np.array([[1, 0], [0, 0]]), np.ones((2, 2)) / 2)
expected_matrix_2 = np.kron(np.array([[0, 0], [0, 1]]), np.array([[0.5, -0.5], [-0.5, 0.5]]))
assert np.allclose(backend.state.rho, expected_matrix_1) or np.allclose(backend.state.rho, expected_matrix_2)

def test_correct_byproduct(self) -> None:
circ = Circuit(1)
circ.rx(0, ANGLE_PI / 2)
pattern = circ.transpile().pattern
measure_method = DefaultMeasureMethod()
backend = DensityMatrixBackend()
backend.add_nodes(pattern.input_nodes)
dm_backend = DensityMatrixBackend()
dm_backend.add_nodes([0])
# node 0 initialized in Backend
backend.add_nodes([1, 2])
backend.entangle_nodes((0, 1))
backend.entangle_nodes((1, 2))
measure_method.measure(backend, pattern[-4])
measure_method.measure(backend, pattern[-3])
backend.correct_byproduct(pattern[-2], measure_method)
backend.correct_byproduct(pattern[-1], measure_method)
backend.finalize(pattern.output_nodes)
rho = backend.state.rho

backend = StatevectorBackend()
backend.add_nodes(pattern.input_nodes)
dm_backend.add_nodes([1, 2])
dm_backend.entangle_nodes((0, 1))
dm_backend.entangle_nodes((1, 2))
measure_method.measure(dm_backend, command.M(0))
measure_method.measure(dm_backend, command.M(1, angle=-ANGLE_PI / 2, s_domain={0}))
dm_backend.correct_byproduct(command.X(2, {1}), measure_method)
dm_backend.correct_byproduct(command.Z(2, {0}), measure_method)
rho = dm_backend.state.rho

sv_backend = StatevectorBackend()
sv_backend.add_nodes([0])
# node 0 initialized in Backend
backend.add_nodes([1, 2])
backend.entangle_nodes((0, 1))
backend.entangle_nodes((1, 2))
measure_method.measure(backend, pattern[-4])
measure_method.measure(backend, pattern[-3])
backend.correct_byproduct(pattern[-2], measure_method)
backend.correct_byproduct(pattern[-1], measure_method)
backend.finalize(pattern.output_nodes)
psi = backend.state.psi

assert np.allclose(rho, np.outer(psi, psi.conj()))
sv_backend.add_nodes([1, 2])
sv_backend.entangle_nodes((0, 1))
sv_backend.entangle_nodes((1, 2))
measure_method.measure(sv_backend, command.M(0))
measure_method.measure(sv_backend, command.M(1, angle=-ANGLE_PI / 2, s_domain={0}))
sv_backend.correct_byproduct(command.X(2, {1}), measure_method)
sv_backend.correct_byproduct(command.Z(2, {0}), measure_method)
psi = sv_backend.state.psi

assert np.allclose(
rho, np.outer(psi.astype(np.complex128, copy=False), psi.conj().astype(np.complex128, copy=False))
)
14 changes: 7 additions & 7 deletions tests/test_statevec.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ def test_basicstates_success(self) -> None:

# even more tests?
def test_default_tensor_success(self, fx_rng: Generator) -> None:
nqb = fx_rng.integers(2, 5)
nqb = int(fx_rng.integers(2, 5))
print(f"nqb is {nqb}")
vec = Statevec(nqubit=nqb)
assert np.allclose(vec.psi, np.ones((2,) * nqb) / (np.sqrt(2)) ** nqb)
assert len(vec.dims()) == nqb

vec = Statevec(nqubit=nqb, data=BasicStates.MINUS_I)
sv_list = [BasicStates.MINUS_I.get_statevector() for _ in range(nqb)]
sv = functools.reduce(np.kron, sv_list)
sv = functools.reduce(lambda a, b: np.kron(a, b).astype(np.complex128, copy=False), sv_list)
assert np.allclose(vec.psi, sv.reshape((2,) * nqb))
assert len(vec.dims()) == nqb

Expand All @@ -69,7 +69,7 @@ def test_default_tensor_success(self, fx_rng: Generator) -> None:
state = PlanarState(rand_plane, rand_angle)
vec = Statevec(nqubit=nqb, data=state)
sv_list = [state.get_statevector() for _ in range(nqb)]
sv = functools.reduce(np.kron, sv_list)
sv = functools.reduce(lambda a, b: np.kron(a, b).astype(np.complex128, copy=False), sv_list)
assert np.allclose(vec.psi, sv.reshape((2,) * nqb))
assert len(vec.dims()) == nqb

Expand All @@ -79,7 +79,7 @@ def test_default_tensor_success(self, fx_rng: Generator) -> None:
states = [PlanarState(plane=i, angle=j) for i, j in zip(rand_planes, rand_angles, strict=True)]
vec = Statevec(nqubit=nqb, data=states)
sv_list = [state.get_statevector() for state in states]
sv = functools.reduce(np.kron, sv_list)
sv = functools.reduce(lambda a, b: np.kron(a, b).astype(np.complex128, copy=False), sv_list)
assert np.allclose(vec.psi, sv.reshape((2,) * nqb))
assert len(vec.dims()) == nqb

Expand Down Expand Up @@ -135,8 +135,8 @@ def test_copy_success(self, fx_rng: Generator) -> None:

# try calling with incorrect number of qubits compared to inferred one
def test_copy_fail(self, fx_rng: Generator) -> None:
nqb = fx_rng.integers(2, 5)
length = 2**nqb
nqb = int(fx_rng.integers(2, 5))
length = 1 << nqb
rand_vec = fx_rng.random(length) + 1j * fx_rng.random(length)
rand_vec /= np.sqrt(np.sum(np.abs(rand_vec) ** 2))
test_vec = Statevec(data=rand_vec)
Expand All @@ -148,4 +148,4 @@ def test_copy_fail(self, fx_rng: Generator) -> None:
def test_normalize() -> None:
statevec = Statevec(nqubit=1, data=BasicStates.PLUS)
statevec.remove_qubit(0)
assert _get_statevec_norm_numeric(statevec) == 1
assert _get_statevec_norm_numeric(statevec.psi.astype(np.complex128, copy=False)) == 1
Loading