diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e52febc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,44 @@ +language: python + +jobs: + include: + # Lint tests + - python: 3.10 + env: TOXENV=lint + script: tox -e $TOXENV + + # Coverage tests + - python: 3.10 + env: TOXENV=coverage + script: tox -e $TOXENV + + # Unit tests (py39) + - python: 3.9 + env: TOXENV=py39 + script: tox -e $TOXENV + + # Unit tests (py310) + - python: 3.10 + env: TOXENV=py310 + script: tox -e $TOXENV + + # Unit tests (py311) + - python: 3.11 + env: TOXENV=py311 + script: tox -e $TOXENV + + # Unit tests (py312) + - python: 3.12 + env: TOXENV=py312 + script: tox -e $TOXENV + + # Notebook tests + - python: 3.10 + before_install: + - sudo apt-get update + env: TOXENV=notebook + script: tox -e $TOXENV + +install: + - pip install --upgrade pip + - pip install tox diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..a8ca5d0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1 @@ +IBM Restricted diff --git a/README.md b/README.md index 4d6bf4c..7bcce46 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -# IC-POVM -Informationally complete POVM +# POVMs -This is a first try +A module for the implementation of positive operator-valued measures (POVMs). diff --git a/test.ipynb b/notebooks/test.ipynb similarity index 99% rename from test.ipynb rename to notebooks/test.ipynb index dca02e7..4fc308b 100644 --- a/test.ipynb +++ b/notebooks/test.ipynb @@ -14,8 +14,6 @@ "from base_povm import Povm\n", "from single_qubit_povm import SingleQubitPOVM\n", "from povm_implementation import ProductPVMSimPOVMImplementation\n", - "from test_base_povm import TestBasePovm\n", - "\n", "from qiskit.quantum_info import random_density_matrix, Operator" ] }, @@ -421,11 +419,11 @@ "sqpovm = SingleQubitPOVM.from_vectors()\n", "print(sqpovm.check_validity())\n", "\n", - "sum = np.zeros(4, dtype=complex)\n", + "summed = np.zeros(4, dtype=complex)\n", "for coef in sqpovm.povm_pauli_decomp:\n", - " sum += coef\n", + " summed += coef\n", "\n", - "sum" + "summed" ] }, { @@ -679,8 +677,6 @@ } ], "source": [ - "import numpy as np\n", - "from povm_implementation import ProductPVMSimPOVMImplementation\n", "povm_implementation = ProductPVMSimPOVMImplementation(2, np.array([0,0, 1,0.5, 1,1, 1,1, 0,0, 1,0.5, 0.3,0.2, 2,1]))\n", "povm_implementation._build_qc().draw()" ] @@ -1165,6 +1161,7 @@ " return povm\n", "\n", "def compare_povms(povm1: Povm, povm2: Povm, tol:float=1e-5) -> bool:\n", + " del tol\n", " povm1 = clean_povm(povm1)\n", " povm2 = clean_povm(povm2)\n", "\n", @@ -1379,7 +1376,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1393,9 +1390,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" + "version": "3.11.8" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/povm_implementation.py b/povm_implementation.py deleted file mode 100644 index 8b54514..0000000 --- a/povm_implementation.py +++ /dev/null @@ -1,96 +0,0 @@ -import numpy as np -from collections import Counter -from qiskit.circuit import QuantumCircuit, ParameterVector -from base_povm import Povm -from single_qubit_povm import SingleQubitPOVM -from product_povm import ProductPOVM - - -class POVMImplementation: - - def __init__( - self, - n_qubit: int, - ) -> None: - - self.n_qubit = n_qubit - self.parametrized_qc = self._build_qc() - - def _build_qc(self) -> QuantumCircuit: - raise NotImplementedError("The subclass of POVMImplementation must implement `_build_from_param` method.") - - def get_parameter_and_shot(self, shot: int) -> QuantumCircuit: - raise NotImplementedError("The subclass of POVMImplementation must implement `get_parameter_and_shot` method.") - - def to_povm(self) -> Povm: - raise NotImplementedError("The subclass of POVMImplementation must implement `to_povm` method.") - - -class ProductPVMSimPOVMImplementation(POVMImplementation): - - def __init__( - self, - n_qubit: int, - parameters: np.ndarray | None = None, - ) -> None: - - super().__init__(n_qubit) - self._set_parameters(parameters) - - def _set_parameters(self, parameters: np.ndarray) -> None: - # n_param = n_qubit*(3*self.n_PVM-1) - if len(parameters) % self.n_qubit != 0: - raise ValueError('The length of the parameter array is expected to be multiple of the number of qubits') - elif (len(parameters) / self.n_qubit + 1) % 3 != 0: - raise ValueError('The number of parameters per qubit is expected to be of the form 3*n_PVM-1') - else: - self.n_PVM = int((len(parameters) // self.n_qubit + 1) // 3) - parameters = parameters.reshape((self.n_qubit, self.n_PVM * 3 - 1)) - self.angles = parameters[:, :2 * self.n_PVM].reshape((self.n_qubit, self.n_PVM, 2)) - self.PVM_distributions = np.concatenate((parameters[:, 2 * self.n_PVM:], np.ones((self.n_qubit, 1))), axis=1) - if np.any(self.PVM_distributions < 0.): - raise ValueError('There should not be any negative values in the probability distribution parameters.') - else: - self.PVM_distributions = self.PVM_distributions / self.PVM_distributions.sum(axis=1)[:, np.newaxis] - - def _build_qc(self) -> QuantumCircuit: - - theta = ParameterVector('theta', length=self.n_qubit) - phi = ParameterVector('phi', length=self.n_qubit) - - qc = QuantumCircuit(self.n_qubit) - for i in range(self.n_qubit): - qc.u(theta=theta[i], phi=phi[i], lam=0, qubit=i) - - return qc - - def get_parameter_and_shot(self, shot: int) -> QuantumCircuit: - """ - Returns a list with concrete parameter values and associated number of shots. - """ - - PVM_idx = np.zeros((shot, self.n_qubit), dtype=int) - - for i in range(self.n_qubit): - PVM_idx[:, i] = np.random.choice(self.n_PVM, size=int(shot), replace=True, p=self.PVM_distributions[i]) - counts = Counter(tuple(x) for x in PVM_idx) - - return [tuple(([self.angles[i, combination[i]] for i in range(self.n_qubit)], counts[combination])) for combination in counts] - - def to_povm(self) -> Povm: - - stabilizers = np.zeros((self.n_qubit, self.n_PVM, 2, 2), dtype=complex) - - stabilizers[:, :, 0, 0] = np.cos(self.angles[:, :, 0] / 2.) - stabilizers[:, :, 0, 1] = (np.cos(self.angles[:, :, 1]) + 1.j * np.sin(self.angles[:, :, 1])) * np.sin(self.angles[:, :, 0] / 2.) - stabilizers[:, :, 1, 0] = stabilizers[:, :, 0, 1].conjugate() - stabilizers[:, :, 1, 1] = -stabilizers[:, :, 0, 0] - - stabilizers = np.multiply(stabilizers.T, np.sqrt(self.PVM_distributions).T).T - stabilizers = stabilizers.reshape((self.n_qubit, 2 * self.n_PVM, 2)) - - sq_povms = [] - for vecs in stabilizers: - sq_povms.append(SingleQubitPOVM.from_vectors(vecs)) - - return ProductPOVM(sq_povms) diff --git a/povms/__init__.py b/povms/__init__.py new file mode 100644 index 0000000..4578657 --- /dev/null +++ b/povms/__init__.py @@ -0,0 +1 @@ +"""TODO.""" diff --git a/base_povm.py b/povms/base_povm.py similarity index 68% rename from base_povm.py rename to povms/base_povm.py index 36a9feb..76cf0d8 100644 --- a/base_povm.py +++ b/povms/base_povm.py @@ -1,3 +1,7 @@ +"""TODO.""" + +from __future__ import annotations + import numpy as np from qiskit.quantum_info import Operator, DensityMatrix @@ -6,20 +10,21 @@ class Povm: """Abstract base class that collects all information that any POVM should specifiy.""" - def __init__(self, povm_ops: np.ndarray): + def __init__(self, povm_ops: np.ndarray) -> None: """Initialize from explicit POVM operators. Args: - povm_operators: np.ndarray that contains the explicit list of POVM operators""" + povm_operators: np.ndarray that contains the explicit list of POVM operators. + Raises: + ValueError: TODO. + """ if not (len(povm_ops.shape) == 3 and povm_ops.shape[1] == povm_ops.shape[1]): - raise ValueError( - f"POVM operators need to be square instead of {povm_ops.shape[1:]}" - ) + raise ValueError(f"POVM operators need to be square instead of {povm_ops.shape[1:]}") - self.n_outcomes = povm_ops.shape[0] - self.dimension = povm_ops.shape[1] - self.povm_operators = [Operator(op) for op in povm_ops] + self.n_outcomes: int = povm_ops.shape[0] + self.dimension: int = povm_ops.shape[1] + self.povm_operators: list[Operator] = [Operator(op) for op in povm_ops] self.array_ops = None self.dual_operators = None @@ -27,20 +32,27 @@ def __init__(self, povm_ops: np.ndarray): self.informationlly_complete = None def check_validity(self) -> bool: - """Checks if POVM axioms are fulfilled.""" + """Check if POVM axioms are fulfilled. - summed_op = np.zeros((self.dimension, self.dimension), dtype=complex) + Returns: + TODO. - for k in range(len(self)): + Raises: + ValueError: TODO. + """ + summed_op: np.ndarray = np.zeros((self.dimension, self.dimension), dtype=complex) - if not np.allclose(self.povm_operators[k].data.conj().T - self.povm_operators[k].data, 0.0, atol=1e-5): + for k in range(len(self)): + if not np.allclose( + self.povm_operators[k].data.conj().T - self.povm_operators[k].data, + 0.0, + atol=1e-5, + ): raise ValueError(f"POVM operator {k} is not hermitian.") for eigval in np.linalg.eigvalsh(self.povm_operators[k].data): if eigval.real < -1e-6 or np.abs(eigval.imag) > 1e-5: - raise ValueError( - f"Negative eigenvalue {eigval} in POVM operator {k}." - ) + raise ValueError(f"Negative eigenvalue {eigval} in POVM operator {k}.") summed_op += self.povm_operators[k].data @@ -49,22 +61,41 @@ def check_validity(self) -> bool: return True - def __getitem__(self, index: slice) -> np.ndarray: + def __getitem__(self, index: slice) -> Operator: """Return a povm operator or a list of povm operators.""" return self.povm_operators[index] - - def __len__(self): + + def __len__(self) -> int: + """TODO.""" return len(self.povm_operators) def get_prob(self, rho: DensityMatrix) -> np.ndarray: - return np.array([ - np.real(np.trace(rho.data @ povm_op.data)) - for povm_op in self.povm_operators]) + """TODO. + + Args: + rho: TODO. + + Returns: + TODO. + """ + return np.array( + [np.real(np.trace(rho.data @ povm_op.data)) for povm_op in self.povm_operators] + ) @classmethod - def from_vectors(cls, povm_vectors: np.ndarray): - """Initialize a POVM from the bloch vectors |psi> (not normalized!) such that Pi = |psi> Povm: + """Initialize a POVM from the bloch vectors |psi> (not normalized!) such that Pi = |psi> None: + """TODO. + + Args: + n_qubit: TODO. + """ + self.n_qubit = n_qubit + self.parametrized_qc = self._build_qc() + + def _build_qc(self) -> QuantumCircuit: + """TODO. + + Raises: + NotImplementedError: TODO. + """ + raise NotImplementedError( + "The subclass of POVMImplementation must implement `_build_from_param` method." + ) + + def get_parameter_and_shot(self, shot: int) -> QuantumCircuit: + """TODO. + + Args: + shot: TODO. + + Returns: + TODO. + + Raises: + NotImplementedError: TODO. + """ + raise NotImplementedError( + "The subclass of POVMImplementation must implement `get_parameter_and_shot` method." + ) + + def to_povm(self) -> Povm: + """TODO. + + Returns: + TODO. + + Raises: + NotImplementedError: TODO. + """ + raise NotImplementedError( + "The subclass of POVMImplementation must implement `to_povm` method." + ) + + +class ProductPVMSimPOVMImplementation(POVMImplementation): + """TODO.""" + + def __init__( + self, + n_qubit: int, + parameters: np.ndarray | None = None, + ) -> None: + """TODO. + + Args: + n_qubit: TODO. + parameters: TODO. + """ + super().__init__(n_qubit) + if parameters is not None: + self._set_parameters(parameters) + + def _set_parameters(self, parameters: np.ndarray) -> None: + """TODO. + + Args: + parameters: TODO. + + Raises: + ValueError: TODO. + """ + # n_param = n_qubit*(3*self.n_PVM-1) + if len(parameters) % self.n_qubit != 0: + raise ValueError( + "The length of the parameter array is expected to be multiple of the number of qubits" + ) + if (len(parameters) / self.n_qubit + 1) % 3 != 0: + raise ValueError( + "The number of parameters per qubit is expected to be of the form 3*n_PVM-1" + ) + + self.n_PVM = int((len(parameters) // self.n_qubit + 1) // 3) + parameters = parameters.reshape((self.n_qubit, self.n_PVM * 3 - 1)) + self.angles = parameters[:, : 2 * self.n_PVM].reshape((self.n_qubit, self.n_PVM, 2)) + self.PVM_distributions = np.concatenate( + (parameters[:, 2 * self.n_PVM :], np.ones((self.n_qubit, 1))), axis=1 + ) + if np.any(self.PVM_distributions < 0.0): + raise ValueError( + "There should not be any negative values in the probability distribution parameters." + ) + self.PVM_distributions = ( + self.PVM_distributions / self.PVM_distributions.sum(axis=1)[:, np.newaxis] + ) + + def _build_qc(self) -> QuantumCircuit: + """TODO. + + Returns: + TODO. + """ + theta = ParameterVector("theta", length=self.n_qubit) + phi = ParameterVector("phi", length=self.n_qubit) + + qc = QuantumCircuit(self.n_qubit) + for i in range(self.n_qubit): + qc.u(theta=theta[i], phi=phi[i], lam=0, qubit=i) + + return qc + + def get_parameter_and_shot(self, shot: int) -> QuantumCircuit: + """Return a list with concrete parameter values and associated number of shots. + + Args: + shot: TODO. + + Returns: + TODO. + """ + PVM_idx: np.ndarray = np.zeros((shot, self.n_qubit), dtype=int) + + for i in range(self.n_qubit): + PVM_idx[:, i] = np.random.choice( + self.n_PVM, size=int(shot), replace=True, p=self.PVM_distributions[i] + ) + counts = Counter(tuple(x) for x in PVM_idx) + + return [ + tuple( + ( + [self.angles[i, combination[i]] for i in range(self.n_qubit)], + counts[combination], + ) + ) + for combination in counts + ] + + def to_povm(self) -> Povm: + """TODO. + + Returns: + TODO. + """ + stabilizers: np.ndarray = np.zeros((self.n_qubit, self.n_PVM, 2, 2), dtype=complex) + + stabilizers[:, :, 0, 0] = np.cos(self.angles[:, :, 0] / 2.0) + stabilizers[:, :, 0, 1] = ( + np.cos(self.angles[:, :, 1]) + 1.0j * np.sin(self.angles[:, :, 1]) + ) * np.sin(self.angles[:, :, 0] / 2.0) + stabilizers[:, :, 1, 0] = stabilizers[:, :, 0, 1].conjugate() + stabilizers[:, :, 1, 1] = -stabilizers[:, :, 0, 0] + + stabilizers = np.multiply(stabilizers.T, np.sqrt(self.PVM_distributions).T).T + stabilizers = stabilizers.reshape((self.n_qubit, 2 * self.n_PVM, 2)) + + sq_povms = [] + for vecs in stabilizers: + sq_povms.append(SingleQubitPOVM.from_vectors(vecs)) + + return ProductPOVM(sq_povms) diff --git a/product_povm.py b/povms/product_povm.py similarity index 98% rename from product_povm.py rename to povms/product_povm.py index 83c6e00..38b4661 100644 --- a/product_povm.py +++ b/povms/product_povm.py @@ -1,3 +1,5 @@ +"""TODO.""" + from typing import List # from qiskit.quantum_info import DensityMatrix, SparsePauliOp @@ -20,6 +22,7 @@ def __init__(self, povm_list: List[Povm]): self.povm_list = povm_list + # def get_prob(self, rho: DensityMatrix): # return get_p_from_paulis(SparsePauliOp.from_operator(rho), self.list_povm).ravel() diff --git a/single_qubit_povm.py b/povms/single_qubit_povm.py similarity index 66% rename from single_qubit_povm.py rename to povms/single_qubit_povm.py index bddd679..ca30a8e 100644 --- a/single_qubit_povm.py +++ b/povms/single_qubit_povm.py @@ -1,8 +1,10 @@ +"""TODO.""" + import numpy as np from qiskit.quantum_info import SparsePauliOp -from base_povm import Povm +from .base_povm import Povm class SingleQubitPOVM(Povm): @@ -10,7 +12,6 @@ class SingleQubitPOVM(Povm): def __init__(self, povm_ops: np.ndarray): """Initialize from explicit POVM operators.""" - super().__init__(povm_ops) # self.check_validity() @@ -21,15 +22,35 @@ def __init__(self, povm_ops: np.ndarray): ] def check_validity(self): + """TODO. + + Returns: + TODO. + + Raises: + ValueError: TODO. + """ if not self.dimension == 2: - raise ValueError(f"Dimension of Single Qubit POVM operator space should be 2, not {self.dimension}.") + raise ValueError( + f"Dimension of Single Qubit POVM operator space should be 2, not {self.dimension}." + ) return Povm.check_validity(self) @staticmethod def pauli_op_to_array(pauli_op: SparsePauliOp): - """Convert a single-qubit SparsePauliOp into an array [c0, c1, c2, c3], - where the indices represent paulis as {"I": 0, "X": 1, "Y": 2, "Z": 3}.""" + """Convert a single-qubit SparsePauliOp into an array ``[c0, c1, c2, c3]``. + + In the returned array the indices represent paulis as ``{"I": 0, "X": 1, "Y": 2, "Z": 3}``. + + Args: + pauli_op: TODO. + + Returns: + TODO. + Raises: + ValueError: TODO. + """ labels = np.zeros(4) + 0j for pauli_idx, coeff in pauli_op.label_iter(): diff --git a/utilities.py b/povms/utilities.py similarity index 55% rename from utilities.py rename to povms/utilities.py index adc3949..52d3d56 100644 --- a/utilities.py +++ b/povms/utilities.py @@ -1,22 +1,36 @@ +"""TODO.""" + import numpy as np # Gram-Schmidt def gs(X: np.ndarray) -> np.ndarray: - """Returns the orthonormal basis resulting from Gram-Schmidt process of X""" + """Return the orthonormal basis resulting from Gram-Schmidt process of X. + + Args: + X: TODO. + Returns: + TODO. + """ Q, _ = np.linalg.qr(X) return Q def n_sphere(param: np.ndarray) -> np.ndarray: - """Returns a unit vector on the n-sphere""" + """Return a unit vector on the n-sphere. + + Args: + param: TODO. + Returns: + TODO. + """ n = len(param) x = np.ones(n + 1) for i in range(n - 1): x[i] *= np.cos(np.pi * param[i]) - x[i + 1:] *= np.sin(np.pi * param[i]) + x[i + 1 :] *= np.sin(np.pi * param[i]) x[-2] *= np.cos(2 * np.pi * param[-1]) x[-1] *= np.sin(2 * np.pi * param[-1]) @@ -24,4 +38,5 @@ def n_sphere(param: np.ndarray) -> np.ndarray: def povms_union(): + """TODO.""" return None diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f38f9c5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,84 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "povms" +version = "0.0.0" +readme = "README.md" +license = {file = "LICENSE.txt"} + +requires-python = ">=3.9" + +dependencies = [ + "numpy>=1.23", + "qiskit>=1.0", +] + +[project.optional-dependencies] +dev = [ + "povms[test,nbtest,lint]", +] +basetest = [ + "pytest>=8.0", +] +test = [ + "povms[basetest]", +] +nbtest = [ + "povms[basetest]", + "nbmake>=1.5.0", +] +style = [ + "autoflake==2.2.1", + "ruff>=0.3.0", + "nbqa>=1.7.1", +] +lint = [ + "povms[style]", + "pydocstyle==6.3.0", + "mypy==1.8.0", + "pylint==3.0.3", + "toml", +] +notebook-dependencies = [ + "povms", +] + +[tool.autoflake] +remove-unused-variables = true +remove-all-unused-imports = true + +[tool.hatch.build.targets.wheel] +only-include = [ + "povms", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.mypy] +python_version = 3.8 +show_error_codes = true +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +filterwarnings = ["ignore:::.*qiskit.opflow*"] +testpaths = ["./povms/", "./test/"] + +[tool.ruff] +line-length = 100 +src = ["povms", "test"] +target-version = "py39" + +[tool.ruff.lint] +ignore = [ + "E501", +] + +[tool.ruff.lint.extend-per-file-ignores] +"notebooks/*" = [ + "E402", # module level import not at top of file +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dbe64a4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -python -numpy -qiskit>=1.0 \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_base_povm.py b/test/test_base_povm.py index 0c79082..1c8c729 100644 --- a/test/test_base_povm.py +++ b/test/test_base_povm.py @@ -10,12 +10,12 @@ class TestBasePovm(TestCase): """Test that we can create valid POVM and get warnings if invalid.""" def test_random_operators(self): - """Test """ + """Test""" - ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.j * np.random.uniform(-1, 1, (6, 2, 2)) + ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.0j * np.random.uniform(-1, 1, (6, 2, 2)) while np.abs(ops[0, 0, 0].imag) < 1e-6: - ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.j * np.random.uniform(-1, 1, (6, 2, 2)) + ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.0j * np.random.uniform(-1, 1, (6, 2, 2)) povm1 = Povm(povm_ops=ops) diff --git a/test/test_implementation.py b/test/test_implementation.py index 56430dd..77903d1 100644 --- a/test/test_implementation.py +++ b/test/test_implementation.py @@ -6,17 +6,17 @@ from povm_implementation import ProductPVMSimPOVMImplementation from single_qubit_povm import SingleQubitPOVM -class TestProductPVMSimPOVMImplementation(TestCase): +class TestProductPVMSimPOVMImplementation(TestCase): def __init__(self, methodName: str = "runTest") -> None: super().__init__(methodName) - basis_0 = np.array([1.,0], dtype=complex) - basis_1 = np.array([0,1.], dtype=complex) - basis_plus = 1.0/np.sqrt(2) * (basis_0 + basis_1) - basis_minus = 1.0/np.sqrt(2) * (basis_0 - basis_1) - basis_plus_i = 1.0/np.sqrt(2) * (basis_0 + 1.j * basis_1) - basis_minus_i = 1.0/np.sqrt(2) * (basis_0 - 1.j * basis_1) + basis_0 = np.array([1.0, 0], dtype=complex) + basis_1 = np.array([0, 1.0], dtype=complex) + basis_plus = 1.0 / np.sqrt(2) * (basis_0 + basis_1) + basis_minus = 1.0 / np.sqrt(2) * (basis_0 - basis_1) + basis_plus_i = 1.0 / np.sqrt(2) * (basis_0 + 1.0j * basis_1) + basis_minus_i = 1.0 / np.sqrt(2) * (basis_0 - 1.0j * basis_1) self.Z0 = np.outer(basis_0, basis_0.conj()) self.Z1 = np.outer(basis_1, basis_1.conj()) @@ -25,16 +25,30 @@ def __init__(self, methodName: str = "runTest") -> None: self.Y0 = np.outer(basis_plus_i, basis_plus_i.conj()) self.Y1 = np.outer(basis_minus_i, basis_minus_i.conj()) - def test_CS_build(self): """Test if we can build a standard Classical Shadow POVM from the generic class""" - q = [1./3., 1./3., 1./3.] - sqpovm = SingleQubitPOVM(np.array([q[0]* self.Z0,q[0]* self.Z1,q[1]* self.X0,q[1]* self.X1,q[2]* self.Y0,q[2]* self.Y1])) + q = [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0] + sqpovm = SingleQubitPOVM( + np.array( + [ + q[0] * self.Z0, + q[0] * self.Z1, + q[1] * self.X0, + q[1] * self.X1, + q[2] * self.Y0, + q[2] * self.Y1, + ] + ) + ) - for n_qubit in range(1,11): - parameters = np.array(n_qubit * [0., 0., 0.5 * np.pi, 0., 0.5 * np.pi, 0.5 * np.pi, 1, 1]) - cs_implementation = ProductPVMSimPOVMImplementation(n_qubit=n_qubit, parameters=parameters) + for n_qubit in range(1, 11): + parameters = np.array( + n_qubit * [0.0, 0.0, 0.5 * np.pi, 0.0, 0.5 * np.pi, 0.5 * np.pi, 1, 1] + ) + cs_implementation = ProductPVMSimPOVMImplementation( + n_qubit=n_qubit, parameters=parameters + ) self.assertEqual(n_qubit, cs_implementation.n_qubit) cs_povm = cs_implementation.to_povm() for i in range(n_qubit): @@ -45,21 +59,35 @@ def test_CS_build(self): def test_LBCS_build(self): """Test if we can build a LB Classical Shadow POVM from the generic class""" - for n_qubit in range(1,11): - q = np.random.uniform(0,5,size=3 * n_qubit).reshape((n_qubit, 3)) + for n_qubit in range(1, 11): + q = np.random.uniform(0, 5, size=3 * n_qubit).reshape((n_qubit, 3)) q /= q.sum(axis=1)[:, np.newaxis] - parameters = np.array(n_qubit * [0., 0., 0.5 * np.pi, 0., 0.5 * np.pi, 0.5 * np.pi, 1, 1]) + parameters = np.array( + n_qubit * [0.0, 0.0, 0.5 * np.pi, 0.0, 0.5 * np.pi, 0.5 * np.pi, 1, 1] + ) for i in range(n_qubit): - parameters[i * 8 + 6] = q[i,0]/q[i,2] - parameters[i * 8 + 7] = q[i,1]/q[i,2] + parameters[i * 8 + 6] = q[i, 0] / q[i, 2] + parameters[i * 8 + 7] = q[i, 1] / q[i, 2] - cs_implementation = ProductPVMSimPOVMImplementation(n_qubit=n_qubit, parameters=parameters) + cs_implementation = ProductPVMSimPOVMImplementation( + n_qubit=n_qubit, parameters=parameters + ) self.assertEqual(n_qubit, cs_implementation.n_qubit) cs_povm = cs_implementation.to_povm() for i in range(n_qubit): - sqpovm = SingleQubitPOVM(np.array([q[i,0]* self.Z0,q[i,0]* self.Z1,q[i,1]* self.X0,q[i,1]* self.X1,q[i,2]* self.Y0,q[i,2]* self.Y1])) + sqpovm = SingleQubitPOVM( + np.array( + [ + q[i, 0] * self.Z0, + q[i, 0] * self.Z1, + q[i, 1] * self.X0, + q[i, 1] * self.X1, + q[i, 2] * self.Y0, + q[i, 2] * self.Y1, + ] + ) + ) self.assertEqual(cs_povm.povm_list[i].n_outcomes, sqpovm.n_outcomes) for k in range(sqpovm.n_outcomes): self.assertTrue(np.allclose(cs_povm.povm_list[i][k], sqpovm[k])) - diff --git a/test/test_sq_povm.py b/test/test_sq_povm.py index 4aecef3..98e2625 100644 --- a/test/test_sq_povm.py +++ b/test/test_sq_povm.py @@ -12,19 +12,19 @@ class TestSingleQubitPovm(TestCase): """Test that we can create valid single qubit POVM and get warnings if invalid.""" def test_random_operators(self): - """Test """ + """Test""" - ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.j * np.random.uniform(-1, 1, (6, 2, 2)) + ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.0j * np.random.uniform(-1, 1, (6, 2, 2)) while np.abs(ops[0, 0, 0].imag) < 1e-6: - ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.j * np.random.uniform(-1, 1, (6, 2, 2)) + ops = np.random.uniform(-1, 1, (6, 2, 2)) + 1.0j * np.random.uniform(-1, 1, (6, 2, 2)) with self.assertRaises(ValueError): povm1 = SingleQubitPOVM(povm_ops=ops) povm1.check_validity() def test_pauli_decomposition(self): - """Test """ + """Test""" # TODO : select a random POVM ... dim = 2 @@ -36,10 +36,10 @@ def test_pauli_decomposition(self): sqpovm = SingleQubitPOVM.from_vectors(V) - sum = np.zeros(4, dtype=complex) + summed = np.zeros(4, dtype=complex) for coef in sqpovm.povm_pauli_decomp: - sum += coef + summed += coef - self.assertTrue(np.allclose(sum, np.array([1., 0., 0., 0.], dtype=complex))) + self.assertTrue(np.allclose(summed, np.array([1.0, 0.0, 0.0, 0.0], dtype=complex))) # also check that the decomposition is correct TODO diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e677a12 --- /dev/null +++ b/tox.ini @@ -0,0 +1,49 @@ +[tox] +minversion = 4.0 +envlist = py{39,310,311,312}{,-notebook}, lint, coverage, docs +isolated_build = True + +[testenv] +extras = + test +commands = + pytest {posargs} + +[testenv:style] +extras = + style +commands = + ruff check --fix povms/ notebooks/ test/ + nbqa ruff --fix notebooks/ + autoflake --in-place --recursive povms/ notebooks/ test/ + black povms/ notebooks/ test/ + +[testenv:lint] +basepython = python3.10 +extras = + lint +commands = + ruff check povms/ notebooks/ test/ + nbqa ruff notebooks/ + autoflake --check --quiet --recursive povms/ notebooks/ test/ + pydocstyle povms/ + mypy povms/ + pylint -rn --py-version=3.9 --disable=all --enable=reimported,no-self-use,no-else-raise,redefined-argument-from-local,redefined-builtin,raise-missing-from,cyclic-import,unused-argument povms/ test/ + nbqa pylint -rn --py-version=3.9 --disable=all --enable=reimported,no-self-use,no-else-raise,redefined-argument-from-local,redefined-builtin,raise-missing-from,cyclic-import,unused-argument notebooks/ + +[testenv:{,py-,py3-,py39-,py310-,py311-,py312-}notebook] +extras = + nbtest + notebook-dependencies +commands = + pytest --nbmake --nbmake-timeout=3000 {posargs} notebooks/ + +[testenv:coverage] +basepython = python3.10 +deps = + coverage>=7.4.1 +extras = + test +commands = + coverage3 run --source=povms/ -m unittest discover test/ + coverage3 report --fail-under=100 --show-missing