diff --git a/fast_pauli/__init__.py b/fast_pauli/__init__.py index 2e7a15b..f6c64c1 100644 --- a/fast_pauli/__init__.py +++ b/fast_pauli/__init__.py @@ -1,4 +1,4 @@ -"""Fast Pauli and helpers.""" +""":code:`fast_pauli` is a Python package with C++ backend, optimized for operations on Pauli Matrices and Pauli Strings.""" # noqa: E501 from ._fast_pauli import ( # noqa: F401 Pauli, diff --git a/fast_pauli/cpp/src/fast_pauli.cpp b/fast_pauli/cpp/src/fast_pauli.cpp index 424e1c1..5662cb2 100644 --- a/fast_pauli/cpp/src/fast_pauli.cpp +++ b/fast_pauli/cpp/src/fast_pauli.cpp @@ -22,63 +22,173 @@ NB_MODULE(_fast_pauli, m) using float_type = double; using cfloat_t = std::complex; - nb::class_(m, "Pauli") + nb::class_( + m, "Pauli", + R"%(A class for efficient representation of a :math:`2 \times 2` Pauli Matrix :math:`\sigma_i \in \{ I, X, Y, Z \}`)%") // Constructors - .def(nb::init<>()) - .def(nb::init(), "code"_a) - .def(nb::init(), "symbol"_a) - + .def(nb::init<>(), "Default constructor to initialize with identity matrix.") + .def(nb::init(), "code"_a, + R"%(Constructor given a numeric code. + +Parameters +---------- +code : int + Numerical number for corresponding Pauli matrix :math:`0: I, 1: X, 2: Y, 3: Z` +)%") + .def(nb::init(), "symbol"_a, + R"%(Constructor given Pauli matrix symbol. + +Parameters +---------- +symbol : str + Pauli matrix symbol :math:`I, X, Y, Z` +)%") // Methods .def( - "__matmul__", [](fp::Pauli const &self, fp::Pauli const &rhs) { return self * rhs; }, nb::is_operator()) - .def("to_tensor", - [](fp::Pauli const &self) { - auto dense_pauli = fp::__detail::owning_ndarray_from_shape({2, 2}); - self.to_tensor(fp::__detail::ndarray_to_mdspan(dense_pauli)); - return dense_pauli; - }) - .def("multiply", [](fp::Pauli const &self, fp::Pauli const &rhs) { return self * rhs; }) - .def("__str__", [](fp::Pauli const &self) { return fmt::format("{}", self); }); + "__matmul__", [](fp::Pauli const &self, fp::Pauli const &rhs) { return self * rhs; }, nb::is_operator(), + R"%(Returns matrix product of two pauli matrices and their phase as a pair. + +Parameters +---------- +rhs : Pauli + Right hand side Pauli object + +Returns +------- +tuple of complex and Pauli + Phase and resulting pauli object +)%") + .def( + "to_tensor", + [](fp::Pauli const &self) { + auto dense_pauli = fp::__detail::owning_ndarray_from_shape({2, 2}); + self.to_tensor(fp::__detail::ndarray_to_mdspan(dense_pauli)); + return dense_pauli; + }, + R"%(Returns a dense representation of Pauli matrix. + +Returns +------- +np.ndarray + 2D numpy array of complex numbers +)%") + .def( + "__str__", [](fp::Pauli const &self) { return fmt::format("{}", self); }, + R"%(Returns a string representation of Pauli matrix. + +Returns +------- +str + single character string representing Pauli matrix :math:`I, X, Y, Z` +)%"); // // // - nb::class_(m, "PauliString") - // Constructors - .def(nb::init<>()) - .def(nb::init(), "string"_a) - .def(nb::init &>(), "paulis"_a) + nb::class_( + m, "PauliString", + R"%(A class representation of a Pauli String :math:`\mathcal{\hat{P}}` (i.e. a tensor product of Pauli matrices) - // - .def("__str__", [](fp::PauliString const &self) { return fmt::format("{}", self); }) +.. math:: + \mathcal{\hat{P}} = \bigotimes_i \sigma_i + + \sigma_i \in \{ I,X,Y,Z \} +)%") + // Constructors + .def(nb::init<>(), "Default constructor to initialize with empty string.") + .def( + nb::init(), "string"_a, + R"%(Constructs a PauliString from a string and calculates the weight. This is often the most compact way to initialize a PauliString. + +Parameters +---------- +string : str + Pauli String representation. Each character should be one of :math:`I, X, Y, Z` +)%") + .def(nb::init &>(), "paulis"_a, + R"%(Constructs a PauliString from a list of Pauli objects and calculates the weight. + +Parameters +---------- +paulis : list[Pauli] + List of ordered Pauli objects +)%") + .def( + "__str__", [](fp::PauliString const &self) { return fmt::format("{}", self); }, + R"%(Returns a string representation of PauliString object. + +Returns +------- +str + string representation of PauliString object +)%") .def( "__matmul__", [](fp::PauliString const &self, fp::PauliString const &rhs) { return self * rhs; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns matrix product of two pauli strings and their phase as a pair. + +Parameters +---------- +rhs : PauliString + Right hand side PauliString object + +Returns +------- +tuple of complex and Pauli + Phase and resulting pauli object +)%") .def( "__add__", [](fp::PauliString const &self, fp::PauliString const &other) { return fp::PauliOp({self, other}); }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the sum of two Pauli strings in a form of PauliOp object. + +Parameters +---------- +rhs : PauliString + The other PauliString object to add + +Returns +------- +PauliOp + PauliOp instance holding the sum of two Pauli strings. +)%") .def( "__sub__", [](fp::PauliString const &self, fp::PauliString const &other) { return fp::PauliOp({1, -1}, {self, other}); }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the difference of two Pauli strings in a form of PauliOp object. + +Parameters +---------- +rhs : PauliString + The other PauliString object to subtract + +Returns +------- +PauliOp + PauliOp instance holding the difference of two Pauli strings. +)%") // Properties - .def_prop_ro("n_qubits", &fp::PauliString::n_qubits) - .def_prop_ro("dim", &fp::PauliString::dim) - .def_prop_ro("weight", [](fp::PauliString const &self) { return self.weight; }) + .def_prop_ro("n_qubits", &fp::PauliString::n_qubits, + "int: The number of qubits in PauliString (i.e. number of Pauli Matrices in tensor product)") + .def_prop_ro("dim", &fp::PauliString::dim, + "int: The dimension of PauliString :math:`2^n, n` - number of qubits") + .def_prop_ro( + "weight", [](fp::PauliString const &self) { return self.weight; }, + "int: The weight of PauliString (i.e. number of non-identity Pauli matrices in it)") // Methods .def( "apply", [](fp::PauliString const &self, nb::ndarray states, cfloat_t c) { - // TODO handle the non-transposed case since that's likely the most - // common - + // TODO handle the non-transposed case since that's likely the most common + // TODO we should handle when users pass a single state (i.e. a 1D array here) if (states.ndim() == 1) { // TODO lots of duplicate code here @@ -102,10 +212,30 @@ NB_MODULE(_fast_pauli, m) fmt::format("apply: expected 1 or 2 dimensions, got {}", states.ndim())); } }, - "states"_a, "coeff"_a = cfloat_t{1.0}) + "states"_a, "coeff"_a = cfloat_t{1.0}, + R"%(Apply a Pauli string to a single dimensional state vector or a batch of states. + +.. math:: + \mathcal{\hat{P}} \ket{\psi_t} + +.. note:: + For batch mode it applies the PauliString to each individual state separately. + In this case, the input array is expected to have the shape of (n_dims, n_states) with states stored as columns. + +Parameters +---------- +states : np.ndarray + The original state(s) represented as 1D or 2D numpy array with states along the columns for batched application. + Outer dimension must match the dimansionality of Pauli string. +coeff : complex + Multiplication factor to scale the PauliString before applying to states + +Returns +------- +np.ndarray + New state(s) in a form of 1D or 2D numpy array corresponding to the shape of input states +)%") .def( - // TODO we should handle when users pass a single state (i.e. a 1D - // array here) "expectation_value", [](fp::PauliString const &self, nb::ndarray states, cfloat_t c) { if (states.ndim() == 1) @@ -134,52 +264,173 @@ NB_MODULE(_fast_pauli, m) fmt::format("expectation_value: expected 1 or 2 dimensions, got {}", states.ndim())); } }, - "states"_a, "coeff"_a = cfloat_t{1.0}) - .def("to_tensor", - [](fp::PauliString const &self) { - auto dense_pauli_str = fp::__detail::owning_ndarray_from_shape({self.dim(), self.dim()}); - self.to_tensor(fp::__detail::ndarray_to_mdspan(dense_pauli_str)); - return dense_pauli_str; - }) - // - ; + "states"_a, "coeff"_a = cfloat_t{1.0}, + R"%(Calculate expectation value(s) for a given single dimensional state vector or a batch of states. + +.. math:: + \bra{\psi_t} \mathcal{\hat{P}} \ket{\psi_t} + +.. note:: + For batch mode it computes the expectation value for each individual state separately. + In this case, the input array is expected to have the shape of (n_dims, n_states) with states stored as columns. + +Parameters +---------- +states : np.ndarray + The original state(s) represented as 1D or 2D numpy array with states along the columns for batched application +coeff : complex + Multiplication factor to scale the PauliString before calculating the expectation value + +Returns +------- +np.ndarray + Expectation value(s) in a form of 1D numpy array corresponding to the number of states +)%") + .def( + "to_tensor", + [](fp::PauliString const &self) { + auto dense_pauli_str = fp::__detail::owning_ndarray_from_shape({self.dim(), self.dim()}); + self.to_tensor(fp::__detail::ndarray_to_mdspan(dense_pauli_str)); + return dense_pauli_str; + }, + R"%(Returns a dense representation of PauliString. + +Returns +------- +np.ndarray + 2D numpy array of complex numbers + )%"); // - nb::class_>(m, "PauliOp") + // + // + + nb::class_>( + m, "PauliOp", + R"%(A class representation for a Pauli Operator :math:`A` (i.e. a weighted sum of Pauli Strings) + +.. math:: + A = \sum_k h_k \mathcal{\hat{P}}_k + + \mathcal{\hat{P}} = \bigotimes_i \sigma_i \quad h_k \in \mathbb{C} +)%") // Constructors - .def(nb::init<>()) - .def(nb::init const &>(), "pauli_strings"_a) - .def(nb::init>()) - .def("__init__", - [](fp::PauliOp *new_obj, nb::ndarray coeffs, - std::vector const &pauli_strings) { - auto [coeffs_vec, _] = fp::__detail::ndarray_to_raw(coeffs); - new (new_obj) fp::PauliOp(coeffs_vec, pauli_strings); - }) - .def("__init__", - [](fp::PauliOp *new_obj, std::vector coeffs_vec, - std::vector const &pauli_strings) { - new (new_obj) fp::PauliOp(coeffs_vec, pauli_strings); - }) - .def("__init__", - [](fp::PauliOp *new_obj, std::vector coeffs_vec, - std::vector const &strings) { - std::vector pauli_strings; - std::transform(strings.begin(), strings.end(), std::back_inserter(pauli_strings), - [](std::string const &pauli) { return fp::PauliString(pauli); }); - new (new_obj) fp::PauliOp(coeffs_vec, pauli_strings); - }) + .def(nb::init<>(), "Default constructor to initialize strings and coefficients with empty arrays.") + .def(nb::init const &>(), "pauli_strings"_a, + R"%(Construct a PauliOp from a list of strings and default corresponding coefficients to ones. + +Parameters +---------- +pauli_strings : List[str] + List of Pauli Strings representation. Each string should be composed of characters :math:`I, X, Y, Z` +)%") + .def(nb::init>(), + R"%(Construct a PauliOp from a list of PauliString objects and default corresponding coefficients to ones. + +Parameters +---------- +pauli_strings : List[PauliString] + List of PauliString objects. +)%") + .def( + "__init__", + [](fp::PauliOp *new_obj, nb::ndarray coeffs, + std::vector const &pauli_strings) { + auto [coeffs_vec, _] = fp::__detail::ndarray_to_raw(coeffs); + new (new_obj) fp::PauliOp(coeffs_vec, pauli_strings); + }, + "coefficients"_a, "pauli_strings"_a, + R"%(Construct a PauliOp from a list of PauliString objects and corresponding coefficients. + +Parameters +---------- +coefficients : np.ndarray + Array of coefficients corresponding to Pauli strings. +pauli_strings : List[PauliString] + List of PauliString objects. +)%") + .def( + "__init__", + [](fp::PauliOp *new_obj, std::vector coeffs_vec, + std::vector const &pauli_strings) { + new (new_obj) fp::PauliOp(coeffs_vec, pauli_strings); + }, + "coefficients"_a, "pauli_strings"_a, + R"%(Construct a PauliOp from a list of PauliString objects and corresponding coefficients. + +Parameters +---------- +coefficients : List[complex] + List of coefficients corresponding to Pauli strings. +pauli_strings : List[PauliString] + List of PauliString objects. +)%") + .def( + "__init__", + [](fp::PauliOp *new_obj, std::vector coeffs_vec, + std::vector const &strings) { + std::vector pauli_strings; + std::transform(strings.begin(), strings.end(), std::back_inserter(pauli_strings), + [](std::string const &pauli) { return fp::PauliString(pauli); }); + new (new_obj) fp::PauliOp(coeffs_vec, pauli_strings); + }, + "coefficients"_a, "pauli_strings"_a, + R"%(Construct a PauliOp from a list of strings and corresponding coefficients. + +Parameters +---------- +coefficients : np.ndarray + Array of coefficients corresponding to Pauli strings. +pauli_strings : List[str] + List of Pauli Strings representation. Each string should be composed of characters :math:`I, X, Y, Z` +)%") // TODO memory efficient implementations for inplace @= operators .def( "__matmul__", [](fp::PauliOp const &self, fp::PauliOp const &rhs) { return self * rhs; }, - nb::is_operator()) + nb::is_operator(), + R"%(Matrix multiplication of two Pauli Operators. + +Parameters +---------- +rhs : PauliOp + Right hand side PauliOp object + +Returns +------- +PauliOp + New PauliOp instance containing the result of the multiplication +)%") .def( "__matmul__", [](fp::PauliOp const &self, fp::PauliString const &rhs) { return self * rhs; }, - nb::is_operator()) + nb::is_operator(), + R"%(Matrix multiplication of PauliOp with a PauliString on the right. + +Parameters +---------- +rhs : PauliString + Right hand side PauliString object + +Returns +------- +PauliOp + New PauliOp instance containing the result of the multiplication +)%") .def( "__rmatmul__", [](fp::PauliOp const &self, fp::PauliString const &lhs) { return lhs * self; }, - nb::is_operator()) + nb::is_operator(), + R"%(Matrix multiplication of PauliOp with a PauliString on the left. + +Parameters +---------- +rhs : PauliOp + Left hand side PauliOp object + +Returns +------- +PauliOp + New PauliOp instance containing the result of the multiplication +)%") .def( "__mul__", [](fp::PauliOp const &self, cfloat_t rhs) { @@ -187,7 +438,19 @@ NB_MODULE(_fast_pauli, m) res_op.scale(rhs); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Scale Pauli Operator by a scalar value. + +Parameters +---------- +rhs : complex or float + Right hand side scalar multiplier + +Returns +------- +PauliOp + New PauliOp instance containing the result of the multiplication +)%") .def( "__rmul__", [](fp::PauliOp const &self, cfloat_t lhs) { @@ -195,14 +458,38 @@ NB_MODULE(_fast_pauli, m) res_op.scale(lhs); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Scale Pauli Operator by a scalar value. + +Parameters +---------- +lhs : complex or float + Left hand side scalar multiplier + +Returns +------- +PauliOp + New PauliOp instance containing the result of the multiplication +)%") .def( "__imul__", [](fp::PauliOp &self, cfloat_t factor) { self.scale(factor); return self; }, - nb::is_operator()) + nb::is_operator(), + R"%(Scale Pauli Operator inplace by a scalar value. + +Parameters +---------- +other : complex or float + Scalar multiplier + +Returns +------- +PauliOp + Current PauliOp instance after scaling +)%") .def( "__add__", [](fp::PauliOp const &lhs_op, fp::PauliOp const &rhs_op) { @@ -210,7 +497,19 @@ NB_MODULE(_fast_pauli, m) res_op.extend(rhs_op); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the sum of two Pauli Operators. + +Parameters +---------- +rhs : PauliOp + The other PauliOp object to add + +Returns +------- +PauliOp + New PauliOp instance holding the sum. +)%") .def( "__add__", [](fp::PauliOp const &lhs_op, fp::PauliString const &rhs_str) { @@ -218,7 +517,19 @@ NB_MODULE(_fast_pauli, m) res_op.extend(rhs_str, 1); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the sum of Pauli Operator with Pauli String. + +Parameters +---------- +rhs : PauliString + Right hand side PauliString object to add + +Returns +------- +PauliOp + New PauliOp instance holding the sum. +)%") .def( "__radd__", [](fp::PauliOp const &self, fp::PauliString const &lhs_str) { @@ -226,21 +537,57 @@ NB_MODULE(_fast_pauli, m) res_op.extend(lhs_str, 1); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the sum of Pauli Operators with Pauli String. + +Parameters +---------- +lhs : PauliString + Left hand side PauliString object to add + +Returns +------- +PauliOp + New PauliOp instance holding the sum. +)%") .def( "__iadd__", [](fp::PauliOp &self, fp::PauliOp const &other_op) { self.extend(other_op); return self; }, - nb::is_operator()) + nb::is_operator(), + R"%(Performs inplace addition with other Pauli Operator. + +Parameters +---------- +other : PauliOp + Pauli operator object to add + +Returns +------- +PauliOp + Current PauliOp instance after addition +)%") .def( "__iadd__", [](fp::PauliOp &self, fp::PauliString const &other_str) { self.extend(other_str, 1); return self; }, - nb::is_operator()) + nb::is_operator(), + R"%(Performs inplace addition with Pauli String. + +Parameters +---------- +other : PauliString + Pauli string object to add + +Returns +------- +PauliOp + Current PauliOp instance after addition +)%") .def( "__sub__", [](fp::PauliOp const &lhs_op, fp::PauliOp const &rhs_op) { @@ -248,7 +595,19 @@ NB_MODULE(_fast_pauli, m) res_op.extend(-rhs_op); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the difference of two Pauli Operators. + +Parameters +---------- +rhs : PauliOp + The other PauliOp object to subtract + +Returns +------- +PauliOp + New PauliOp instance holding the difference. +)%") .def( "__sub__", [](fp::PauliOp const &lhs_op, fp::PauliString const &rhs_str) { @@ -256,7 +615,19 @@ NB_MODULE(_fast_pauli, m) res_op.extend(rhs_str, -1); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the difference of Pauli Operator with Pauli String. + +Parameters +---------- +rhs : PauliString + Right hand side PauliString object to subtract + +Returns +------- +PauliOp + New PauliOp instance holding the difference. +)%") .def( "__rsub__", [](fp::PauliOp const &self, fp::PauliString const &lhs_str) { @@ -264,123 +635,251 @@ NB_MODULE(_fast_pauli, m) res_op.extend(lhs_str, 1); return res_op; }, - nb::is_operator()) + nb::is_operator(), + R"%(Returns the difference of Pauli Operators with Pauli String. + +Parameters +---------- +lhs : PauliString + Left hand side PauliString object to subtract + +Returns +------- +PauliOp + New PauliOp instance holding the difference. +)%") .def( "__isub__", [](fp::PauliOp &self, fp::PauliOp const &other_op) { self.extend(-other_op); return self; }, - nb::is_operator()) + nb::is_operator(), + R"%(Performs inplace subtraction with other Pauli Operator. + +Parameters +---------- +other : PauliOp + Pauli operator object to subtract + +Returns +------- +PauliOp + Current PauliOp instance after subtraction +)%") .def( "__isub__", [](fp::PauliOp &self, fp::PauliString const &other_str) { self.extend(other_str, -1); return self; }, - nb::is_operator()) + nb::is_operator(), + R"%(Performs inplace subtraction with Pauli String. + +Parameters +---------- +other : PauliString + Pauli string object to subtract + +Returns +------- +PauliOp + Current PauliOp instance after subtraction +)%") .def( "extend", [](fp::PauliOp &self, fp::PauliOp const &other) { self.extend(other); }, - "other"_a) + "other"_a, + R"%(Add another PauliOp to the current one by extending the internal summation with new terms. + +Parameters +---------- +other : PauliOp + PauliOp object to extend the current one with +)%") .def( "extend", [](fp::PauliOp &self, fp::PauliString const &other, cfloat_t multiplier, bool dedupe) { self.extend(other, multiplier, dedupe); }, - "other"_a, "multiplier"_a, "dedupe"_a = true) + "other"_a, "multiplier"_a, "dedupe"_a = true, + R"%(Add a Pauli String term with appropriate coefficient to the summation inside PauliOp. + +Parameters +---------- +other : PauliString + PauliString object to add to the summation +multiplier : complex + Coefficient to apply to the PauliString +dedupe : bool + Whether to deduplicate provided PauliString +)%") // Getters - .def_prop_ro("dim", &fp::PauliOp::dim) - .def_prop_ro("n_qubits", &fp::PauliOp::n_qubits) - .def_prop_ro("n_pauli_strings", &fp::PauliOp::n_pauli_strings) - // TODO these may dangerous, keep an eye on them if users start modifying - // internals - .def_prop_ro("coeffs", [](fp::PauliOp const &self) { return self.coeffs; }) - .def_prop_ro("pauli_strings", [](fp::PauliOp const &self) { return self.pauli_strings; }) - .def_prop_ro("pauli_strings_as_str", - [](fp::PauliOp const &self) { - // return self.pauli_strings; - std::vector strings(self.n_pauli_strings()); - std::transform(self.pauli_strings.begin(), self.pauli_strings.end(), strings.begin(), - [](fp::PauliString const &ps) { return fmt::format("{}", ps); }); - return strings; - }) + .def_prop_ro("dim", &fp::PauliOp::dim, + "int: The dimension of PauliStrings used to compose PauliOp :math:`2^n, n` - number of qubits") + .def_prop_ro("n_qubits", &fp::PauliOp::n_qubits, "int: The number of qubits in PauliOp") + .def_prop_ro("n_pauli_strings", &fp::PauliOp::n_pauli_strings, + "int: The number of PauliString terms in PauliOp") + // TODO these may dangerous, keep an eye on them if users start modifying internals + .def_prop_ro( + "coeffs", [](fp::PauliOp const &self) { return self.coeffs; }, + "List[complex]: Ordered list of coefficients corresponding to Pauli strings") + .def_prop_ro( + "pauli_strings", [](fp::PauliOp const &self) { return self.pauli_strings; }, + "List[PauliString]: Ordered list of PauliString objects in PauliOp") + .def_prop_ro( + "pauli_strings_as_str", + [](fp::PauliOp const &self) { + // return self.pauli_strings; + std::vector strings(self.n_pauli_strings()); + std::transform(self.pauli_strings.begin(), self.pauli_strings.end(), strings.begin(), + [](fp::PauliString const &ps) { return fmt::format("{}", ps); }); + return strings; + }, + "List[str]: Ordered list of Pauli Strings representations from PauliOp") // Methods .def( - "scale", [](fp::PauliOp &self, cfloat_t factor) { self.scale(factor); }, "factor"_a) + "scale", [](fp::PauliOp &self, cfloat_t factor) { self.scale(factor); }, "factor"_a, + R"%(Scale each individual term of Pauli Operator by a scalar value. + +Parameters +---------- +factor : complex or float + Scalar multiplier +)%") .def( "scale", [](fp::PauliOp &self, nb::ndarray factors) { auto factors_mdspan = fp::__detail::ndarray_to_mdspan(factors); self.scale(factors_mdspan); }, - "factors"_a) - .def("apply", - [](fp::PauliOp const &self, nb::ndarray states) { - if (states.ndim() == 1) - { - auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); - auto new_states = fp::__detail::owning_ndarray_like_mdspan(states_mdspan); - auto new_states_mdspan = std::mdspan(new_states.data(), new_states.size()); - self.apply(std::execution::par, new_states_mdspan, states_mdspan); - return new_states; - } - else if (states.ndim() == 2) - { - auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); - auto new_states = fp::__detail ::owning_ndarray_like_mdspan(states_mdspan); - auto new_states_mdspan = fp::__detail::ndarray_to_mdspan(new_states); - - self.apply(std::execution::par, new_states_mdspan, states_mdspan); - - return new_states; - } - else - { - throw std::invalid_argument( - fmt::format("apply: expected 1 or 2 dimensions, got {}", states.ndim())); - } - }) - .def("expectation_value", - [](fp::PauliOp const &self, nb::ndarray states) { - if (states.ndim() == 1) - { - auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); - auto states_mdspan_2d = std::mdspan(states_mdspan.data_handle(), states_mdspan.extent(0), 1); - std::array out_shape = {1}; - auto expected_vals_out = fp::__detail::owning_ndarray_from_shape(out_shape); - auto expected_vals_out_mdspan = fp::__detail::ndarray_to_mdspan(expected_vals_out); - self.expectation_value(std::execution::par, expected_vals_out_mdspan, states_mdspan_2d); - return expected_vals_out; - } - else if (states.ndim() == 2) - { - auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); - std::array out_shape = {states_mdspan.extent(1)}; - auto expected_vals_out = fp::__detail::owning_ndarray_from_shape(out_shape); - auto expected_vals_out_mdspan = fp::__detail::ndarray_to_mdspan(expected_vals_out); - - self.expectation_value(std::execution::par, expected_vals_out_mdspan, states_mdspan); - - return expected_vals_out; - } - else - { - throw std::invalid_argument( - fmt::format("expectation_value: expected 1 or 2 dimensions, got {}", states.ndim())); - } - }) - .def("to_tensor", - [](fp::PauliOp const &self) { - auto dense_op = fp::__detail::owning_ndarray_from_shape({self.dim(), self.dim()}); - self.to_tensor(fp::__detail::ndarray_to_mdspan(dense_op)); - return dense_op; - }) - // - ; + "factors"_a, + R"%(Scale each individual term of Pauli Operator by a scalar value. + +Parameters +---------- +factors : np.ndarray + Array of factors to scale each term with. The length of the array should match the number of Pauli strings in PauliOp +)%") + .def( + "apply", + [](fp::PauliOp const &self, nb::ndarray states) { + if (states.ndim() == 1) + { + auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); + auto new_states = fp::__detail::owning_ndarray_like_mdspan(states_mdspan); + auto new_states_mdspan = std::mdspan(new_states.data(), new_states.size()); + self.apply(std::execution::par, new_states_mdspan, states_mdspan); + return new_states; + } + else if (states.ndim() == 2) + { + auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); + auto new_states = fp::__detail ::owning_ndarray_like_mdspan(states_mdspan); + auto new_states_mdspan = fp::__detail::ndarray_to_mdspan(new_states); + + self.apply(std::execution::par, new_states_mdspan, states_mdspan); + + return new_states; + } + else + { + throw std::invalid_argument( + fmt::format("apply: expected 1 or 2 dimensions, got {}", states.ndim())); + } + }, + "states"_a, + R"%(Apply a Pauli Operator to a single dimensional state vector or a batch of states. + +.. math:: + \big( \sum_k h_k \mathcal{\hat{P}}_k \big) \psi_t + +.. note:: + For batch mode it applies the PauliOp to each individual state separately. + In this case, the input array is expected to have the shape of (n_dims, n_states) with states stored as columns. + +Parameters +---------- +states : np.ndarray + The original state(s) represented as 1D or 2D numpy array with states along the columns for batched application. + Outer dimension must match the dimansionality of Pauli Operator. + +Returns +------- +np.ndarray + New state(s) in a form of 1D or 2D numpy array corresponding to the shape of input states +)%") + .def( + "expectation_value", + [](fp::PauliOp const &self, nb::ndarray states) { + if (states.ndim() == 1) + { + auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); + auto states_mdspan_2d = std::mdspan(states_mdspan.data_handle(), states_mdspan.extent(0), 1); + std::array out_shape = {1}; + auto expected_vals_out = fp::__detail::owning_ndarray_from_shape(out_shape); + auto expected_vals_out_mdspan = fp::__detail::ndarray_to_mdspan(expected_vals_out); + self.expectation_value(std::execution::par, expected_vals_out_mdspan, states_mdspan_2d); + return expected_vals_out; + } + else if (states.ndim() == 2) + { + auto states_mdspan = fp::__detail::ndarray_to_mdspan(states); + std::array out_shape = {states_mdspan.extent(1)}; + auto expected_vals_out = fp::__detail::owning_ndarray_from_shape(out_shape); + auto expected_vals_out_mdspan = fp::__detail::ndarray_to_mdspan(expected_vals_out); + + self.expectation_value(std::execution::par, expected_vals_out_mdspan, states_mdspan); + + return expected_vals_out; + } + else + { + throw std::invalid_argument( + fmt::format("expectation_value: expected 1 or 2 dimensions, got {}", states.ndim())); + } + }, + "states"_a, + R"%(Calculate expectation value(s) for a given single dimensional state vector or a batch of states. + +.. math:: + \bra{\psi_t} \big( \sum_k h_{k} \mathcal{\hat{P}}_k \big) \ket{\psi_t} + +.. note:: + For batch mode it computes the expectation value for each individual state separately. + In this case, the input array is expected to have the shape of (n_dims, n_states) with states stored as columns. + +Parameters +---------- +states : np.ndarray + The original state(s) represented as 1D or 2D numpy array with states along the columns for batched application. + Outer dimension must match the dimansionality of Pauli Operator. + +Returns +------- +np.ndarray + Expectation value(s) in a form of 1D numpy array corresponding to the number of states +)%") + .def( + "to_tensor", + [](fp::PauliOp const &self) { + auto dense_op = fp::__detail::owning_ndarray_from_shape({self.dim(), self.dim()}); + self.to_tensor(fp::__detail::ndarray_to_mdspan(dense_op)); + return dense_op; + }, + R"%(Returns a dense representation of PauliOp. + +Returns +------- +np.ndarray + 2D numpy array of complex numbers + )%"); // + // + // + nb::class_>(m, "SummedPauliOp") // Constructors // See