diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 7ba22a034..070b7bb76 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -2,6 +2,9 @@

New features since last release

+* The PennyLane plugin now has the option to print out instructions in the null device + [(#1316)](https://github.com/PennyLaneAI/catalyst/pull/1346) +

Improvements 🛠

* Replace pybind11 with nanobind for C++/Python bindings in the frontend and in the runtime. @@ -113,4 +116,5 @@ Mehrdad Malekmohammadi, William Maxwell Romain Moyard, Raul Torres, -Paul Haochen Wang. +Paul Haochen Wang, +Stefanie Muroya Lei. diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py index 421bd3f1e..6b1b914ac 100644 --- a/frontend/catalyst/device/qjit_device.py +++ b/frontend/catalyst/device/qjit_device.py @@ -303,7 +303,7 @@ def extract_backend_info(device, capabilities: DeviceCapabilities) -> BackendInf return extract_backend_info(device, capabilities) @debug_logger_init - def __init__(self, original_device): + def __init__(self, original_device, print_instructions=False): self.original_device = original_device for key, value in original_device.__dict__.items(): @@ -331,6 +331,11 @@ def __init__(self, original_device): self.backend_name = backend.c_interface_name self.backend_lib = backend.lpath self.backend_kwargs = backend.kwargs + + if original_device.name == "null.qubit": + # include 'print_instructions' as a keyword argument for the device constructor. + self.backend_kwargs["print_instructions"] = print_instructions + self.capabilities = get_qjit_device_capabilities(device_capabilities) @debug_logger diff --git a/frontend/catalyst/jit.py b/frontend/catalyst/jit.py index f040f01b7..9f3c4e58d 100644 --- a/frontend/catalyst/jit.py +++ b/frontend/catalyst/jit.py @@ -87,6 +87,7 @@ def qjit( seed=None, experimental_capture=False, circuit_transform_pipeline=None, + print_instructions=False ): # pylint: disable=too-many-arguments,unused-argument """A just-in-time decorator for PennyLane and JAX programs using Catalyst. @@ -153,6 +154,8 @@ def qjit( dictionaries of valid keyword arguments and values for the specific pass. The order of keys in this dictionary will determine the pass pipeline. If not specified, the default pass pipeline will be applied. + print_instructions (Optional[bool]): + If set to True, instructions are printed when executing in the null device. Returns: QJIT object. @@ -430,11 +433,12 @@ def sum_abstracted(arr): """ kwargs = copy.copy(locals()) kwargs.pop("fn") + kwargs.pop("print_instructions") if fn is None: return functools.partial(qjit, **kwargs) - return QJIT(fn, CompileOptions(**kwargs)) + return QJIT(fn, CompileOptions(**kwargs), print_instructions=print_instructions) ## IMPL ## @@ -452,6 +456,8 @@ class QJIT(CatalystCallable): Args: fn (Callable): the quantum or classical function to compile compile_options (CompileOptions): compilation options to use + print_instructions (Optional[bool]): If True, prints instructions + when executing in a null device. :ivar original_function: This attribute stores `fn`, the quantum or classical function object to compile, as is, without any modifications @@ -462,7 +468,10 @@ class QJIT(CatalystCallable): """ @debug_logger_init - def __init__(self, fn, compile_options): + def __init__(self, fn, compile_options, print_instructions=False): + # flag for printing instructions in the null device + self.print_instructions = print_instructions + functools.update_wrapper(self, fn) self.original_function = fn self.compile_options = compile_options @@ -635,6 +644,10 @@ def closure(qnode, *args, **kwargs): params = {} params["static_argnums"] = kwargs.pop("static_argnums", static_argnums) params["_out_tree_expected"] = [] + + if qnode.device.name == "null.qubit": + kwargs["print_instructions"] = self.print_instructions + return QFunc.__call__( qnode, pass_pipeline=self.compile_options.circuit_transform_pipeline, diff --git a/frontend/catalyst/qfunc.py b/frontend/catalyst/qfunc.py index d0f41fe01..296318da0 100644 --- a/frontend/catalyst/qfunc.py +++ b/frontend/catalyst/qfunc.py @@ -122,7 +122,10 @@ def __call__(self, *args, **kwargs): mcm_config.postselect_mode = mcm_config.postselect_mode or "hw-like" return Function(dynamic_one_shot(self, mcm_config=mcm_config))(*args, **kwargs) - qjit_device = QJITDevice(self.device) + # retrieve the flag to print instructions, used for executing + # pre-compiled programs in a null device. + print_instructions = kwargs.pop("print_instructions", False) + qjit_device = QJITDevice(self.device, print_instructions) static_argnums = kwargs.pop("static_argnums", ()) out_tree_expected = kwargs.pop("_out_tree_expected", []) diff --git a/runtime/lib/backend/null_qubit/InstructionStrBuilder.hpp b/runtime/lib/backend/null_qubit/InstructionStrBuilder.hpp new file mode 100644 index 000000000..e58d16644 --- /dev/null +++ b/runtime/lib/backend/null_qubit/InstructionStrBuilder.hpp @@ -0,0 +1,329 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "DataView.hpp" +#include "Types.h" + +namespace Catalyst::Runtime { +using namespace std; + +// string representation of observables +static const unordered_map obs_id_to_str = { + {ObsId::Identity, "Identity"}, {ObsId::PauliX, "PauliX"}, {ObsId::PauliY, "PauliY"}, + {ObsId::PauliZ, "PauliZ"}, {ObsId::Hadamard, "Hadamard"}, {ObsId::Hermitian, "Hermitian"}, +}; + +/** + * InstructionStrBuilder + * + * @brief This class is used by the null device (NullQubit.hpp) whenever the flag to print + * instructions (print_instructions) is set to true. It is in charge of building string + * representations of the operations invoked in the aforementioned device interface. + */ +class InstructionStrBuilder { + private: + unordered_map + obs_id_type_to_str{}; // whenever a new observable is created we store the corresponding + // string representation in this hashmap + + /** + * @brief Template function that returns the string representation of some object. + */ + template string element_to_str(const T &e) { return to_string(e); } + + /** + * @brief Template specialized to return the string representation of complex numbers. + */ + template string element_to_str(const complex &c) + { + ostringstream oss; + if (c.real() == 0 && c.imag() == 0) { + oss << 0; + return oss.str(); + } + + bool is_first = true; // keep track in which term we are so we can add '+' or '-' + // appropriately + + // to keep printing output as short as possible, we only print non-zero coefficients + if (c.real() != 0) { + oss << c.real(); + is_first = false; + } + + if (c.imag() != 0) { + if (!is_first) { + if (c.imag() > 0) { + oss << " + " << c.imag() << "i"; + } + else { + oss << " - " << -1 * c.imag() << "i"; + } + } + else { + oss << c.imag() << "i"; + } + } + + return oss.str(); + } + + /** + * @brief Takes as input a vector and returns its string representation. If with_brackets=True, + * the square brackets will be included to enclose the vector. E.g. + * - vector_to_string([0,1,2], false) returns "0, 1, 2" + * - vector_to_string([0,1,2], true) returns "[0, 1, 2]" + */ + template + string vector_to_string(const vector &v, const bool &with_brackets = true) + { + ostringstream oss; + if (with_brackets) + oss << "["; + + for (size_t i = 0; i < v.size(); i++) { + if (i > 0) { + oss << ", "; + } + oss << element_to_str(v[i]); + } + if (with_brackets) + oss << "]"; + + return oss.str(); + } + + public: + InstructionStrBuilder() = default; + ~InstructionStrBuilder() = default; + + /** + * @brief This method is used to build a string representation of AllocateQubit(), + * AllocateQubits(), ReleaseQubit(), ReleaseAllQubits(), State(), Probs(), Measure() + */ + template string get_simple_op_str(const string &name, const T ¶m) + { + ostringstream oss; + oss << name << "(" << param << ")"; + return oss.str(); + } + + /** + * @brief This method is used to build a string representation of Expval() and Var() + */ + string get_op_with_obs_str(const string &name, const ObsIdType &o) + { + ostringstream oss; + oss << name << "(" << get_obs_str(o) << ")"; + return oss.str(); + } + + /** + * @brief This method is used to get the string representation of NamedOperation(), + * PartialProbs() + */ + string get_named_op_str(const std::string &name, const std::vector ¶ms, + const std::vector &wires, bool inverse = false, + const std::vector &controlled_wires = {}, + const std::vector &controlled_values = {}, + const bool &explicit_wires = true) + { + ostringstream oss; + vector values_to_print; // Store the string representation of the parameters passed + // NamedOperation(). We only consider non-empty vectors to + // preserve printing-output simplicity. + + for (auto p : params) + values_to_print.push_back(std::to_string(p)); + + if (wires.size() > 0) { + if (explicit_wires) { + values_to_print.push_back("wires=" + vector_to_string(wires, true)); + } + else { + values_to_print.push_back(vector_to_string(wires, false)); + } + } + + // if inverse is false, we will not print its value + if (inverse) + values_to_print.push_back("inverse=true"); + + if (controlled_wires.size() > 0) + values_to_print.push_back("control=" + vector_to_string(controlled_wires)); + + if (controlled_values.size() > 0) + values_to_print.push_back("control_value=" + vector_to_string(controlled_values)); + + oss << name << "("; + for (auto i = 0; i < values_to_print.size(); i++) { + if (i > 0) { + oss << ", "; + } + oss << values_to_print[i]; + } + oss << ")"; + return oss.str(); + } + + /** + * @brief This method is used to get the string representation of MatrixOperation() + */ + string get_matrix_op_str(const std::vector> &matrix, + const std::vector &wires, bool inverse = false, + const std::vector &controlled_wires = {}, + const std::vector &controlled_values = {}, + const string &name = "MatrixOperation") + { + ostringstream oss; + vector values_to_print; // Store the string representation of the parameters passed + // NamedOperation(). We only consider non-empty vectors to + // preserve printing-output simplicity. + + values_to_print.emplace_back(vector_to_string(matrix)); + + if (wires.size() > 0) + values_to_print.emplace_back("wires=" + vector_to_string(wires)); + + if (inverse) + values_to_print.push_back("inverse=true"); + + if (controlled_wires.size() > 0) + values_to_print.push_back("control=" + vector_to_string(controlled_wires)); + + if (controlled_values.size() > 0) + values_to_print.push_back("control_value=" + vector_to_string(controlled_values)); + + oss << name << "("; + for (auto i = 0; i < values_to_print.size(); i++) { + if (i > 0) { + oss << ", "; + } + oss << values_to_print[i]; + } + + oss << ")"; + return oss.str(); + } + + /** + * @brief Every time Observable() is invoked in the null device interface, we invoke this + * function to create a new ObsIdType and its corresponding string representation. + */ + ObsIdType create_obs_str(ObsId obs_id, const std::vector> &matrix, + const std::vector &wires) + { + ObsIdType new_id = obs_id_type_to_str.size(); + + if (obs_id == ObsId::Hermitian) { + obs_id_type_to_str.emplace( + new_id, get_matrix_op_str(matrix, wires, false, {}, {}, "Hermitian")); + } + else { + auto it = obs_id_to_str.find(obs_id); + if (it != obs_id_to_str.end()) { + obs_id_type_to_str.emplace( + new_id, get_named_op_str(it->second, {}, wires, false, {}, {}, false)); + } + else { + RT_FAIL( + ("please check obs_id_to_str in file InstructionPrinter. Observation with ID" + + to_string(obs_id) + "is not recognized.") + .c_str()); + } + } + + return new_id; + } + + /** + * @brief Every time TensorObservable() is invoked in the null device interface, we invoke this + * function to create a new ObsIdType and its corresponding string representation. + */ + ObsIdType create_tensor_obs_str(const std::vector &obs_keys) + { + ostringstream oss; + for (auto i = 0; i < obs_keys.size(); i++) { + if (i > 0) { + oss << " ⊗ "; + } + oss << obs_id_type_to_str[obs_keys[i]]; + } + ObsIdType new_id = obs_id_type_to_str.size(); + obs_id_type_to_str[new_id] = oss.str(); + return new_id; + } + + /** + * @brief Every time HamiltonianObservable() is invoked in the null device interface, we invoke + * this function to create a new ObsIdType and its corresponding string representation. + */ + ObsIdType create_hamiltonian_obs_str(const std::vector &coeffs, + const std::vector &obs_keys) + { + RT_FAIL_IF(coeffs.size() != obs_keys.size(), + "number of coefficients should match the number of observables"); + + ostringstream oss; + + bool is_first = true; + for (auto i = 0; i < coeffs.size(); i++) { + if (!is_first) { + + // handle the addition of the terms + if (coeffs[i] > 0) { + oss << " + " << coeffs[i]; + } + else if (coeffs[i] < 0) { + oss << " - " << -1 * coeffs[i]; // a negative sign is manually added so we + // multiply the coefficient by -1 + } + } + + if (coeffs[i] != 0) { + if (is_first) + oss << coeffs[i]; // if is the first element then this coefficient is not yet + // added to the string + oss << "*" << obs_id_type_to_str[obs_keys[i]]; + is_first = false; + } + } + + ObsIdType new_id = obs_id_type_to_str.size(); + obs_id_type_to_str[new_id] = oss.str(); + return new_id; + } + + /** + * @brief Getter function to retrieve the string representation of the observables we created. + */ + string get_obs_str(const ObsIdType &o) { return obs_id_type_to_str.at(o); } + + /** + * @brief This method is used to get the string representation of Sample(), PartialSample(), + * Counts() and PartialCounts(). + */ + string get_distribution_op_str(const string &name, const size_t &shots, + const vector &wires = {}) + { + ostringstream oss; + bool is_first = true; + oss << name << "("; + + oss << "shots=" << shots; + + if (wires.size() > 0) { + oss << ", wires=" << vector_to_string(wires); + } + + oss << ")"; + + return oss.str(); + } +}; +} // namespace Catalyst::Runtime diff --git a/runtime/lib/backend/null_qubit/NullQubit.hpp b/runtime/lib/backend/null_qubit/NullQubit.hpp index f5ea09c54..8db79e7be 100644 --- a/runtime/lib/backend/null_qubit/NullQubit.hpp +++ b/runtime/lib/backend/null_qubit/NullQubit.hpp @@ -16,12 +16,14 @@ #include // generate_n #include +#include #include #include #include #include #include "DataView.hpp" +#include "InstructionStrBuilder.hpp" #include "QuantumDevice.hpp" #include "QubitManager.hpp" #include "Types.h" @@ -40,7 +42,12 @@ namespace Catalyst::Runtime::Devices { * of the device; these are used to implement Quantum Instruction Set (QIS) instructions. */ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { - NullQubit(const std::string &kwargs = "{}") {} + NullQubit(const std::string &kwargs = "{}") + { + std::unordered_map device_kwargs = + Catalyst::Runtime::parse_kwargs(kwargs); + print_instructions = device_kwargs["print_instructions"] == "True" ? 1 : 0; + } ~NullQubit() = default; // LCOV_EXCL_LINE NullQubit &operator=(const NullQubit &) = delete; @@ -55,6 +62,11 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { */ auto AllocateQubit() -> QubitIdType { + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("AllocateQubit", "") + << std::endl; + } + num_qubits_++; // next_id return this->qubit_manager.Allocate(num_qubits_); } @@ -68,11 +80,21 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { */ auto AllocateQubits(size_t num_qubits) -> std::vector { + bool prev_print_instructions = print_instructions; + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("AllocateQubits", num_qubits) + << std::endl; + } + print_instructions = false; // set to false so we do not print instructions for each call to + // AllocateQubit() below. + if (!num_qubits) { return {}; } std::vector result(num_qubits); std::generate_n(result.begin(), num_qubits, [this]() { return AllocateQubit(); }); + + print_instructions = prev_print_instructions; // restore actual value of print_instructions return result; } @@ -81,6 +103,10 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { */ void ReleaseQubit(QubitIdType q) { + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("ReleaseQubit", q) << std::endl; + } + if (!num_qubits_) { num_qubits_--; this->qubit_manager.Release(q); @@ -92,6 +118,11 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { */ void ReleaseAllQubits() { + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("ReleaseAllQubits", "") + << std::endl; + } + num_qubits_ = 0; this->qubit_manager.ReleaseAll(); } @@ -189,17 +220,27 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { const std::vector &controlled_wires = {}, const std::vector &controlled_values = {}) { + if (print_instructions) { + std::cout << instruction_str_builder.get_named_op_str( + name, params, wires, inverse, controlled_wires, controlled_values) + << std::endl; + } } /** * @brief Doesn't Apply a given matrix directly to the state vector of a device. * */ - void MatrixOperation(const std::vector> &, - const std::vector &, bool, + void MatrixOperation(const std::vector> &matrix, + const std::vector &wires, bool inverse, const std::vector &controlled_wires = {}, const std::vector &controlled_values = {}) { + if (print_instructions) { + std::cout << instruction_str_builder.get_matrix_op_str( + matrix, wires, inverse, controlled_wires, controlled_values) + << std::endl; + } } /** @@ -208,10 +249,14 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { * * @return `ObsIdType` Index of the constructed observable */ - auto Observable(ObsId, const std::vector> &, - const std::vector &) -> ObsIdType + auto Observable(ObsId obs_id, const std::vector> &matrix, + const std::vector &wires) -> ObsIdType { - return 0.0; + if (print_instructions) { + return instruction_str_builder.create_obs_str(obs_id, matrix, wires); + } + + return 0; } /** @@ -219,17 +264,28 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { * * @return `ObsIdType` Index of the constructed observable */ - auto TensorObservable(const std::vector &) -> ObsIdType { return 0.0; } + auto TensorObservable(const std::vector &obs_keys) -> ObsIdType + { + if (print_instructions) { + return instruction_str_builder.create_tensor_obs_str(obs_keys); + } + + return 0; + } /** * @brief Doesn't Construct a Hamiltonian observable. * * @return `ObsIdType` Index of the constructed observable */ - auto HamiltonianObservable(const std::vector &, const std::vector &) - -> ObsIdType + auto HamiltonianObservable(const std::vector &matrix, + const std::vector &obs_keys) -> ObsIdType { - return 0.0; + if (print_instructions) { + return instruction_str_builder.create_hamiltonian_obs_str(matrix, obs_keys); + } + + return 0; } /** @@ -237,35 +293,69 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { * * @return `double` The expected value */ - auto Expval(ObsIdType) -> double { return 0.0; } + auto Expval(ObsIdType o) -> double + { + if (print_instructions) { + std::cout << instruction_str_builder.get_op_with_obs_str("Expval", o) << std::endl; + } + return 0.0; + } /** * @brief Doesn't Compute the variance of an observable. * * @return `double` The variance */ - auto Var(ObsIdType) -> double { return 0.0; } + auto Var(ObsIdType o) -> double + { + if (print_instructions) { + std::cout << instruction_str_builder.get_op_with_obs_str("Var", o) << std::endl; + } + return 0.0; + } /** * @brief Doesn't Get the state-vector of a device. */ - void State(DataView, 1> &) {} + void State(DataView, 1> &state) + { + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("State", "") << std::endl; + } + } /** * @brief Doesn't Compute the probabilities of each computational basis state. */ - void Probs(DataView &) {} + void Probs(DataView &probs) + { + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("Probs", "") << std::endl; + } + } /** * @brief Doesn't Compute the probabilities for a subset of the full system. */ - void PartialProbs(DataView &, const std::vector &) {} + void PartialProbs(DataView &probs, const std::vector &wires) + { + if (print_instructions) { + std::cout << instruction_str_builder.get_named_op_str("PartialProbs", {}, wires) + << std::endl; + } + } /** * @brief Doesn't Compute samples with the number of shots on the entire wires, * returing raw samples. */ - void Sample(DataView &, size_t) {} + void Sample(DataView &samples, size_t shots) + { + if (print_instructions) { + std::cout << instruction_str_builder.get_distribution_op_str("Sample", shots) + << std::endl; + } + } /** * @brief Doesn't Compute partial samples with the number of shots on `wires`, @@ -275,21 +365,40 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { * shape `shots * numWires`. The built-in iterator in `DataView` * iterates over all elements of `samples` row-wise. */ - void PartialSample(DataView &, const std::vector &, size_t) {} + void PartialSample(DataView &samples, const std::vector &wires, + size_t shots) + { + if (print_instructions) { + std::cout << instruction_str_builder.get_distribution_op_str("PartialSample", shots, + wires) + << std::endl; + } + } /** * @brief Doesn't Sample with the number of shots on the entire wires, returning the * number of counts for each sample. */ - void Counts(DataView &, DataView &, size_t) {} + void Counts(DataView &eigen_vals, DataView &counts, size_t shots) + { + if (print_instructions) { + std::cout << instruction_str_builder.get_distribution_op_str("Counts", shots) + << std::endl; + } + } /** * @brief Doesn't Partial sample with the number of shots on `wires`, returning the * number of counts for each sample. */ - void PartialCounts(DataView &, DataView &, - const std::vector &, size_t) + void PartialCounts(DataView &eigen_vals, DataView &counts, + const std::vector &wires, size_t shots) { + if (print_instructions) { + std::cout << instruction_str_builder.get_distribution_op_str("PartialCounts", shots, + wires) + << std::endl; + } } /** @@ -297,8 +406,12 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { * * @return `Result` The measurement result */ - auto Measure(QubitIdType, std::optional) -> Result + auto Measure(QubitIdType q, std::optional postselect) -> Result { + if (print_instructions) { + std::cout << instruction_str_builder.get_simple_op_str("Measure", q) << std::endl; + } + bool *ret = (bool *)malloc(sizeof(bool)); *ret = true; return ret; @@ -318,7 +431,9 @@ struct NullQubit final : public Catalyst::Runtime::QuantumDevice { } private: + bool print_instructions; std::size_t num_qubits_{0}; Catalyst::Runtime::QubitManager qubit_manager{}; + Catalyst::Runtime::InstructionStrBuilder instruction_str_builder{}; }; } // namespace Catalyst::Runtime::Devices diff --git a/runtime/tests/CMakeLists.txt b/runtime/tests/CMakeLists.txt index 9924ea26a..75155e516 100644 --- a/runtime/tests/CMakeLists.txt +++ b/runtime/tests/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries(runner_tests_qir_runtime PRIVATE target_sources(runner_tests_qir_runtime PRIVATE Test_NullQubit.cpp + Test_InstructionStrBuilder.cpp ) catch_discover_tests(runner_tests_qir_runtime) diff --git a/runtime/tests/Test_InstructionStrBuilder.cpp b/runtime/tests/Test_InstructionStrBuilder.cpp new file mode 100644 index 000000000..1a0c94eec --- /dev/null +++ b/runtime/tests/Test_InstructionStrBuilder.cpp @@ -0,0 +1,104 @@ +#include "InstructionStrBuilder.hpp" +#include "TestUtils.hpp" + +TEST_CASE("string building is correct for instructions that require up to one parameter ", + "[InstructionStrBuilder]") +{ + InstructionStrBuilder str_builder; + CHECK(str_builder.get_simple_op_str("AllocateQubit", "") == "AllocateQubit()"); + CHECK(str_builder.get_simple_op_str("AllocateQubits", 5) == "AllocateQubits(5)"); + CHECK(str_builder.get_simple_op_str("ReleaseQubit", "") == "ReleaseQubit()"); + CHECK(str_builder.get_simple_op_str("ReleaseAllQubits", "") == "ReleaseAllQubits()"); + CHECK(str_builder.get_simple_op_str("State", "") == "State()"); + CHECK(str_builder.get_simple_op_str("Probs", "") == "Probs()"); + CHECK(str_builder.get_simple_op_str("Measure", 0) == "Measure(0)"); +} + +TEST_CASE("string building is correct for instructions that require a parameter of type ObsIdType", + "[InstructionStrBuilder]") +{ + InstructionStrBuilder str_builder; + auto id = str_builder.create_obs_str(ObsId::PauliX, {}, {0}); + + CHECK(str_builder.get_op_with_obs_str("Expval", id) == "Expval(PauliX(0))"); + CHECK(str_builder.get_op_with_obs_str("Var", id) == "Var(PauliX(0))"); +} + +TEST_CASE("string building is correct for NamedOperation", "[InstructionStrBuilder]") +{ + InstructionStrBuilder str_builder; + + CHECK(str_builder.get_named_op_str("NamedOperation", {3.14, 0.705}, {}) == + "NamedOperation(3.140000, 0.705000)"); + CHECK(str_builder.get_named_op_str("NamedOperation", {3.14, 0.705}, {0}) == + "NamedOperation(3.140000, 0.705000, wires=[0])"); + CHECK(str_builder.get_named_op_str("NamedOperation", {}, {0}) == "NamedOperation(wires=[0])"); + CHECK(str_builder.get_named_op_str("NamedOperation", {}, {0}, true) == + "NamedOperation(wires=[0], inverse=true)"); + CHECK(str_builder.get_named_op_str("NamedOperation", {}, {1}, false, {0}) == + "NamedOperation(wires=[1], control=[0])"); + CHECK(str_builder.get_named_op_str("NamedOperation", {}, {1}, false, {0}, {true}) == + "NamedOperation(wires=[1], control=[0], control_value=[1])"); + CHECK(str_builder.get_named_op_str("PartialProbs", {}, {0}) == "PartialProbs(wires=[0])"); +} + +TEST_CASE("string building is correct for MatrixOperation", "[InstructionStrBuilder]") +{ + InstructionStrBuilder str_builder; + std::vector> v = { + std::complex(0.707, -0.707), std::complex(0.0, 0.707), + std::complex(0, -0.707), std::complex(1.0), + std::complex(-1.0), std::complex(0), + std::complex(0.707, 0.707), std::complex(-0.707, 0.707)}; + + CHECK(str_builder.get_matrix_op_str(v, {}) == + "MatrixOperation([0.707 - 0.707i, 0.707i, -0.707i, 1, -1, 0, 0.707 + 0.707i, -0.707 + " + "0.707i])"); + CHECK(str_builder.get_matrix_op_str(v, {0}) == + "MatrixOperation([0.707 - 0.707i, 0.707i, -0.707i, 1, -1, 0, 0.707 + 0.707i, -0.707 + " + "0.707i], wires=[0])"); + CHECK(str_builder.get_matrix_op_str(v, {0}, true) == + "MatrixOperation([0.707 - 0.707i, 0.707i, -0.707i, 1, -1, 0, 0.707 + 0.707i, -0.707 + " + "0.707i], wires=[0], inverse=true)"); + CHECK(str_builder.get_matrix_op_str(v, {1}, false, {0}) == + "MatrixOperation([0.707 - 0.707i, 0.707i, -0.707i, 1, -1, 0, 0.707 + 0.707i, -0.707 + " + "0.707i], wires=[1], control=[0])"); + CHECK(str_builder.get_matrix_op_str(v, {1}, false, {0}, {true}) == + "MatrixOperation([0.707 - 0.707i, 0.707i, -0.707i, 1, -1, 0, 0.707 + 0.707i, -0.707 + " + "0.707i], wires=[1], control=[0], control_value=[1])"); +} + +TEST_CASE("registers correctly a new observable", "[InstructionStrBuilder]") +{ + InstructionStrBuilder str_builder; + + ObsId obs_id1 = ObsId::PauliX; + ObsId obs_id2 = ObsId::Hermitian; + + CHECK(str_builder.create_obs_str(obs_id1, {}, {0}) == 0); + CHECK(str_builder.create_obs_str(obs_id2, {1, 0, 0, 1}, {0}) == 1); + CHECK(str_builder.get_obs_str(0) == "PauliX(0)"); + CHECK(str_builder.get_obs_str(1) == "Hermitian([1, 0, 0, 1], wires=[0])"); + + // test tensor product observable + CHECK(str_builder.create_tensor_obs_str({0, 1}) == 2); + CHECK(str_builder.get_obs_str(2) == "PauliX(0) ⊗ Hermitian([1, 0, 0, 1], wires=[0])"); + + // test hamiltonian observable + CHECK(str_builder.create_hamiltonian_obs_str({0.1, 0.2}, {0, 1}) == 3); + CHECK(str_builder.get_obs_str(3) == "0.1*PauliX(0) + 0.2*Hermitian([1, 0, 0, 1], wires=[0])"); + + CHECK(str_builder.create_hamiltonian_obs_str({0.0, 0.2}, {0, 1}) == 4); + CHECK(str_builder.get_obs_str(4) == "0.2*Hermitian([1, 0, 0, 1], wires=[0])"); + + CHECK(str_builder.create_hamiltonian_obs_str({0.1, -0.2}, {0, 1}) == 5); + CHECK(str_builder.get_obs_str(5) == "0.1*PauliX(0) - 0.2*Hermitian([1, 0, 0, 1], wires=[0])"); +} + +TEST_CASE("string building for Counts() and PartialCounts()", "[InstructionStrBuilder]") +{ + InstructionStrBuilder str_builder; + CHECK(str_builder.get_distribution_op_str("Counts", 100) == "Counts(shots=100)"); + CHECK(str_builder.get_distribution_op_str("PartialCounts", 100, {0}) == + "PartialCounts(shots=100, wires=[0])"); +}