From 9c02377e12bec3b6e26353e3a9aeff32ea2fa629 Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:43:14 -0400 Subject: [PATCH 01/11] Minor fixes on c++ side --- fast_pauli/cpp/include/__pauli_string.hpp | 13 +++++++++---- fast_pauli/cpp/src/fast_pauli.cpp | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/fast_pauli/cpp/include/__pauli_string.hpp b/fast_pauli/cpp/include/__pauli_string.hpp index 0dbc8f1..7f8588e 100644 --- a/fast_pauli/cpp/include/__pauli_string.hpp +++ b/fast_pauli/cpp/include/__pauli_string.hpp @@ -106,7 +106,9 @@ struct PauliString { * * @return size_t */ - size_t dims() const noexcept { return 1UL << paulis.size(); } + size_t dims() const noexcept { + return paulis.size() ? 1UL << paulis.size() : 0; + } /** * @brief Get the sparse representation of the pauli string matrix. @@ -139,7 +141,10 @@ struct PauliString { size_t const nY = std::count_if(ps.begin(), ps.end(), [](fast_pauli::Pauli const &p) { return p.code == 2; }); - size_t const dim = 1 << n; + size_t const dim = n ? 1 << n : 0; + + if (dim == 0) + return; // Safe, but expensive, we overwrite the vectors k = std::vector(dim); @@ -155,7 +160,7 @@ struct PauliString { } }; // Helper function that resolves first value of pauli string - auto inital_value = [&nY]() -> std::complex { + auto initial_value = [&nY]() -> std::complex { switch (nY % 4) { case 0: return 1.0; @@ -174,7 +179,7 @@ struct PauliString { for (size_t i = 0; i < ps.size(); ++i) { k[0] += (1UL << i) * diag(ps[i]); } - m[0] = inital_value(); + m[0] = initial_value(); // Populate the rest of the values in a recursive-like manner for (size_t l = 0; l < n; ++l) { diff --git a/fast_pauli/cpp/src/fast_pauli.cpp b/fast_pauli/cpp/src/fast_pauli.cpp index bf79fd7..446b3c0 100644 --- a/fast_pauli/cpp/src/fast_pauli.cpp +++ b/fast_pauli/cpp/src/fast_pauli.cpp @@ -75,7 +75,7 @@ PYBIND11_MODULE(_fast_pauli, m) { } return results; }, - "states"_a, "coef"_a) + "states"_a, "coef"_a = std::complex{1.0}) .def("__str__", [](fp::PauliString const &self) { return fmt::format("{}", self); }); From 3a535a941270856f88a3bbb7ba39a936d0719a1b Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:50:20 -0400 Subject: [PATCH 02/11] Bunch of tests for bindings in python land --- tests/bindings/__init__.py | 1 + tests/bindings/test_pauli.py | 81 +++++++++++++++++++++++++++++ tests/bindings/test_pauli_string.py | 44 ++++++++++++++++ tests/conftest.py | 13 +++++ tests/equivalence/__init__.py | 1 + 5 files changed, 140 insertions(+) create mode 100644 tests/bindings/__init__.py create mode 100644 tests/bindings/test_pauli.py create mode 100644 tests/bindings/test_pauli_string.py create mode 100644 tests/conftest.py create mode 100644 tests/equivalence/__init__.py diff --git a/tests/bindings/__init__.py b/tests/bindings/__init__.py new file mode 100644 index 0000000..007b624 --- /dev/null +++ b/tests/bindings/__init__.py @@ -0,0 +1 @@ +"""Test module for testing C++ library against python implementation.""" diff --git a/tests/bindings/test_pauli.py b/tests/bindings/test_pauli.py new file mode 100644 index 0000000..42dffb2 --- /dev/null +++ b/tests/bindings/test_pauli.py @@ -0,0 +1,81 @@ +"""Test pauli objects from c++ against python implementations.""" + +import itertools as it + +import numpy as np +import pytest + +import fast_pauli._fast_pauli as fp +import fast_pauli.pypauli.operations as pp + + +def test_pauli_wrapper(paulis: dict) -> None: + """Test pauli wrapper in python land.""" + np.testing.assert_array_equal( + np.array(fp.Pauli().to_tensor()), + paulis["I"], + ) + + for i in [0, 1, 2, 3]: + pcpp = fp.Pauli(code=i) + np.testing.assert_array_equal( + np.array(pcpp.to_tensor()), + paulis[i], + ) + + for c in ["I", "X", "Y", "Z"]: + pcpp = fp.Pauli(symbol=c) + np.testing.assert_allclose( + np.array(pcpp.to_tensor()), + paulis[c], + atol=1e-15, + ) + np.testing.assert_allclose( + np.array(pcpp.to_tensor()), + pp.PauliString(c).dense(), + atol=1e-15, + ) + np.testing.assert_string_equal(str(pcpp), c) + np.testing.assert_string_equal(str(pcpp), str(pp.PauliString(c))) + + +def test_pauli_wrapper_multiply(paulis: dict) -> None: + """Test custom __mul__ in c++ wrapper.""" + c, pcpp = fp.Pauli("I") * fp.Pauli("I") + # TODO: counter-intuitive interface for * operator + np.testing.assert_array_equal( + np.array(pcpp.to_tensor()), + np.eye(2), + ) + np.testing.assert_equal(c, 1) + + c, pcpp = fp.Pauli("Z") * fp.Pauli("Z") + np.testing.assert_array_equal( + np.array(pcpp.to_tensor()), + np.eye(2), + ) + np.testing.assert_equal(c, 1) + + for p1, p2 in it.permutations("IXYZ", 2): + c, pcpp = fp.Pauli(p1) * fp.Pauli(p2) + np.testing.assert_allclose( + c * np.array(pcpp.to_tensor()), + paulis[p1] @ paulis[p2], + atol=1e-15, + ) + + +def test_pauli_wrapper_exceptions() -> None: + """Test that exceptions from c++ are raised and propagated correctly.""" + with np.testing.assert_raises(ValueError): + fp.Pauli("II") + with np.testing.assert_raises(ValueError): + fp.Pauli("A") + with np.testing.assert_raises(ValueError): + fp.Pauli(-1) + with np.testing.assert_raises(ValueError): + fp.Pauli(5) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/bindings/test_pauli_string.py b/tests/bindings/test_pauli_string.py new file mode 100644 index 0000000..a27b381 --- /dev/null +++ b/tests/bindings/test_pauli_string.py @@ -0,0 +1,44 @@ +"""Test pauli objects from c++ against python implementations.""" + +import numpy as np +import pytest + +import fast_pauli._fast_pauli as fp + + +def test_pauli_string_wrapper(paulis: dict) -> None: + """Test pauli string wrapper in python land.""" + for empty_ps in [fp.PauliString(), fp.PauliString(""), fp.PauliString([])]: + assert empty_ps.weight == 0 + assert empty_ps.dims == 0 + assert empty_ps.n_qubits == 0 + assert str(empty_ps) == "" + assert len(empty_ps.to_tensor()) == 0 + + # TODO + + +def test_pauli_string_exceptions() -> None: + """Test that exceptions from c++ are raised and propagated correctly.""" + with np.testing.assert_raises(TypeError): + fp.PauliString([1, 2]) + with np.testing.assert_raises(ValueError): + fp.PauliString("ABC") + with np.testing.assert_raises(ValueError): + fp.PauliString("xyz") + + with np.testing.assert_raises(AttributeError): + fp.PauliString("XYZ").dims = 99 + with np.testing.assert_raises(AttributeError): + fp.PauliString("XYZ").weight = 99 + + with np.testing.assert_raises(ValueError): + fp.PauliString("II").apply([0.1, 0.2, 0.3]) + with np.testing.assert_raises(TypeError): + fp.PauliString("II").apply_batch([0.1, 0.2, 0.3, 0.4]) + with np.testing.assert_raises(ValueError): + fp.PauliString("XYZ").apply_batch(np.eye(4).tolist()) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e287286 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +"""pytest configuration file for fast_pauli tests.""" + +import numpy as np +import pytest + +from fast_pauli.pypauli.helpers import pauli_matrices + + +# TODO: make this as global fixture shared with pypauli unit tests +@pytest.fixture +def paulis() -> dict[str | int, np.ndarray]: + """Fixture to provide dict with Pauli matrices.""" + return pauli_matrices() # type: ignore diff --git a/tests/equivalence/__init__.py b/tests/equivalence/__init__.py new file mode 100644 index 0000000..007b624 --- /dev/null +++ b/tests/equivalence/__init__.py @@ -0,0 +1 @@ +"""Test module for testing C++ library against python implementation.""" From 7f682dced60dac15c332d3c8f9b52a1396adf76e Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:18:27 -0400 Subject: [PATCH 03/11] Few small c++ changes --- fast_pauli/cpp/include/__pauli_string.hpp | 2 +- fast_pauli/cpp/tests/test_pauli_string.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fast_pauli/cpp/include/__pauli_string.hpp b/fast_pauli/cpp/include/__pauli_string.hpp index 7f8588e..b638aa4 100644 --- a/fast_pauli/cpp/include/__pauli_string.hpp +++ b/fast_pauli/cpp/include/__pauli_string.hpp @@ -466,7 +466,7 @@ template <> struct fmt::formatter { template auto format(fast_pauli::PauliString const &ps, FormatContext &ctx) const { std::vector paulis = ps.paulis; - return fmt::format_to(ctx.out(), "{}", fmt::join(paulis, "x")); + return fmt::format_to(ctx.out(), "{}", fmt::join(paulis, "")); } }; diff --git a/fast_pauli/cpp/tests/test_pauli_string.cpp b/fast_pauli/cpp/tests/test_pauli_string.cpp index 5c31721..bb6169f 100644 --- a/fast_pauli/cpp/tests/test_pauli_string.cpp +++ b/fast_pauli/cpp/tests/test_pauli_string.cpp @@ -124,7 +124,7 @@ TEST_CASE("getters") { { PauliString ps; CHECK(ps.n_qubits() == 0); - CHECK(ps.dims() == 1); + CHECK(ps.dims() == 0); } } From 37053998511f46594d7d18d5878482bea0a9101d Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:25:02 -0400 Subject: [PATCH 04/11] Cover PauliString from _fast_pauli module with tests --- tests/bindings/test_pauli_string.py | 169 +++++++++++++++++++++++++++- tests/conftest.py | 3 + 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/tests/bindings/test_pauli_string.py b/tests/bindings/test_pauli_string.py index a27b381..f702c57 100644 --- a/tests/bindings/test_pauli_string.py +++ b/tests/bindings/test_pauli_string.py @@ -1,9 +1,18 @@ """Test pauli objects from c++ against python implementations.""" +import itertools as it + import numpy as np import pytest import fast_pauli._fast_pauli as fp +from fast_pauli.pypauli.helpers import naive_pauli_converter + +# TODO: consider unifying unit tests (+ numerical validations) for equivalent structures +# from pypauli and _fast_pauli submodules. Essentially, we would have unified +# test cases parametrized by _fast_pauli.PauliString and pypauli.PauliString. +# This would require identical interface for both things +# (which is probably what are aiming for in a long term) def test_pauli_string_wrapper(paulis: dict) -> None: @@ -15,7 +24,165 @@ def test_pauli_string_wrapper(paulis: dict) -> None: assert str(empty_ps) == "" assert len(empty_ps.to_tensor()) == 0 - # TODO + for s in it.chain( + ["I", "X", "Y", "Z"], + [[fp.Pauli("I")], [fp.Pauli("X")], [fp.Pauli("Y")], [fp.Pauli("Z")]], + ): + p = fp.PauliString(s) + assert p.weight == 1 or str(p) == "I" + assert p.dims == 2 + assert p.n_qubits == 1 + if isinstance(s, str): + assert str(p) == s + np.testing.assert_allclose( + np.array(p.to_tensor()), + paulis[s], + atol=1e-15, + ) + + for s in map(lambda x: "".join(x), it.permutations("IXYZ", 3)): + p = fp.PauliString(s) + assert p.weight == len(s) - s.count("I") + assert p.dims == 8 + assert p.n_qubits == 3 + assert str(p) == s + np.testing.assert_allclose( + np.array(p.to_tensor()), + np.kron(paulis[s[0]], np.kron(paulis[s[1]], paulis[s[2]])), + atol=1e-15, + ) + + +def test_sparse_pauli_composer(paulis: dict) -> None: + """Test sparse pauli composer inside c++ implementation of to_tensor().""" + ps = fp.PauliString("II") + assert ps.weight == 0 + assert ps.dims == 4 + np.testing.assert_array_equal(np.array(ps.to_tensor()), np.eye(4)) + + ps = fp.PauliString("IIII") + assert ps.weight == 0 + assert ps.dims == 16 + np.testing.assert_array_equal(np.array(ps.to_tensor()), np.eye(16)) + + ps = fp.PauliString("XXX") + assert ps.weight == 3 + assert ps.dims == 8 + np.testing.assert_array_equal(np.array(ps.to_tensor()), np.fliplr(np.eye(8))) + + ps = fp.PauliString("IY") + assert ps.weight == 1 + assert ps.dims == 4 + np.testing.assert_array_equal( + np.array(ps.to_tensor()), + np.block([[paulis["Y"], np.zeros((2, 2))], [np.zeros((2, 2)), paulis["Y"]]]), + ) + + ps = fp.PauliString("IZ") + assert ps.weight == 1 + assert ps.dims == 4 + np.testing.assert_array_equal( + np.array(ps.to_tensor()), + np.block([[paulis["Z"], np.zeros((2, 2))], [np.zeros((2, 2)), paulis["Z"]]]), + ) + + for s in it.chain( + map(lambda x: "".join(x), it.permutations("IXYZ", 2)), + ["XYIZXYZ", "XXIYYIZZ", "ZIXIZYXXY"], + ): + ps = fp.PauliString(s) + assert ps.weight == len(s) - s.count("I") + assert ps.dims == 2 ** len(s) + assert str(ps) == s + np.testing.assert_allclose( + np.array(ps.to_tensor()), + naive_pauli_converter(s), + atol=1e-15, + ) + + +def test_pauli_string_apply() -> None: + """Test pauli string multiplication with 1d vector.""" + rng = np.random.default_rng(321) + + np.testing.assert_allclose( + np.array(fp.PauliString("IXYZ").apply(np.zeros(16).tolist())), + np.zeros(16), + atol=1e-15, + ) + + np.testing.assert_allclose( + np.array(fp.PauliString("III").apply(np.arange(8).tolist())), + np.arange(8), + atol=1e-15, + ) + + np.testing.assert_allclose( + np.array(fp.PauliString("ZYX").apply(np.ones(8).tolist())), + naive_pauli_converter("ZYX").sum(1), + atol=1e-15, + ) + + for s in it.chain( + list("IXYZ"), + map(lambda x: "".join(x), it.permutations("IXYZ", 3)), + ["XYIZXYZ", "XXIYYIZZ", "ZIXIZYXX"], + ): + n = 2 ** len(s) + psi = rng.random(n) + np.testing.assert_allclose( + np.array(fp.PauliString(s).apply(psi.tolist())), + naive_pauli_converter(s).dot(psi), + atol=1e-15, + ) + + +def test_pauli_string_apply_batch() -> None: + """Test pauli string multiplication with 2d tensor.""" + rng = np.random.default_rng(321) + + np.testing.assert_allclose( + np.array(fp.PauliString("IXYZ").apply_batch(np.zeros((16, 16)).tolist())), + np.zeros((16, 16)), + atol=1e-15, + ) + + np.testing.assert_allclose( + np.array( + fp.PauliString("III").apply_batch(np.arange(8 * 8).reshape(8, 8).tolist()) + ), + np.arange(8 * 8).reshape(8, 8), + atol=1e-15, + ) + + np.testing.assert_allclose( + np.array(fp.PauliString("ZYX").apply_batch(np.eye(8).tolist())), + naive_pauli_converter("ZYX"), + atol=1e-15, + ) + + for s in it.chain( + list("IXYZ"), + map(lambda x: "".join(x), it.permutations("IXYZ", 3)), + ["XYIZXYZ", "XXIYYIZZ", "ZIXIZYXX"], + ): + n = 2 ** len(s) + psis = rng.random((n, 42)) + np.testing.assert_allclose( + np.array(fp.PauliString(s).apply_batch(psis.tolist())), + naive_pauli_converter(s) @ psis, + atol=1e-15, + ) + + for s in map(lambda x: "".join(x), it.permutations("IXYZ", 2)): + n = 2 ** len(s) + coef = rng.random() + psis = rng.random((n, 7)) + np.testing.assert_allclose( + np.array(fp.PauliString(s).apply_batch(psis.tolist(), coef)), + coef * naive_pauli_converter(s) @ psis, + atol=1e-15, + ) def test_pauli_string_exceptions() -> None: diff --git a/tests/conftest.py b/tests/conftest.py index e287286..b0905c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,3 +11,6 @@ def paulis() -> dict[str | int, np.ndarray]: """Fixture to provide dict with Pauli matrices.""" return pauli_matrices() # type: ignore + + +# TODO: fixtures to wrap around numpy testing functions with default tolerances From 4ac7f0f1b6bfe4eb7e52ecc70510418b977adcde Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:40:54 -0400 Subject: [PATCH 05/11] Tests for c++ objects against python implementations --- tests/{equivalence => approvals}/__init__.py | 0 tests/approvals/test_pauli_string.py | 69 ++++++++++++++++++++ 2 files changed, 69 insertions(+) rename tests/{equivalence => approvals}/__init__.py (100%) create mode 100644 tests/approvals/test_pauli_string.py diff --git a/tests/equivalence/__init__.py b/tests/approvals/__init__.py similarity index 100% rename from tests/equivalence/__init__.py rename to tests/approvals/__init__.py diff --git a/tests/approvals/test_pauli_string.py b/tests/approvals/test_pauli_string.py new file mode 100644 index 0000000..9564485 --- /dev/null +++ b/tests/approvals/test_pauli_string.py @@ -0,0 +1,69 @@ +"""Test pauli c++ objects against python implementations.""" + +import itertools as it + +import numpy as np +import pytest + +import fast_pauli._fast_pauli as fp +import fast_pauli.pypauli.operations as pp + + +@pytest.fixture +def sample_strings() -> list[str]: + """Provide sample strings for testing.""" + strings = it.chain( + ["I", "X", "Y", "Z"], + it.permutations("IXYZ", 2), + it.permutations("IXYZ", 3), + ["ZIZI", "YZYZ", "XYZXYZ", "ZZZIII", "XYIZXYZ", "XXIYYIZZ", "ZIXIZYXX"], + ) + return list(map("".join, strings)) + + +def test_pauli_object(paulis: dict) -> None: + """Test that C++ Pauli object is numerically equivalent to Python Pauli object.""" + # ideally, here we want to test against corresponding Pauli struct + # from python land, but currently we don't have one + for c in ["I", "X", "Y", "Z"]: + p = fp.Pauli(c) + np.testing.assert_array_equal( + np.array(p.to_tensor()), + paulis[c], + ) + np.testing.assert_string_equal(str(p), c) + + +def test_pauli_string_representations(sample_strings: list[str]) -> None: + """Test that C++ PauliString is numerically equivalent to Python PauliString.""" + for s in sample_strings: + pcpp = fp.PauliString(s) + ppy = pp.PauliString(s) + + np.testing.assert_allclose( + pcpp.to_tensor(), + ppy.dense(), + atol=1e-15, + ) + np.testing.assert_string_equal(str(pcpp), str(ppy)) + assert pcpp.dims == ppy.dim + assert pcpp.weight == ppy.weight + + +def test_pauli_string_apply_batch(sample_strings: list[str]) -> None: + """Test that C++ PauliString is numerically equivalent to Python PauliString.""" + rng = np.random.default_rng(321) + + for s in sample_strings: + n = 2 ** len(s) + psis = rng.random((n, 42)) + + np.testing.assert_allclose( + np.array(fp.PauliString(s).apply_batch(psis.tolist())), + pp.PauliString(s).multiply(psis), + atol=1e-15, + ) + + +if __name__ == "__main__": + pytest.main([__file__]) From 99e5ccd5507119cec45e721313333875aee190be Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:53:25 -0400 Subject: [PATCH 06/11] Arrange helper functions in a separate header --- fast_pauli/cpp/include/__pauli_helpers.hpp | 138 +++++++++++++++++++++ fast_pauli/cpp/include/__pauli_string.hpp | 126 ------------------- fast_pauli/cpp/include/fast_pauli.hpp | 1 + 3 files changed, 139 insertions(+), 126 deletions(-) create mode 100644 fast_pauli/cpp/include/__pauli_helpers.hpp diff --git a/fast_pauli/cpp/include/__pauli_helpers.hpp b/fast_pauli/cpp/include/__pauli_helpers.hpp new file mode 100644 index 0000000..ef276f0 --- /dev/null +++ b/fast_pauli/cpp/include/__pauli_helpers.hpp @@ -0,0 +1,138 @@ +#ifndef __PAULI_HELPERS_HPP +#define __PAULI_HELPERS_HPP + +#include + +#include + +#include "__pauli_string.hpp" + +namespace fast_pauli { +// +// Helper +// + +/** + * @brief Get the nontrivial sets of pauli matrices given a weight. + * + * @param weight + * @return std::vector + */ +std::vector get_nontrivial_paulis(size_t const weight) { + // We want to return no paulis for weight 0 + if (weight == 0) { + return {}; + } + + // For Weight >= 1 + std::vector set_of_nontrivial_paulis{"X", "Y", "Z"}; + + for (size_t i = 1; i < weight; i++) { + std::vector updated_set_of_nontrivial_paulis; + for (auto const &str : set_of_nontrivial_paulis) { + for (auto pauli : {"X", "Y", "Z"}) { + updated_set_of_nontrivial_paulis.push_back(str + pauli); + } + } + set_of_nontrivial_paulis = std::move(updated_set_of_nontrivial_paulis); + } + return set_of_nontrivial_paulis; +} + +/** + * @brief Get all the combinations of k indices for a given array of size n. + * + * @param n + * @param k + * @return std::vector> + */ +std::vector> idx_combinations(size_t const n, + size_t const k) { + + // TODO this is a very inefficient way to do this + std::vector> result; + std::vector bitmask(k, 1); // K leading 1's + bitmask.resize(n, 0); // N-K trailing 0's + + do { + std::vector combo; + for (size_t i = 0; i < n; ++i) { + if (bitmask[i]) { + combo.push_back(i); + } + } + result.push_back(combo); + } while (std::ranges::prev_permutation(bitmask).found); + return result; +} + +/** + * @brief Calculate all possible PauliStrings for a given number of qubits and + * weight and return them in lexicographical order. + * + * @param n_qubits + * @param weight + * @return std::vector + */ +std::vector calcutate_pauli_strings(size_t const n_qubits, + size_t const weight) { + + // base case + if (weight == 0) { + return {PauliString(std::string(n_qubits, 'I'))}; + } + + // for weight >= 1 + std::string base_str(n_qubits, 'I'); + + auto nontrivial_paulis = get_nontrivial_paulis(weight); + auto idx_combos = idx_combinations(n_qubits, weight); + size_t n_pauli_strings = nontrivial_paulis.size() * idx_combos.size(); + std::vector result(n_pauli_strings); + + fmt::println( + "n_qubits = {} weight = {} n_nontrivial_paulis = {} n_combos = {}", + n_qubits, weight, nontrivial_paulis.size(), idx_combos.size()); + + // Iterate through all the nontrivial paulis and all the combinations + for (size_t i = 0; i < nontrivial_paulis.size(); ++i) { + for (size_t j = 0; j < idx_combos.size(); ++j) { + // Creating new pauli string at index i*idx_combos.size() + j + // Overwriting the base string with the appropriate nontrivial paulis + // at the specified indices + std::string str = base_str; + for (size_t k = 0; k < idx_combos[j].size(); ++k) { + size_t idx = idx_combos[j][k]; + str[idx] = nontrivial_paulis[i][k]; + } + result[i * idx_combos.size() + j] = PauliString(str); + } + } + + return result; +} + +/** + * @brief Calculate all possible PauliStrings for a given number of qubits and + * all weights less than or equal to a given weight. + * + * @param n_qubits + * @param weight + * @return std::vector + */ +std::vector calculate_pauli_strings_max_weight(size_t n_qubits, + size_t weight) { + std::vector result; + for (size_t i = 0; i <= weight; ++i) { + auto ps = calcutate_pauli_strings(n_qubits, i); + result.insert(result.end(), ps.begin(), ps.end()); + } + + fmt::println("n_qubits = {} weight = {} n_pauli_strings = {}", n_qubits, + weight, result.size()); + return result; +} + +} // namespace fast_pauli + +#endif // __PAULI_HELPERS_HPP \ No newline at end of file diff --git a/fast_pauli/cpp/include/__pauli_string.hpp b/fast_pauli/cpp/include/__pauli_string.hpp index b638aa4..4b1f580 100644 --- a/fast_pauli/cpp/include/__pauli_string.hpp +++ b/fast_pauli/cpp/include/__pauli_string.hpp @@ -327,132 +327,6 @@ struct PauliString { } }; -// -// Helper -// -// TODO arrange following functions in a separate header __pauli_utils.hpp - -/** - * @brief Get the nontrivial sets of pauli matrices given a weight. - * - * @param weight - * @return std::vector - */ -std::vector get_nontrivial_paulis(size_t const weight) { - // We want to return no paulis for weight 0 - if (weight == 0) { - return {}; - } - - // For Weight >= 1 - std::vector set_of_nontrivial_paulis{"X", "Y", "Z"}; - - for (size_t i = 1; i < weight; i++) { - std::vector updated_set_of_nontrivial_paulis; - for (auto str : set_of_nontrivial_paulis) { - for (auto pauli : {"X", "Y", "Z"}) { - updated_set_of_nontrivial_paulis.push_back(str + pauli); - } - } - set_of_nontrivial_paulis = std::move(updated_set_of_nontrivial_paulis); - } - return set_of_nontrivial_paulis; -} - -/** - * @brief Get all the combinations of k indices for a given array of size n. - * - * @param n - * @param k - * @return std::vector> - */ -std::vector> idx_combinations(size_t const n, - size_t const k) { - - // TODO this is a very inefficient way to do this - std::vector> result; - std::vector bitmask(k, 1); // K leading 1's - bitmask.resize(n, 0); // N-K trailing 0's - - do { - std::vector combo; - for (size_t i = 0; i < n; ++i) { - if (bitmask[i]) { - combo.push_back(i); - } - } - result.push_back(combo); - } while (std::ranges::prev_permutation(bitmask).found); - return result; -} - -/** - * @brief Calculate all possible PauliStrings for a given number of qubits and - * weight and return them in lexicographical order. - * - * @param n_qubits - * @param weight - * @return std::vector - */ -std::vector calcutate_pauli_strings(size_t const n_qubits, - size_t const weight) { - - // base case - if (weight == 0) { - return {PauliString(std::string(n_qubits, 'I'))}; - } - - // for weight >= 1 - std::string base_str(n_qubits, 'I'); - - auto nontrivial_paulis = get_nontrivial_paulis(weight); - auto idx_combos = idx_combinations(n_qubits, weight); - size_t n_pauli_strings = nontrivial_paulis.size() * idx_combos.size(); - std::vector result(n_pauli_strings); - - fmt::println( - "n_qubits = {} weight = {} n_nontrivial_paulis = {} n_combos = {}", - n_qubits, weight, nontrivial_paulis.size(), idx_combos.size()); - - // Iterate through all the nontrivial paulis and all the combinations - for (size_t i = 0; i < nontrivial_paulis.size(); ++i) { - for (size_t j = 0; j < idx_combos.size(); ++j) { - // Creating new pauli string at index i*idx_combos.size() + j - // Overwriting the base string with the appropriate nontrivial paulis - // at the specified indices - std::string str = base_str; - for (size_t k = 0; k < idx_combos[j].size(); ++k) { - size_t idx = idx_combos[j][k]; - str[idx] = nontrivial_paulis[i][k]; - } - result[i * idx_combos.size() + j] = PauliString(str); - } - } - - return result; -} - -/** - * @brief Calculate all possible PauliStrings for a given number of qubits and - * all weights less than or equal to a given weight. - * - * @param n_qubits - * @param weight - * @return std::vector - */ -std::vector calculate_pauli_strings_max_weight(size_t n_qubits, - size_t weight) { - std::vector result; - for (size_t i = 0; i <= weight; ++i) { - auto ps = calcutate_pauli_strings(n_qubits, i); - result.insert(result.end(), ps.begin(), ps.end()); - } - - fmt::println("n_qubits = {} weight = {} n_pauli_strings = {}", n_qubits, - weight, result.size()); - return result; -} - } // namespace fast_pauli // diff --git a/fast_pauli/cpp/include/fast_pauli.hpp b/fast_pauli/cpp/include/fast_pauli.hpp index 24c6abe..e79c580 100644 --- a/fast_pauli/cpp/include/fast_pauli.hpp +++ b/fast_pauli/cpp/include/fast_pauli.hpp @@ -3,6 +3,7 @@ #include "__factory.hpp" #include "__pauli.hpp" +#include "__pauli_helpers.hpp" #include "__pauli_op.hpp" #include "__pauli_string.hpp" #include "__summed_pauli_op.hpp" From 32d59baf974eb36e15c29f61bdbb74de138984f2 Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:55:58 -0400 Subject: [PATCH 07/11] Minor cosmetics in cpp files --- fast_pauli/cpp/include/__pauli_string.hpp | 2 +- fast_pauli/cpp/src/fast_pauli.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/fast_pauli/cpp/include/__pauli_string.hpp b/fast_pauli/cpp/include/__pauli_string.hpp index 4b1f580..15431e7 100644 --- a/fast_pauli/cpp/include/__pauli_string.hpp +++ b/fast_pauli/cpp/include/__pauli_string.hpp @@ -229,7 +229,7 @@ struct PauliString { */ template std::vector> - apply(std::mdspan, std::dextents> v) const { + apply(std::mdspan const, std::dextents> v) const { // Input check if (v.size() != dims()) { throw std::invalid_argument( diff --git a/fast_pauli/cpp/src/fast_pauli.cpp b/fast_pauli/cpp/src/fast_pauli.cpp index 446b3c0..f9685e2 100644 --- a/fast_pauli/cpp/src/fast_pauli.cpp +++ b/fast_pauli/cpp/src/fast_pauli.cpp @@ -45,12 +45,14 @@ PYBIND11_MODULE(_fast_pauli, m) { [](fp::PauliString const &self, std::vector>> inputs, std::complex coef) { + if (inputs.empty()) + return std::vector>>{}; // for now we expect row major inputs which have states as columns - const size_t n_states = inputs[0].size(); + size_t const n_states = inputs.front().size(); std::vector> flat_inputs; flat_inputs.reserve(inputs.size() * n_states); - for (const auto &vec : inputs) + for (auto const &vec : inputs) if (vec.size() != n_states) throw std::invalid_argument("Bad shape of states array"); else @@ -66,6 +68,7 @@ PYBIND11_MODULE(_fast_pauli, m) { flat_inputs.data(), inputs.size(), n_states}, coef); + // TODO arrange this ugly converters into utility functions at least std::vector>> results( inputs.size()); for (size_t i = 0; i < inputs.size(); i++) { From c1f7111a8f69cd9b415ff34d299427d041a4a8a3 Mon Sep 17 00:00:00 2001 From: Eugene <16805621+stand-by@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:33 -0400 Subject: [PATCH 08/11] Update fast_pauli/cpp/include/__pauli_helpers.hpp Co-authored-by: James E T Smith --- fast_pauli/cpp/include/__pauli_helpers.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast_pauli/cpp/include/__pauli_helpers.hpp b/fast_pauli/cpp/include/__pauli_helpers.hpp index ef276f0..e8cf30a 100644 --- a/fast_pauli/cpp/include/__pauli_helpers.hpp +++ b/fast_pauli/cpp/include/__pauli_helpers.hpp @@ -135,4 +135,4 @@ std::vector calculate_pauli_strings_max_weight(size_t n_qubits, } // namespace fast_pauli -#endif // __PAULI_HELPERS_HPP \ No newline at end of file +#endif // __PAULI_HELPERS_HPP From 94df8ad866245b57c8fb42d12091d632c57d3ec4 Mon Sep 17 00:00:00 2001 From: Eugene <16805621+stand-by@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:44 -0400 Subject: [PATCH 09/11] Update fast_pauli/cpp/src/fast_pauli.cpp Co-authored-by: James E T Smith --- fast_pauli/cpp/src/fast_pauli.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast_pauli/cpp/src/fast_pauli.cpp b/fast_pauli/cpp/src/fast_pauli.cpp index f9685e2..8ab1822 100644 --- a/fast_pauli/cpp/src/fast_pauli.cpp +++ b/fast_pauli/cpp/src/fast_pauli.cpp @@ -78,7 +78,7 @@ PYBIND11_MODULE(_fast_pauli, m) { } return results; }, - "states"_a, "coef"_a = std::complex{1.0}) + "states"_a, "coeff"_a = std::complex{1.0}) .def("__str__", [](fp::PauliString const &self) { return fmt::format("{}", self); }); From c64112bde4986ad52d7f90eaae930214f6f9a631 Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:51:16 -0400 Subject: [PATCH 10/11] Address comments --- fast_pauli/cpp/include/__pauli_string.hpp | 1 + tests/approvals/test_pauli_string.py | 12 +++++---- tests/bindings/test_pauli.py | 18 ++----------- tests/bindings/test_pauli_string.py | 32 +++++++++++------------ tests/conftest.py | 8 +++++- 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/fast_pauli/cpp/include/__pauli_string.hpp b/fast_pauli/cpp/include/__pauli_string.hpp index 15431e7..d5bf83c 100644 --- a/fast_pauli/cpp/include/__pauli_string.hpp +++ b/fast_pauli/cpp/include/__pauli_string.hpp @@ -103,6 +103,7 @@ struct PauliString { /** * @brief Return the dimension (2^n_qubits) of the PauliString. + * @note this returns 0 if the PauliString is empty. * * @return size_t */ diff --git a/tests/approvals/test_pauli_string.py b/tests/approvals/test_pauli_string.py index 9564485..53f7084 100644 --- a/tests/approvals/test_pauli_string.py +++ b/tests/approvals/test_pauli_string.py @@ -1,6 +1,7 @@ """Test pauli c++ objects against python implementations.""" import itertools as it +from typing import Callable import numpy as np import pytest @@ -50,13 +51,14 @@ def test_pauli_string_representations(sample_strings: list[str]) -> None: assert pcpp.weight == ppy.weight -def test_pauli_string_apply_batch(sample_strings: list[str]) -> None: +def test_pauli_string_apply_batch( + sample_strings: list[str], generate_random_complex: Callable +) -> None: """Test that C++ PauliString is numerically equivalent to Python PauliString.""" - rng = np.random.default_rng(321) - for s in sample_strings: - n = 2 ** len(s) - psis = rng.random((n, 42)) + n_dim = 2 ** len(s) + n_states = 42 + psis = generate_random_complex(n_dim, n_states) np.testing.assert_allclose( np.array(fp.PauliString(s).apply_batch(psis.tolist())), diff --git a/tests/bindings/test_pauli.py b/tests/bindings/test_pauli.py index 42dffb2..9fe1c5e 100644 --- a/tests/bindings/test_pauli.py +++ b/tests/bindings/test_pauli.py @@ -41,22 +41,8 @@ def test_pauli_wrapper(paulis: dict) -> None: def test_pauli_wrapper_multiply(paulis: dict) -> None: """Test custom __mul__ in c++ wrapper.""" - c, pcpp = fp.Pauli("I") * fp.Pauli("I") # TODO: counter-intuitive interface for * operator - np.testing.assert_array_equal( - np.array(pcpp.to_tensor()), - np.eye(2), - ) - np.testing.assert_equal(c, 1) - - c, pcpp = fp.Pauli("Z") * fp.Pauli("Z") - np.testing.assert_array_equal( - np.array(pcpp.to_tensor()), - np.eye(2), - ) - np.testing.assert_equal(c, 1) - - for p1, p2 in it.permutations("IXYZ", 2): + for p1, p2 in it.product("IXYZ", repeat=2): c, pcpp = fp.Pauli(p1) * fp.Pauli(p2) np.testing.assert_allclose( c * np.array(pcpp.to_tensor()), @@ -78,4 +64,4 @@ def test_pauli_wrapper_exceptions() -> None: if __name__ == "__main__": - pytest.main() + pytest.main([__file__]) diff --git a/tests/bindings/test_pauli_string.py b/tests/bindings/test_pauli_string.py index f702c57..a663492 100644 --- a/tests/bindings/test_pauli_string.py +++ b/tests/bindings/test_pauli_string.py @@ -1,6 +1,7 @@ """Test pauli objects from c++ against python implementations.""" import itertools as it +from typing import Callable import numpy as np import pytest @@ -101,10 +102,8 @@ def test_sparse_pauli_composer(paulis: dict) -> None: ) -def test_pauli_string_apply() -> None: +def test_pauli_string_apply(generate_random_complex: Callable) -> None: """Test pauli string multiplication with 1d vector.""" - rng = np.random.default_rng(321) - np.testing.assert_allclose( np.array(fp.PauliString("IXYZ").apply(np.zeros(16).tolist())), np.zeros(16), @@ -128,8 +127,8 @@ def test_pauli_string_apply() -> None: map(lambda x: "".join(x), it.permutations("IXYZ", 3)), ["XYIZXYZ", "XXIYYIZZ", "ZIXIZYXX"], ): - n = 2 ** len(s) - psi = rng.random(n) + n_dim = 2 ** len(s) + psi = generate_random_complex(n_dim) np.testing.assert_allclose( np.array(fp.PauliString(s).apply(psi.tolist())), naive_pauli_converter(s).dot(psi), @@ -137,10 +136,8 @@ def test_pauli_string_apply() -> None: ) -def test_pauli_string_apply_batch() -> None: +def test_pauli_string_apply_batch(generate_random_complex: Callable) -> None: """Test pauli string multiplication with 2d tensor.""" - rng = np.random.default_rng(321) - np.testing.assert_allclose( np.array(fp.PauliString("IXYZ").apply_batch(np.zeros((16, 16)).tolist())), np.zeros((16, 16)), @@ -166,8 +163,9 @@ def test_pauli_string_apply_batch() -> None: map(lambda x: "".join(x), it.permutations("IXYZ", 3)), ["XYIZXYZ", "XXIYYIZZ", "ZIXIZYXX"], ): - n = 2 ** len(s) - psis = rng.random((n, 42)) + n_dim = 2 ** len(s) + n_states = 42 + psis = generate_random_complex(n_dim, n_states) np.testing.assert_allclose( np.array(fp.PauliString(s).apply_batch(psis.tolist())), naive_pauli_converter(s) @ psis, @@ -175,12 +173,14 @@ def test_pauli_string_apply_batch() -> None: ) for s in map(lambda x: "".join(x), it.permutations("IXYZ", 2)): - n = 2 ** len(s) - coef = rng.random() - psis = rng.random((n, 7)) + n_dim = 2 ** len(s) + n_states = 7 + coeff = generate_random_complex(1)[0] + psis = generate_random_complex(n_dim, n_states) + np.testing.assert_allclose( - np.array(fp.PauliString(s).apply_batch(psis.tolist(), coef)), - coef * naive_pauli_converter(s) @ psis, + np.array(fp.PauliString(s).apply_batch(psis.tolist(), coeff)), + coeff * naive_pauli_converter(s) @ psis, atol=1e-15, ) @@ -208,4 +208,4 @@ def test_pauli_string_exceptions() -> None: if __name__ == "__main__": - pytest.main() + pytest.main([__file__]) diff --git a/tests/conftest.py b/tests/conftest.py index b0905c8..8b47476 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ from fast_pauli.pypauli.helpers import pauli_matrices +# TODO: fixtures to wrap around numpy testing functions with default tolerances + # TODO: make this as global fixture shared with pypauli unit tests @pytest.fixture @@ -13,4 +15,8 @@ def paulis() -> dict[str | int, np.ndarray]: return pauli_matrices() # type: ignore -# TODO: fixtures to wrap around numpy testing functions with default tolerances +@pytest.fixture(scope="function") +def generate_random_complex(rng_seed: int = 321) -> np.ndarray: + """Generate random complex numpy array with desired shape.""" + rng = np.random.default_rng(rng_seed) + return lambda *shape: rng.random(shape) + 1j * rng.random(shape) From 08f8af5ac54d61709a3657c68d320f78e9b85e5b Mon Sep 17 00:00:00 2001 From: Eugene Rublenko <16805621+stand-by@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:52:17 -0400 Subject: [PATCH 11/11] Update CI/CD to run python tests --- .github/workflows/all_push.yml | 6 ++++++ Makefile | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/all_push.yml b/.github/workflows/all_push.yml index e37c887..b8f15dd 100644 --- a/.github/workflows/all_push.yml +++ b/.github/workflows/all_push.yml @@ -29,6 +29,10 @@ jobs: # Build your program with the given configuration run: | cmake --build ${{github.workspace}}/build --verbose --parallel + - name: Install + run: | + cmake --install ${{github.workspace}}/build + python -m pip install .[dev] - name: Test C++ env: OMP_NUM_THREADS: 2 @@ -40,6 +44,8 @@ jobs: ./${CPP_TEST_DIR}/test_pauli_op --test-case-exclude="*multistring*" ./${CPP_TEST_DIR}/test_pauli_string ./${CPP_TEST_DIR}/test_summed_pauli_op + - name: Test Python + run: make test-py # - name: Test Python # run: PYTHONPATH=build:$PYTHONPATH pytest -v test diff --git a/Makefile b/Makefile index 7ca344b..f1560f5 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,15 @@ build: python -m pip install ".[dev]" python -m build . -.PHONY: tests -tests: +test-cpp: ctest --test-dir build - python -m pytest fast_pauli/py/tests - python -m pytest tests + +test-py: + python -m pytest -v fast_pauli/py/tests + python -m pytest -v tests + +.PHONY: test +test: test-cpp test-py .PHONY: clean clean: