pyrauli 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyrauli-0.3.2 → pyrauli-0.4.0}/CMakeLists.txt +2 -1
- {pyrauli-0.3.2 → pyrauli-0.4.0}/PKG-INFO +4 -1
- pyrauli-0.4.0/benchmarks/test_benchmark_qaoa.py +43 -0
- pyrauli-0.4.0/docs/guides/how_to_symbolic.rst +99 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/index.rst +1 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/pyproject.toml +5 -1
- {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/__init__.py +17 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/_core/bindings.cpp +333 -7
- pyrauli-0.4.0/tests/snippets/test_symbolic_circuit_noise.py +22 -0
- pyrauli-0.4.0/tests/snippets/test_symbolic_circuit_snippet.py +19 -0
- pyrauli-0.4.0/tests/snippets/test_symbolic_coefficient_guide.py +53 -0
- pyrauli-0.4.0/tests/snippets/test_symbolic_observable_guide.py +46 -0
- pyrauli-0.4.0/tests/snippets/test_symbolic_sympy.py +24 -0
- pyrauli-0.4.0/tests/test_symbolic.py +12 -0
- pyrauli-0.4.0/tests/test_symbolic_circuit.py +61 -0
- pyrauli-0.4.0/tests/test_symbolic_coefficient.py +76 -0
- pyrauli-0.4.0/tests/test_symbolic_observable.py +88 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/.clang-format +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/benchmark.yml +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/ci.yml +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/doc.yml +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/pypi.yml +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/.gitignore +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/LICENSE +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/README.md +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/benchmarks/test_benchmark_circuit.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/benchmarks/test_benchmark_observable.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/benchmarks/test_benchmark_qiskit.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/.gitignore +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/Makefile +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/_static/.gitkeep +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/_templates/.gitkeep +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/conf.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/explanation/theory.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_circuit.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_complexity.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_noise.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_observables.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_qiskit.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/make.bat +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/reference/api.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/requirements.txt +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/tutorials/getting_started.rst +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/examples/qiskit_backend.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/backend.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/converters.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/estimator.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_basic_circuit.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_observable_evolution.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_qiskit_backend_usage.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_readme.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_backend.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_circuit.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_circuit_qiskit.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_noise_model.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_observable.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_observable_qiskit.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_pauli.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_policies.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_pyrauli.py +0 -0
- {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_truncator.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: pyrauli
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.0
|
4
4
|
Summary: A very fast and easy to use Quantum circuit simulator relying on Pauli propagation. Compatible with qiskit.
|
5
5
|
License: GNU GENERAL PUBLIC LICENSE
|
6
6
|
Version 3, 29 June 2007
|
@@ -687,11 +687,14 @@ Requires-Python: >=3.9
|
|
687
687
|
Provides-Extra: qiskit
|
688
688
|
Requires-Dist: qiskit>=2.0.0; extra == "qiskit"
|
689
689
|
Requires-Dist: numpy; extra == "qiskit"
|
690
|
+
Provides-Extra: symbolic
|
691
|
+
Requires-Dist: sympy; extra == "symbolic"
|
690
692
|
Provides-Extra: test
|
691
693
|
Requires-Dist: pytest>=8.0.0; extra == "test"
|
692
694
|
Requires-Dist: pytest-cov>=5.0.0; extra == "test"
|
693
695
|
Requires-Dist: pytest-benchmark; extra == "test"
|
694
696
|
Requires-Dist: numpy; extra == "test"
|
697
|
+
Requires-Dist: sympy; extra == "test"
|
695
698
|
Description-Content-Type: text/markdown
|
696
699
|
|
697
700
|
# pyrauli: High-Performance Quantum Circuit Simulation
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import pytest
|
2
|
+
from pyrauli import SymbolicObservable, SymbolicCircuit
|
3
|
+
|
4
|
+
def rzz(qc, q1, q2, v):
|
5
|
+
qc.add_operation("cx", q1, q2)
|
6
|
+
qc.add_operation("rz", q2, v)
|
7
|
+
qc.add_operation("cx", q1, q2)
|
8
|
+
|
9
|
+
def rx(qc, q, v):
|
10
|
+
qc.add_operation("h", q)
|
11
|
+
qc.add_operation("rz", q, v)
|
12
|
+
qc.add_operation("h", q)
|
13
|
+
|
14
|
+
@pytest.fixture(scope="module")
|
15
|
+
def maxcut_qaoa_N4P1():
|
16
|
+
"""Fixture"""
|
17
|
+
obs = SymbolicObservable(["ZZII", "ZIZI", "IZZI", "ZIIZ", "IZIZ", "IIZZ"])
|
18
|
+
qc = SymbolicCircuit(4)
|
19
|
+
|
20
|
+
for i in range(4):
|
21
|
+
qc.add_operation("h", i)
|
22
|
+
|
23
|
+
rzz(qc, 0, 1, "tz")
|
24
|
+
rzz(qc, 0, 3, "tz")
|
25
|
+
rzz(qc, 0, 2, "tz")
|
26
|
+
|
27
|
+
rx(qc, 0, "tx")
|
28
|
+
|
29
|
+
rzz(qc, 1, 2, "tz")
|
30
|
+
rzz(qc, 1, 3, "tz")
|
31
|
+
|
32
|
+
rx(qc, 1, "tx")
|
33
|
+
rzz(qc, 2, 3, "tz")
|
34
|
+
|
35
|
+
rx(qc, 2, "tx")
|
36
|
+
rx(qc, 3, "tx")
|
37
|
+
|
38
|
+
return qc, obs
|
39
|
+
|
40
|
+
def test_qaoa_N4P1_run(maxcut_qaoa_N4P1, benchmark):
|
41
|
+
qc, obs = maxcut_qaoa_N4P1
|
42
|
+
|
43
|
+
benchmark(qc.run, obs)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
.. _how_to_symbolic:
|
2
|
+
|
3
|
+
Symbolic Simulation
|
4
|
+
===================
|
5
|
+
|
6
|
+
`pyrauli` offers a powerful symbolic simulation mode that allows you to work with parameterized quantum circuits. Instead of providing fixed numerical values for parameters like gate angles or noise strengths, you can use symbolic variables. This is particularly useful for tasks like variational algorithms, gradient calculations, and sensitivity analysis, where you need to explore a function's behavior over a range of parameter values.
|
7
|
+
|
8
|
+
The Symbolic Toolkit
|
9
|
+
--------------------
|
10
|
+
|
11
|
+
The symbolic mode is built on three core classes:
|
12
|
+
|
13
|
+
* :py:class:`~pyrauli.SymbolicCoefficient`: Represents a mathematical expression that can include variables, constants, and standard mathematical operations.
|
14
|
+
* :py:class:`~pyrauli.SymbolicObservable`: An observable whose terms have `SymbolicCoefficient` objects as coefficients.
|
15
|
+
* :py:class:`~pyrauli.SymbolicCircuit`: A circuit that can accept `SymbolicCoefficient` objects as parameters for its operations.
|
16
|
+
|
17
|
+
Working with `SymbolicCoefficient`
|
18
|
+
------------------------------------
|
19
|
+
|
20
|
+
The :py:class:`~pyrauli.SymbolicCoefficient` is the fundamental building block. You can create one from a number or a string (which becomes a variable name).
|
21
|
+
|
22
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_coefficient_guide.py
|
23
|
+
:language: python
|
24
|
+
:start-after: # [symbolic_init]
|
25
|
+
:end-before: # [symbolic_init]
|
26
|
+
:dedent: 4
|
27
|
+
|
28
|
+
These objects support standard mathematical operations, allowing you to build complex expressions.
|
29
|
+
|
30
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_coefficient_guide.py
|
31
|
+
:language: python
|
32
|
+
:start-after: # [symbolic_ops]
|
33
|
+
:end-before: # [symbolic_ops]
|
34
|
+
:dedent: 4
|
35
|
+
|
36
|
+
Evaluating Expressions
|
37
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
38
|
+
|
39
|
+
A key feature is the ability to substitute variables with values. There are two ways to do this:
|
40
|
+
|
41
|
+
1. **.evaluate()**: This method substitutes all variables and computes a final floating-point number. It will raise an error if any variables are left unbound.
|
42
|
+
2. **.symbolic_evaluate()**: This method substitutes only the specified variables, returning a new, potentially simpler `SymbolicCoefficient`.
|
43
|
+
|
44
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_coefficient_guide.py
|
45
|
+
:language: python
|
46
|
+
:start-after: # [symbolic_evaluate]
|
47
|
+
:end-before: # [symbolic_evaluate]
|
48
|
+
:dedent: 4
|
49
|
+
|
50
|
+
Simplifying Expressions
|
51
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
52
|
+
|
53
|
+
The :py:meth:`~pyrauli.SymbolicCoefficient.simplified` method applies arithmetic rules (like `x*1=x` or `x+0=x`) to reduce the complexity of an expression.
|
54
|
+
|
55
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_coefficient_guide.py
|
56
|
+
:language: python
|
57
|
+
:start-after: # [symbolic_simplify]
|
58
|
+
:end-before: # [symbolic_simplify]
|
59
|
+
:dedent: 4
|
60
|
+
|
61
|
+
Constructing a `SymbolicObservable`
|
62
|
+
-------------------------------------
|
63
|
+
|
64
|
+
A :py:class:`~pyrauli.SymbolicObservable` works just like a regular :py:class:`~pyrauli.Observable`, but its coefficients are symbolic.
|
65
|
+
|
66
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_observable_guide.py
|
67
|
+
:language: python
|
68
|
+
:start-after: # [symbolic_obs_init]
|
69
|
+
:end-before: # [symbolic_obs_init]
|
70
|
+
:dedent: 4
|
71
|
+
|
72
|
+
The :py:meth:`~pyrauli.SymbolicObservable.simplify` method on an observable will simplify the symbolic coefficients of all its terms. You can also pass a dictionary of variable substitutions to this method.
|
73
|
+
|
74
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_observable_guide.py
|
75
|
+
:language: python
|
76
|
+
:start-after: # [symbolic_obs_simplify]
|
77
|
+
:end-before: # [symbolic_obs_simplify]
|
78
|
+
:dedent: 4
|
79
|
+
|
80
|
+
Building and Running a `SymbolicCircuit`
|
81
|
+
------------------------------------------
|
82
|
+
|
83
|
+
The end-to-end workflow is straightforward. You build a :py:class:`~pyrauli.SymbolicCircuit` using variable names for your parameters and then run it on a :py:class:`~pyrauli.SymbolicObservable`. The simulation propagates the symbolic expressions through the circuit according to the rules of quantum mechanics.
|
84
|
+
|
85
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_circuit_snippet.py
|
86
|
+
:language: python
|
87
|
+
:start-after: # [symbolic_circuit]
|
88
|
+
:end-before: # [symbolic_circuit]
|
89
|
+
:dedent: 4
|
90
|
+
|
91
|
+
The final result is a new :py:class:`~pyrauli.SymbolicObservable`. To get the final expectation value, you call its :py:meth:`~pyrauli.SymbolicObservable.expectation_value` method, which returns a `SymbolicCoefficient`. You can then evaluate this coefficient for any set of concrete parameter values.
|
92
|
+
|
93
|
+
.. literalinclude:: /../tests/snippets/test_symbolic_circuit_snippet.py
|
94
|
+
:language: python
|
95
|
+
:start-after: # [symbolic_evaluation]
|
96
|
+
:end-before: # [symbolic_evaluation]
|
97
|
+
:dedent: 4
|
98
|
+
|
99
|
+
This workflow allows you to run the simulation once to get a general symbolic result and then analyze that result for many different parameter values without needing to re-run the simulation each time.
|
@@ -5,7 +5,7 @@ build-backend = "scikit_build_core.build"
|
|
5
5
|
|
6
6
|
[project]
|
7
7
|
name = "pyrauli"
|
8
|
-
version = "0.
|
8
|
+
version = "0.4.0"
|
9
9
|
description = "A very fast and easy to use Quantum circuit simulator relying on Pauli propagation. Compatible with qiskit."
|
10
10
|
readme = "README.md"
|
11
11
|
requires-python = ">=3.9"
|
@@ -28,11 +28,15 @@ qiskit = [
|
|
28
28
|
"qiskit>=2.0.0",
|
29
29
|
"numpy"
|
30
30
|
]
|
31
|
+
symbolic = [
|
32
|
+
"sympy"
|
33
|
+
]
|
31
34
|
test = [
|
32
35
|
"pytest>=8.0.0",
|
33
36
|
"pytest-cov>=5.0.0",
|
34
37
|
"pytest-benchmark",
|
35
38
|
"numpy",
|
39
|
+
"sympy"
|
36
40
|
]
|
37
41
|
|
38
42
|
[tool.scikit-build.wheel]
|
@@ -12,6 +12,7 @@ from ._core import (
|
|
12
12
|
SchedulingPolicy, NeverPolicy, AlwaysBeforeSplittingPolicy,
|
13
13
|
AlwaysAfterSplittingPolicy, Circuit, OperationType, Timing,
|
14
14
|
SimulationState, CompressionResult, LambdaPolicy,
|
15
|
+
SymbolicCoefficient, SymbolicPauliTerm, SymbolicObservable, SymbolicNoise, SymbolicNoiseModel, SymbolicTruncator, SymbolicWeightTruncator, SymbolicNeverTruncator, SymbolicMultiTruncator, SymbolicCircuit
|
15
16
|
)
|
16
17
|
|
17
18
|
__all__ = [
|
@@ -22,8 +23,24 @@ __all__ = [
|
|
22
23
|
"AlwaysBeforeSplittingPolicy", "AlwaysAfterSplittingPolicy", "Circuit",
|
23
24
|
"OperationType", "Timing", "SimulationState", "CompressionResult",
|
24
25
|
"LambdaPolicy",
|
26
|
+
"SymbolicCoefficient", "SymbolicObservable", "SymbolicPauliTerm", "SymbolicNoise", "SymbolicNoiseModel", "SymbolicTruncator", "SymbolicWeightTruncator", "SymbolicNeverTruncator", "SymbolicMultiTruncator", "SymbolicCircuit"
|
27
|
+
|
25
28
|
]
|
26
29
|
|
30
|
+
try:
|
31
|
+
import sympy
|
32
|
+
def to_sympy(self):
|
33
|
+
"""
|
34
|
+
Converts the SymbolicCoefficient to a SymPy expression.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
A SymPy expression equivalent to the SymbolicCoefficient.
|
38
|
+
"""
|
39
|
+
return sympy.sympify(self.to_string())
|
40
|
+
SymbolicCoefficient.to_sympy = to_sympy
|
41
|
+
except ImportError:
|
42
|
+
pass
|
43
|
+
|
27
44
|
# Conditionally import Qiskit-related functionality
|
28
45
|
try:
|
29
46
|
from .backend import PBackend
|
@@ -16,6 +16,7 @@
|
|
16
16
|
#include "pauli_term.hpp"
|
17
17
|
#include "scheduler.hpp"
|
18
18
|
#include "truncate.hpp"
|
19
|
+
#include "symbolic/coefficient.hpp"
|
19
20
|
|
20
21
|
namespace py = pybind11;
|
21
22
|
|
@@ -27,8 +28,8 @@ using SchedulingPolicyPtr = std::shared_ptr<SchedulingPolicy>;
|
|
27
28
|
|
28
29
|
// Concrete holder types
|
29
30
|
using CoeffTruncatorPtr = std::shared_ptr<CoefficientTruncator<coeff_t>>;
|
30
|
-
using WeightTruncatorPtr = std::shared_ptr<WeightTruncator
|
31
|
-
using NeverTruncatorPtr = std::shared_ptr<NeverTruncator
|
31
|
+
using WeightTruncatorPtr = std::shared_ptr<WeightTruncator<coeff_t>>;
|
32
|
+
using NeverTruncatorPtr = std::shared_ptr<NeverTruncator<coeff_t>>;
|
32
33
|
using KeepNTruncatorPtr = std::shared_ptr<KeepNTruncator<coeff_t>>;
|
33
34
|
using NeverPolicyPtr = std::shared_ptr<NeverPolicy>;
|
34
35
|
using AlwaysBeforePolicyPtr = std::shared_ptr<AlwaysBeforeSplittingPolicy>;
|
@@ -38,6 +39,18 @@ using LambdaPredicate_t = std::function<bool(PauliTermContainer<coeff_t>::NonOwn
|
|
38
39
|
using LambdaTruncator = PredicateTruncator<LambdaPredicate_t>;
|
39
40
|
using LambdaTruncatorPtr = std::shared_ptr<LambdaTruncator>;
|
40
41
|
|
42
|
+
// Symbolic
|
43
|
+
using SymbolicCoeff_t = SymbolicCoefficient<coeff_t>;
|
44
|
+
using SymbolicObs_t = Observable<SymbolicCoeff_t>;
|
45
|
+
using SymbolicCircuit_t = Circuit<SymbolicCoeff_t>;
|
46
|
+
using SymbolicTruncatorPtr = std::shared_ptr<Truncator<SymbolicCoeff_t>>;
|
47
|
+
using SymbolicWeightTruncatorPtr = std::shared_ptr<WeightTruncator<SymbolicCoeff_t>>;
|
48
|
+
using SymbolicNeverTruncatorPtr = std::shared_ptr<NeverTruncator<SymbolicCoeff_t>>;
|
49
|
+
using SymbolicLambdaPredicate_t = std::function<bool(PauliTermContainer<SymbolicCoeff_t>::NonOwningPauliTermPacked const&)>;
|
50
|
+
using SymbolicLambdaTruncator = PredicateTruncator<SymbolicLambdaPredicate_t>;
|
51
|
+
using SymbolicLambdaTruncatorPtr = std::shared_ptr<SymbolicLambdaTruncator>;
|
52
|
+
using sPTC = PauliTermContainer<SymbolicCoeff_t>;
|
53
|
+
|
41
54
|
struct LambdaPolicy : public SchedulingPolicy {
|
42
55
|
public:
|
43
56
|
using predicate_t = std::function<bool(SimulationState const&, OperationType, Timing)>;
|
@@ -51,6 +64,11 @@ struct LambdaPolicy : public SchedulingPolicy {
|
|
51
64
|
predicate_t predicate;
|
52
65
|
};
|
53
66
|
|
67
|
+
// very very slow. That's why it's not in propauli.
|
68
|
+
bool operator==(SymbolicCoeff_t const& lhs, SymbolicCoeff_t const& rhs) {
|
69
|
+
return lhs.to_string() == rhs.to_string();
|
70
|
+
}
|
71
|
+
|
54
72
|
PYBIND11_MODULE(_core, m) {
|
55
73
|
m.doc() = "Core C++ functionality for pyrauli, wrapped with pybind11";
|
56
74
|
|
@@ -91,7 +109,9 @@ PYBIND11_MODULE(_core, m) {
|
|
91
109
|
.def("weight", &Pauli::weight, "Calculates the Pauli weight (1 if not Identity, 0 otherwise).")
|
92
110
|
.def("apply_pauli", &Pauli::apply_pauli,
|
93
111
|
"Applies a Pauli gate to this operator (in the Heisenberg picture).")
|
94
|
-
.def("apply_unital_noise", &Pauli::apply_unital_noise
|
112
|
+
.def("apply_unital_noise", &Pauli::apply_unital_noise<coeff_t>,
|
113
|
+
"Applies a unital noise channel to this operator.")
|
114
|
+
.def("apply_unital_noise", &Pauli::apply_unital_noise<SymbolicCoeff_t>,
|
95
115
|
"Applies a unital noise channel to this operator.")
|
96
116
|
.def("apply_clifford", &Pauli::apply_clifford,
|
97
117
|
"Applies a single-qubit Clifford gate to this operator, modifying it in place.")
|
@@ -187,6 +207,7 @@ PYBIND11_MODULE(_core, m) {
|
|
187
207
|
return ss.str();
|
188
208
|
});
|
189
209
|
|
210
|
+
|
190
211
|
// NoiseModel class
|
191
212
|
py::class_<Noise<coeff_t>>(m, "Noise", "Defines the strengths of different noise channels.")
|
192
213
|
.def(py::init<>())
|
@@ -206,11 +227,11 @@ PYBIND11_MODULE(_core, m) {
|
|
206
227
|
py::class_<CoefficientTruncator<coeff_t>, Truncator<coeff_t>, CoeffTruncatorPtr>(
|
207
228
|
m, "CoefficientTruncator", "Truncator that removes Pauli terms with small coefficients.")
|
208
229
|
.def(py::init<coeff_t>());
|
209
|
-
py::class_<WeightTruncator
|
230
|
+
py::class_<WeightTruncator<coeff_t>, Truncator<coeff_t>, WeightTruncatorPtr>(
|
210
231
|
m, "WeightTruncator", "Truncator that removes Pauli terms with high Pauli weight.")
|
211
232
|
.def(py::init<size_t>());
|
212
|
-
py::class_<NeverTruncator
|
213
|
-
|
233
|
+
py::class_<NeverTruncator<coeff_t>, Truncator<coeff_t>, NeverTruncatorPtr>(
|
234
|
+
m, "NeverTruncator", "A truncator that never removes any terms.")
|
214
235
|
.def(py::init<>());
|
215
236
|
py::class_<KeepNTruncator<coeff_t>, Truncator<coeff_t>, KeepNTruncatorPtr>(
|
216
237
|
m, "KeepNTruncator",
|
@@ -285,7 +306,7 @@ PYBIND11_MODULE(_core, m) {
|
|
285
306
|
"Represents a quantum circuit and provides a high-level simulation interface.")
|
286
307
|
.def(py::init<unsigned, std::shared_ptr<Truncator<coeff_t>>, const NoiseModel<coeff_t>&,
|
287
308
|
std::shared_ptr<SchedulingPolicy>, std::shared_ptr<SchedulingPolicy>>(),
|
288
|
-
py::arg("nb_qubits"), py::arg("truncator") = std::make_shared<NeverTruncator
|
309
|
+
py::arg("nb_qubits"), py::arg("truncator") = std::make_shared<NeverTruncator<coeff_t>>(),
|
289
310
|
py::arg("noise_model") = NoiseModel<coeff_t>(),
|
290
311
|
py::arg("merge_policy") = std::make_shared<AlwaysAfterSplittingPolicy>(),
|
291
312
|
py::arg("truncate_policy") = std::make_shared<AlwaysAfterSplittingPolicy>())
|
@@ -380,4 +401,309 @@ PYBIND11_MODULE(_core, m) {
|
|
380
401
|
ss << pt;
|
381
402
|
return ss.str();
|
382
403
|
});
|
404
|
+
|
405
|
+
// Symbolic
|
406
|
+
py::class_<Variable>(m, "Variable", "Symbolic string variable")
|
407
|
+
.def(py::init<std::string>());
|
408
|
+
|
409
|
+
py::class_<SymbolicCoeff_t>(m, "SymbolicCoefficient", "An easy to use symbolic coefficient.")
|
410
|
+
.def(py::init<coeff_t>(), "Construct from a constant value.")
|
411
|
+
.def(py::init([](const std::string& variable_name) {
|
412
|
+
return SymbolicCoeff_t{Variable{variable_name}};
|
413
|
+
}), "Construct from a variable name (string).")
|
414
|
+
.def(py::init<Variable>(), "Construct from a variable.")
|
415
|
+
.def("to_string", &SymbolicCoeff_t::to_string, "Convert to string using a formatting for real.", py::arg("format") = "{:.3f}")
|
416
|
+
.def("evaluate", &SymbolicCoeff_t::evaluate, "Evaluate into a real by replacing variables.", py::arg("variables") = std::unordered_map<std::string, SymbolicCoeff_t>{})
|
417
|
+
.def("symbolic_evaluate", &SymbolicCoeff_t::symbolic_evaluate, "Evaluate into another symbolic coefficient by replacing some variables.", py::arg("variables") = std::unordered_map<std::string, SymbolicCoeff_t>{})
|
418
|
+
.def("simplified", &SymbolicCoeff_t::simplified, "Returns simplified symbolic coefficient using arithmetic rules.")
|
419
|
+
.def("__repr__", [](SymbolicCoeff_t const& coeff) { return coeff.to_string(); })
|
420
|
+
.def(py::self *= py::self)
|
421
|
+
.def(py::self += py::self)
|
422
|
+
.def(py::self /= py::self)
|
423
|
+
.def(py::self -= py::self)
|
424
|
+
.def(-py::self)
|
425
|
+
.def(py::self + py::self)
|
426
|
+
.def(py::self * py::self)
|
427
|
+
.def(py::self / py::self)
|
428
|
+
.def(py::self - py::self)
|
429
|
+
.def(py::self *= float())
|
430
|
+
.def(py::self += float())
|
431
|
+
.def(py::self /= float())
|
432
|
+
.def(py::self -= float())
|
433
|
+
.def(py::self + float())
|
434
|
+
.def(py::self * float())
|
435
|
+
.def(py::self / float())
|
436
|
+
.def(py::self - float())
|
437
|
+
.def(float() + py::self)
|
438
|
+
.def(float() * py::self)
|
439
|
+
.def(float() / py::self)
|
440
|
+
.def(float() - py::self)
|
441
|
+
|
442
|
+
.def("cos", &SymbolicCoeff_t::cos, "Apply cosinus")
|
443
|
+
.def("sin", &SymbolicCoeff_t::sin, "Apply sinus")
|
444
|
+
.def("sqrt", &SymbolicCoeff_t::sqrt, "Apply sqrt");
|
445
|
+
|
446
|
+
py::class_<PauliTerm<SymbolicCoeff_t>>(
|
447
|
+
m, "SymbolicPauliTerm",
|
448
|
+
"Represents a single term in an observable, consisting of a Pauli string and a coefficient.")
|
449
|
+
.def(py::init<std::string_view, SymbolicCoeff_t>(), py::arg("pauli_string"), py::arg("coefficient") = SymbolicCoeff_t{1.0f},
|
450
|
+
"Constructs from a string representation and a coefficient.")
|
451
|
+
.def(py::init([](std::string_view sv, std::string const& variable_name) {
|
452
|
+
return PauliTerm<SymbolicCoeff_t>{sv, SymbolicCoeff_t{Variable{variable_name}}};
|
453
|
+
}),
|
454
|
+
"Constructs from a string representation and a coefficient.")
|
455
|
+
.def(py::init([](std::string_view sv, coeff_t coeff) {
|
456
|
+
return PauliTerm<SymbolicCoeff_t>{sv, SymbolicCoeff_t{coeff}};
|
457
|
+
}), "Construct from a constant coefficient")
|
458
|
+
.def("apply_pauli", &PauliTerm<SymbolicCoeff_t>::apply_pauli,
|
459
|
+
"Applies a Pauli gate to a specific qubit of the term.")
|
460
|
+
.def("apply_clifford", &PauliTerm<SymbolicCoeff_t>::apply_clifford,
|
461
|
+
"Applies a Clifford gate to a specific qubit of the term.")
|
462
|
+
.def("apply_unital_noise", &PauliTerm<SymbolicCoeff_t>::apply_unital_noise,
|
463
|
+
"Applies a unital noise channel to a specific qubit of the term.")
|
464
|
+
.def("apply_cx", &PauliTerm<SymbolicCoeff_t>::apply_cx, "Applies a CNOT gate to the term.")
|
465
|
+
.def("apply_rz", &PauliTerm<SymbolicCoeff_t>::apply_rz, "Applies an Rz gate, potentially splitting the term.")
|
466
|
+
.def("apply_amplitude_damping_xy", &PauliTerm<SymbolicCoeff_t>::apply_amplitude_damping_xy,
|
467
|
+
"Applies the X/Y part of the amplitude damping channel.")
|
468
|
+
.def("apply_amplitude_damping_z", &PauliTerm<SymbolicCoeff_t>::apply_amplitude_damping_z,
|
469
|
+
"Applies the Z part of the amplitude damping channel, splitting the term.")
|
470
|
+
.def("expectation_value", &PauliTerm<SymbolicCoeff_t>::expectation_value,
|
471
|
+
"Calculates the expectation value of this single term.")
|
472
|
+
.def("pauli_weight", &PauliTerm<SymbolicCoeff_t>::pauli_weight,
|
473
|
+
"Calculates the Pauli weight (number of non-identity operators).")
|
474
|
+
.def_property_readonly("coefficient", &PauliTerm<SymbolicCoeff_t>::coefficient, "The coefficient of the term.")
|
475
|
+
.def("__getitem__", [](const PauliTerm<SymbolicCoeff_t>& pt, size_t i) { return pt[i]; })
|
476
|
+
.def("__setitem__", [](PauliTerm<SymbolicCoeff_t>& pt, size_t i, const Pauli& p) { pt[i] = p; })
|
477
|
+
.def("__len__", &PauliTerm<SymbolicCoeff_t>::size)
|
478
|
+
.def(py::self == py::self)
|
479
|
+
.def(py::self != py::self)
|
480
|
+
.def("__repr__", [](const PauliTerm<SymbolicCoeff_t>& pt) {
|
481
|
+
std::stringstream ss;
|
482
|
+
ss << pt;
|
483
|
+
return ss.str();
|
484
|
+
});
|
485
|
+
|
486
|
+
|
487
|
+
py::class_<SymbolicObs_t>(m, "SymbolicObservable",
|
488
|
+
"Represents a quantum observable symbolically, as a linear combination of Pauli strings.")
|
489
|
+
.def(py::init<std::string_view, SymbolicCoeff_t>(), py::arg("pauli_string"), py::arg("coeff") = SymbolicCoeff_t{1.f},
|
490
|
+
"Constructs an observable from a single Pauli string.")
|
491
|
+
.def(py::init([](std::string_view sv, std::string const& variable_name) {
|
492
|
+
return Observable<SymbolicCoeff_t>{sv, SymbolicCoeff_t{Variable{variable_name}}};
|
493
|
+
}), "Construct from a variable coefficient")
|
494
|
+
.def(py::init([](std::string_view sv, coeff_t coeff) {
|
495
|
+
return Observable<SymbolicCoeff_t>{sv, SymbolicCoeff_t{coeff}};
|
496
|
+
}), "Construct from a constant coefficient")
|
497
|
+
.def(py::init<std::initializer_list<std::string_view>>(),
|
498
|
+
"Constructs an observable from an initializer_list of Pauli strings.")
|
499
|
+
// Use a lambda to correctly initialize from a list of PauliTerm objects
|
500
|
+
.def(py::init([](const std::vector<PauliTerm<SymbolicCoeff_t>>& paulis) {
|
501
|
+
return Observable<SymbolicCoeff_t>(paulis.begin(), paulis.end());
|
502
|
+
}),
|
503
|
+
"Constructs an observable from a list of PauliTerm objects.")
|
504
|
+
.def(py::init([](const std::vector<std::string>& paulis) {
|
505
|
+
return Observable<SymbolicCoeff_t>(paulis.begin(), paulis.end());
|
506
|
+
}),
|
507
|
+
"Constructs an observable from a list of Pauli strings.")
|
508
|
+
.def("apply_pauli", &Observable<SymbolicCoeff_t>::apply_pauli,
|
509
|
+
"Applies a single-qubit Pauli gate to the observable.")
|
510
|
+
.def("apply_clifford", &Observable<SymbolicCoeff_t>::apply_clifford,
|
511
|
+
"Applies a single-qubit Clifford gate to the observable.")
|
512
|
+
.def("apply_unital_noise",
|
513
|
+
&Observable<SymbolicCoeff_t>::apply_unital_noise,
|
514
|
+
"Applies a single-qubit unital noise channel.")
|
515
|
+
.def(
|
516
|
+
"apply_unital_noise",
|
517
|
+
[](Observable<SymbolicCoeff_t>& self, UnitalNoise noise, size_t qubit, std::string const& strength) {
|
518
|
+
self.apply_unital_noise(noise, qubit, SymbolicCoeff_t(Variable{strength}));
|
519
|
+
},
|
520
|
+
"Applies a single-qubit unital noise channel, using a variable name for the strength.")
|
521
|
+
.def("apply_cx", &Observable<SymbolicCoeff_t>::apply_cx, "Applies a CNOT (CX) gate to the observable.")
|
522
|
+
.def("apply_rz", &Observable<SymbolicCoeff_t>::apply_rz,
|
523
|
+
"Applies a single-qubit Rz rotation gate to the observable.")
|
524
|
+
.def(
|
525
|
+
"apply_rz",
|
526
|
+
[](Observable<SymbolicCoeff_t>& self, size_t qubit, std::string const& param) {
|
527
|
+
self.apply_rz(qubit, SymbolicCoeff_t(Variable{param}));
|
528
|
+
},
|
529
|
+
"Applies a single-qubit Rz rotation gate to the observable, using a variable name for the angle.")
|
530
|
+
.def("apply_amplitude_damping", &Observable<SymbolicCoeff_t>::apply_amplitude_damping, "Applies an amplitude damping noise channel.")
|
531
|
+
.def(
|
532
|
+
"apply_amplitude_damping",
|
533
|
+
[](Observable<SymbolicCoeff_t>& self, size_t qubit, std::string const& strength) {
|
534
|
+
self.apply_amplitude_damping(qubit, SymbolicCoeff_t(Variable{strength}));
|
535
|
+
},
|
536
|
+
"Applies an amplitude damping noise channel, using a variable name for the strength.")
|
537
|
+
.def("expectation_value", &Observable<SymbolicCoeff_t>::expectation_value,
|
538
|
+
"Calculates the expectation value of the observable.")
|
539
|
+
.def("merge", &Observable<SymbolicCoeff_t>::merge, "Merges Pauli terms with identical Pauli strings.")
|
540
|
+
.def("size", &Observable<SymbolicCoeff_t>::size, "Gets the number of Pauli terms in the observable.")
|
541
|
+
.def("simplify", &Observable<SymbolicCoeff_t>::simplify<SymbolicCoeff_t>, py::arg("variable_map") = std::unordered_map<std::string, coeff_t>{}, "Simplify the observable coefficient and replace variables.")
|
542
|
+
.def(
|
543
|
+
"truncate", [](Observable<SymbolicCoeff_t>& obs, SymbolicTruncatorPtr ptr) { return obs.truncate(*ptr); },
|
544
|
+
"Truncates the observable based on a given truncation strategy.")
|
545
|
+
.def(py::self == py::self)
|
546
|
+
.def(py::self != py::self)
|
547
|
+
.def("__getitem__", [](const Observable<SymbolicCoeff_t>& obs, size_t i) { return obs[i]; })
|
548
|
+
.def("__len__", &Observable<SymbolicCoeff_t>::size)
|
549
|
+
.def(
|
550
|
+
"__iter__",
|
551
|
+
[](const Observable<SymbolicCoeff_t>& obs) { return py::make_iterator(obs.begin(), obs.end()); },
|
552
|
+
py::keep_alive<0, 1>())
|
553
|
+
.def("__repr__", [](const Observable<SymbolicCoeff_t>& obs) {
|
554
|
+
std::stringstream ss;
|
555
|
+
ss << obs;
|
556
|
+
return ss.str();
|
557
|
+
});
|
558
|
+
|
559
|
+
|
560
|
+
py::class_<Noise<SymbolicCoeff_t>>(m, "SymbolicNoise", "Defines the strengths of different noise channels.")
|
561
|
+
.def(py::init<>())
|
562
|
+
.def_readwrite("depolarizing_strength", &Noise<SymbolicCoeff_t>::depolarizing_strength)
|
563
|
+
.def_readwrite("dephasing_strength", &Noise<SymbolicCoeff_t>::dephasing_strength)
|
564
|
+
.def_readwrite("amplitude_damping_strength", &Noise<SymbolicCoeff_t>::amplitude_damping_strength);
|
565
|
+
|
566
|
+
py::class_<NoiseModel<SymbolicCoeff_t>>(m, "SymbolicNoiseModel", "A model for applying noise to quantum gates.")
|
567
|
+
.def(py::init<>())
|
568
|
+
.def("add_unital_noise_on_gate",
|
569
|
+
&NoiseModel<SymbolicCoeff_t>::add_unital_noise_on_gate,
|
570
|
+
"Adds a unital noise channel to be applied after a specific gate type.")
|
571
|
+
.def(
|
572
|
+
"add_unital_noise_on_gate",
|
573
|
+
[](NoiseModel<SymbolicCoeff_t>& self, QGate gate, UnitalNoise noise, std::string const& strength) {
|
574
|
+
self.add_unital_noise_on_gate(gate, noise, SymbolicCoeff_t(Variable{strength}));
|
575
|
+
},
|
576
|
+
"Adds a unital noise channel to be applied after a specific gate type, using a variable name for strength.")
|
577
|
+
.def("add_amplitude_damping_on_gate",
|
578
|
+
&NoiseModel<SymbolicCoeff_t>::add_amplitude_damping_on_gate,
|
579
|
+
"Adds an amplitude damping channel to be applied after a specific gate type.")
|
580
|
+
.def(
|
581
|
+
"add_amplitude_damping_on_gate",
|
582
|
+
[](NoiseModel<SymbolicCoeff_t>& self, QGate gate, std::string const& strength) {
|
583
|
+
self.add_amplitude_damping_on_gate(gate, SymbolicCoeff_t(Variable{strength}));
|
584
|
+
},
|
585
|
+
"Adds an amplitude damping channel to be applied after a specific gate type, using a variable name for strength.");
|
586
|
+
// symbolic truncators
|
587
|
+
py::class_<Truncator<SymbolicCoeff_t>, SymbolicTruncatorPtr>(m, "SymbolicTruncator",
|
588
|
+
"Abstract base class for defining truncation strategies.");
|
589
|
+
py::class_<WeightTruncator<SymbolicCoeff_t>, Truncator<SymbolicCoeff_t>, SymbolicWeightTruncatorPtr>(
|
590
|
+
m, "SymbolicWeightTruncator", "Truncator that removes Pauli terms with high Pauli weight.")
|
591
|
+
.def(py::init<size_t>());
|
592
|
+
py::class_<NeverTruncator<SymbolicCoeff_t>, Truncator<SymbolicCoeff_t>, SymbolicNeverTruncatorPtr>(
|
593
|
+
m, "SymbolicNeverTruncator", "A truncator that never removes any terms.")
|
594
|
+
.def(py::init<>());
|
595
|
+
//py::class_<PredicateTruncator<SymbolicCoeff_t>, Truncator<SymbolicCoeff_t>, std::shared_ptr<PredicateTruncator<SymbolicLambdaPredicate_t>>>(
|
596
|
+
// m, "SymbolicLambdaTruncator", "A truncator that uses a Python function as a predicate.")
|
597
|
+
// .def(py::init<SymbolicLambdaPredicate_t>());
|
598
|
+
py::class_<RuntimeMultiTruncators<SymbolicCoeff_t>, Truncator<SymbolicCoeff_t>,
|
599
|
+
std::shared_ptr<RuntimeMultiTruncators<SymbolicCoeff_t>>>(
|
600
|
+
m, "SymbolicMultiTruncator", "A truncator that combines multiple truncators at runtime.")
|
601
|
+
.def(py::init<const std::vector<SymbolicTruncatorPtr>&>());
|
602
|
+
|
603
|
+
py::class_<Circuit<SymbolicCoeff_t>>(m, "SymbolicCircuit",
|
604
|
+
"Represents a quantum circuit and provides a high-level simulation interface.")
|
605
|
+
.def(py::init<unsigned, std::shared_ptr<Truncator<SymbolicCoeff_t>>, const NoiseModel<SymbolicCoeff_t>&,
|
606
|
+
std::shared_ptr<SchedulingPolicy>, std::shared_ptr<SchedulingPolicy>>(),
|
607
|
+
py::arg("nb_qubits"), py::arg("truncator") = std::make_shared<NeverTruncator<SymbolicCoeff_t>>(),
|
608
|
+
py::arg("noise_model") = NoiseModel<SymbolicCoeff_t>(),
|
609
|
+
py::arg("merge_policy") = std::make_shared<AlwaysAfterSplittingPolicy>(),
|
610
|
+
py::arg("truncate_policy") = std::make_shared<AlwaysAfterSplittingPolicy>())
|
611
|
+
.def("nb_qubits", &Circuit<SymbolicCoeff_t>::nb_qubits, "Gets the number of qubits in the circuit.")
|
612
|
+
// Use lambdas to resolve templated overloads
|
613
|
+
.def(
|
614
|
+
"add_operation",
|
615
|
+
[](Circuit<SymbolicCoeff_t>& self, std::string op, unsigned q1) { self.add_operation(op, q1); },
|
616
|
+
"Adds a single-qubit gate.", py::arg("op"), py::arg("qubit"))
|
617
|
+
.def(
|
618
|
+
"add_operation",
|
619
|
+
[](Circuit<SymbolicCoeff_t>& self, std::string op, unsigned q1, SymbolicCoeff_t p) {
|
620
|
+
self.add_operation(op, q1, p);
|
621
|
+
},
|
622
|
+
"Adds a single-qubit gate with a parameter.", py::arg("op"), py::arg("qubit"), py::arg("param"))
|
623
|
+
.def(
|
624
|
+
"add_operation",
|
625
|
+
[](Circuit<SymbolicCoeff_t>& self, std::string op, unsigned q1, std::string const& p) {
|
626
|
+
self.add_operation(op, q1, SymbolicCoeff_t(Variable(p)));
|
627
|
+
},
|
628
|
+
"Adds a single-qubit gate with a parameter.", py::arg("op"), py::arg("qubit"), py::arg("param"))
|
629
|
+
.def(
|
630
|
+
"add_operation",
|
631
|
+
[](Circuit<SymbolicCoeff_t>& self, std::string op, unsigned q1, unsigned q2) {
|
632
|
+
self.add_operation(op, q1, q2);
|
633
|
+
},
|
634
|
+
"Adds a two-qubit gate.", py::arg("op"), py::arg("control"), py::arg("target"))
|
635
|
+
.def("run", &Circuit<SymbolicCoeff_t>::run, "Runs the simulation on the circuit.")
|
636
|
+
.def("reset", &Circuit<SymbolicCoeff_t>::reset, "Clears all operations from the circuit.")
|
637
|
+
.def("set_truncator", &Circuit<SymbolicCoeff_t>::set_truncator, "Sets a new truncator for the circuit.")
|
638
|
+
.def("set_merge_policy", &Circuit<SymbolicCoeff_t>::set_merge_policy,
|
639
|
+
"Sets a new policy for when to merge Pauli terms.")
|
640
|
+
.def("set_truncate_policy", &Circuit<SymbolicCoeff_t>::set_truncate_policy,
|
641
|
+
"Sets a new policy for when to truncate the observable.");
|
642
|
+
|
643
|
+
py::class_<sPTC::ReadOnlyNonOwningPauliTermPacked>(m, "SymbolicReadOnlyPackedPauliTermView",
|
644
|
+
"A read-only, non-owning view of a packed Pauli term.")
|
645
|
+
.def_property_readonly("coefficient", &sPTC::ReadOnlyNonOwningPauliTermPacked::coefficient,
|
646
|
+
"The coefficient of the term.")
|
647
|
+
.def_property_readonly("nb_qubits", &sPTC::ReadOnlyNonOwningPauliTermPacked::size,
|
648
|
+
"The number of qubits in the term.")
|
649
|
+
.def("pauli_weight", &sPTC::ReadOnlyNonOwningPauliTermPacked::pauli_weight,
|
650
|
+
"Calculates the Pauli weight (number of non-identity operators).")
|
651
|
+
.def("expectation_value", &sPTC::ReadOnlyNonOwningPauliTermPacked::expectation_value,
|
652
|
+
"Calculates the expectation value of this single term.")
|
653
|
+
.def(
|
654
|
+
"to_pauli_term",
|
655
|
+
[](const sPTC::ReadOnlyNonOwningPauliTermPacked& self) {
|
656
|
+
return static_cast<PauliTerm<SymbolicCoeff_t>>(self);
|
657
|
+
},
|
658
|
+
"Creates an owning PauliTerm copy from this view.")
|
659
|
+
.def("__len__", &sPTC::ReadOnlyNonOwningPauliTermPacked::size)
|
660
|
+
.def("__getitem__", &sPTC::ReadOnlyNonOwningPauliTermPacked::get_pauli,
|
661
|
+
"Gets the Pauli operator at a specific qubit index.")
|
662
|
+
.def(py::self == py::self)
|
663
|
+
.def(
|
664
|
+
"__eq__",
|
665
|
+
[](const sPTC::ReadOnlyNonOwningPauliTermPacked& self, const PauliTerm<SymbolicCoeff_t>& other) {
|
666
|
+
return self == other;
|
667
|
+
},
|
668
|
+
"Compares this view with an owning PauliTerm object.")
|
669
|
+
.def("__repr__", [](const sPTC::ReadOnlyNonOwningPauliTermPacked& pt) {
|
670
|
+
std::stringstream ss;
|
671
|
+
ss << pt;
|
672
|
+
return ss.str();
|
673
|
+
});
|
674
|
+
|
675
|
+
py::class_<sPTC::NonOwningPauliTermPacked>(m, "SymbolicPackedPauliTermView",
|
676
|
+
"A mutable, non-owning view of a packed Pauli term.")
|
677
|
+
.def_property("coefficient", &sPTC::NonOwningPauliTermPacked::coefficient,
|
678
|
+
&sPTC::NonOwningPauliTermPacked::set_coefficient,
|
679
|
+
"The coefficient of the term (read/write).")
|
680
|
+
.def_property_readonly("nb_qubits", &sPTC::NonOwningPauliTermPacked::size,
|
681
|
+
"The number of qubits in the term.")
|
682
|
+
.def("pauli_weight", &sPTC::NonOwningPauliTermPacked::pauli_weight,
|
683
|
+
"Calculates the Pauli weight (number of non-identity operators).")
|
684
|
+
.def("expectation_value", &sPTC::NonOwningPauliTermPacked::expectation_value,
|
685
|
+
"Calculates the expectation value of this single term.")
|
686
|
+
.def(
|
687
|
+
"to_pauli_term",
|
688
|
+
[](const sPTC::NonOwningPauliTermPacked& self) { return static_cast<PauliTerm<SymbolicCoeff_t>>(self); },
|
689
|
+
"Creates an owning PauliTerm copy from this view.")
|
690
|
+
.def("add_coeff", &sPTC::NonOwningPauliTermPacked::add_coeff, "Adds a value to the term's coefficient.")
|
691
|
+
.def("simplify", &sPTC::NonOwningPauliTermPacked::simplify<SymbolicCoeff_t>, "Simplify (in-place) a symbolic pauli term coefficient and replace variables.")
|
692
|
+
.def("__len__", &sPTC::NonOwningPauliTermPacked::size)
|
693
|
+
.def("__getitem__", &sPTC::NonOwningPauliTermPacked::get_pauli,
|
694
|
+
"Gets the Pauli operator at a specific qubit index.")
|
695
|
+
.def("__setitem__", &sPTC::NonOwningPauliTermPacked::set_pauli,
|
696
|
+
"Sets the Pauli operator at a specific qubit index.")
|
697
|
+
.def(py::self == py::self)
|
698
|
+
.def(
|
699
|
+
"__eq__",
|
700
|
+
[](const sPTC::NonOwningPauliTermPacked& self, const PauliTerm<SymbolicCoeff_t>& other) {
|
701
|
+
return self == other;
|
702
|
+
},
|
703
|
+
"Compares this view with an owning PauliTerm object.")
|
704
|
+
.def("__repr__", [](const sPTC::NonOwningPauliTermPacked& pt) {
|
705
|
+
std::stringstream ss;
|
706
|
+
ss << pt;
|
707
|
+
return ss.str();
|
708
|
+
});
|
383
709
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from pyrauli import SymbolicCircuit, SymbolicObservable, SymbolicNoiseModel, UnitalNoise, QGate
|
2
|
+
import pytest
|
3
|
+
|
4
|
+
def test_symbolic_noise():
|
5
|
+
# [symbolic_noise]
|
6
|
+
# 1. Define a symbolic noise model
|
7
|
+
noise_model = SymbolicNoiseModel()
|
8
|
+
noise_model.add_unital_noise_on_gate(QGate.H, UnitalNoise.Depolarizing, "p_noise")
|
9
|
+
|
10
|
+
# 2. Create a circuit with the noise model
|
11
|
+
circuit = SymbolicCircuit(1, noise_model=noise_model)
|
12
|
+
circuit.add_operation("H", 0)
|
13
|
+
|
14
|
+
# 3. Run the simulation
|
15
|
+
observable = SymbolicObservable("X")
|
16
|
+
final_observable = circuit.run(observable)
|
17
|
+
result = final_observable.expectation_value()
|
18
|
+
|
19
|
+
# 4. Evaluate for different noise levels
|
20
|
+
print(f"Expectation value with no noise: {result.evaluate({'p_noise': 0.0})}")
|
21
|
+
print(f"Expectation value with some noise: {result.evaluate({'p_noise': 0.1})}")
|
22
|
+
# [symbolic_noise]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from pyrauli import SymbolicCircuit, SymbolicObservable
|
2
|
+
|
3
|
+
def test_symbolic_obs():
|
4
|
+
# [symbolic_circuit]
|
5
|
+
circuit = SymbolicCircuit(1)
|
6
|
+
circuit.add_operation("H", 0)
|
7
|
+
circuit.add_operation("Rz", 0, "theta")
|
8
|
+
circuit.add_operation("H", 0)
|
9
|
+
# [symbolic_circuit]
|
10
|
+
|
11
|
+
observable = SymbolicObservable("Z")
|
12
|
+
|
13
|
+
# [symbolic_evaluation]
|
14
|
+
final_observable = circuit.run(observable)
|
15
|
+
expectation_value = final_observable.expectation_value()
|
16
|
+
|
17
|
+
# Evaluate for a specific angle
|
18
|
+
value = expectation_value.evaluate({"theta": 3.14159 / 4})
|
19
|
+
# [symbolic_evaluation]
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
def test_snippet_symbolic_coeff():
|
4
|
+
# [symbolic_init]
|
5
|
+
from pyrauli import SymbolicCoefficient
|
6
|
+
|
7
|
+
# From a constant
|
8
|
+
const_coeff = SymbolicCoefficient(1.23)
|
9
|
+
|
10
|
+
# From a variable name
|
11
|
+
var_coeff = SymbolicCoefficient("theta")
|
12
|
+
|
13
|
+
print(const_coeff)
|
14
|
+
print(var_coeff)
|
15
|
+
# [symbolic_init]
|
16
|
+
|
17
|
+
# [symbolic_ops]
|
18
|
+
a = SymbolicCoefficient("a")
|
19
|
+
b = SymbolicCoefficient("b")
|
20
|
+
|
21
|
+
# Perform standard arithmetic
|
22
|
+
expr = (a * 2 + b) / 3
|
23
|
+
print(f"Expression: {expr}")
|
24
|
+
|
25
|
+
# Use trigonometric functions
|
26
|
+
trig_expr = expr.cos()
|
27
|
+
print(f"Trigonometric Expression: {trig_expr}")
|
28
|
+
# [symbolic_ops]
|
29
|
+
|
30
|
+
# [symbolic_evaluate]
|
31
|
+
expression = SymbolicCoefficient("a") + SymbolicCoefficient("b")
|
32
|
+
|
33
|
+
# Evaluate fully by providing all variables
|
34
|
+
result = expression.evaluate({"a": 2.5, "b": 1.5})
|
35
|
+
print(f"Final scalar value: {result}")
|
36
|
+
|
37
|
+
# Partially evaluate by providing only some variables
|
38
|
+
partial_result = expression.symbolic_evaluate({"a": 2.5})
|
39
|
+
print(f"Partially evaluated expression: {partial_result}")
|
40
|
+
# [symbolic_evaluate]
|
41
|
+
|
42
|
+
|
43
|
+
# [symbolic_simplify]
|
44
|
+
x = SymbolicCoefficient("x")
|
45
|
+
|
46
|
+
# Create a redundant expression
|
47
|
+
expr = (x * 1 + 0) - (x * 0)
|
48
|
+
print(f"Original expression: {expr}")
|
49
|
+
|
50
|
+
# Simplify it
|
51
|
+
simplified_expr = expr.simplified()
|
52
|
+
print(f"Simplified expression: {simplified_expr}")
|
53
|
+
# [symbolic_simplify]
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
def test_symbolic_observable_snippet():
|
3
|
+
# [symbolic_obs_init]
|
4
|
+
from pyrauli import SymbolicObservable, SymbolicPauliTerm
|
5
|
+
|
6
|
+
# From a single Pauli string with a symbolic coefficient
|
7
|
+
obs1 = SymbolicObservable("X", "a")
|
8
|
+
print(obs1)
|
9
|
+
|
10
|
+
# From a list of Pauli strings (default coefficient is 1.0)
|
11
|
+
obs2 = SymbolicObservable(["XX", "YY"])
|
12
|
+
print(obs2)
|
13
|
+
|
14
|
+
# From a list of PauliTerms with symbolic coefficients
|
15
|
+
obs3 = SymbolicObservable([
|
16
|
+
SymbolicPauliTerm("X", "a"),
|
17
|
+
SymbolicPauliTerm("Y", "b")
|
18
|
+
])
|
19
|
+
print(obs3)
|
20
|
+
# [symbolic_obs_init]
|
21
|
+
|
22
|
+
# [symbolic_obs_simplify]
|
23
|
+
from pyrauli import SymbolicObservable, SymbolicPauliTerm
|
24
|
+
|
25
|
+
# Create an observable with redundant terms
|
26
|
+
obs = SymbolicObservable(["X", "X", "Y"])
|
27
|
+
obs.merge() # ['2.000 * X', '1.000 * Y']
|
28
|
+
|
29
|
+
# Add another term with a symbolic coefficient
|
30
|
+
obs = SymbolicObservable([
|
31
|
+
SymbolicPauliTerm("X", 2.0),
|
32
|
+
SymbolicPauliTerm("Y", 1.0),
|
33
|
+
SymbolicPauliTerm("X", "a")
|
34
|
+
])
|
35
|
+
|
36
|
+
obs.merge()
|
37
|
+
print(f"Merged observable: {obs}")
|
38
|
+
|
39
|
+
# Simplify the coefficients
|
40
|
+
obs.simplify()
|
41
|
+
print(f"Simplified observable: {obs}")
|
42
|
+
|
43
|
+
# Simplify and substitute a variable
|
44
|
+
obs.simplify({"a": 3.0})
|
45
|
+
print(f"Simplified and substituted observable: {obs}")
|
46
|
+
# [symbolic_obs_simplify]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# tests/snippets/test_symbolic_sympy.py
|
2
|
+
from pyrauli import SymbolicCircuit, SymbolicObservable
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
sympy = pytest.importorskip("sympy", reason="symbolic extra not installed")
|
6
|
+
|
7
|
+
def test_sympy_snippet():
|
8
|
+
# [symbolic_sympy]
|
9
|
+
# Create and run a symbolic circuit as before
|
10
|
+
circuit = SymbolicCircuit(1)
|
11
|
+
circuit.add_operation("H", 0)
|
12
|
+
circuit.add_operation("Rz", 0, "theta")
|
13
|
+
circuit.add_operation("H", 0)
|
14
|
+
observable = SymbolicObservable("Z")
|
15
|
+
final_observable = circuit.run(observable)
|
16
|
+
symbolic_coeff = final_observable.expectation_value()
|
17
|
+
|
18
|
+
# Convert to a SymPy expression
|
19
|
+
sympy_expr = symbolic_coeff.to_sympy()
|
20
|
+
|
21
|
+
# Now you can use SymPy's features
|
22
|
+
theta = sympy.Symbol("theta")
|
23
|
+
print(sympy.diff(sympy_expr, theta))
|
24
|
+
# [symbolic_sympy]
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from pyrauli import (
|
2
|
+
SymbolicCircuit,
|
3
|
+
SymbolicObservable,
|
4
|
+
SymbolicNoiseModel,
|
5
|
+
SymbolicWeightTruncator,
|
6
|
+
UnitalNoise,
|
7
|
+
QGate
|
8
|
+
)
|
9
|
+
import pytest
|
10
|
+
|
11
|
+
|
12
|
+
def test_init():
|
13
|
+
SymbolicCircuit(2)
|
14
|
+
SymbolicCircuit(2, truncator=SymbolicWeightTruncator(1))
|
15
|
+
|
16
|
+
|
17
|
+
def test_add_operations():
|
18
|
+
circuit = SymbolicCircuit(2)
|
19
|
+
circuit.add_operation("H", 0)
|
20
|
+
circuit.add_operation("CX", 0, 1)
|
21
|
+
circuit.add_operation("Rz", 1, "theta")
|
22
|
+
|
23
|
+
|
24
|
+
def test_run_symbolic():
|
25
|
+
circuit = SymbolicCircuit(1)
|
26
|
+
circuit.add_operation("H", 0)
|
27
|
+
circuit.add_operation("Rz", 0, "theta")
|
28
|
+
circuit.add_operation("H", 0)
|
29
|
+
|
30
|
+
obs = SymbolicObservable("Z")
|
31
|
+
final_obs = circuit.run(obs)
|
32
|
+
|
33
|
+
assert final_obs.expectation_value().evaluate({"theta": 0}) == pytest.approx(1.0)
|
34
|
+
assert final_obs.expectation_value().evaluate({"theta": 3.14159}) == pytest.approx(
|
35
|
+
-1.0
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
def test_with_noise():
|
40
|
+
noise = SymbolicNoiseModel()
|
41
|
+
noise.add_unital_noise_on_gate(QGate.H, UnitalNoise.Depolarizing, "p")
|
42
|
+
|
43
|
+
circuit = SymbolicCircuit(1, noise_model=noise)
|
44
|
+
circuit.add_operation("H", 0)
|
45
|
+
|
46
|
+
obs = SymbolicObservable("X")
|
47
|
+
final_obs = circuit.run(obs)
|
48
|
+
|
49
|
+
# With no noise, H|X> = |Z>, EV = 1
|
50
|
+
assert final_obs.expectation_value().evaluate({"p": 0}) == pytest.approx(1.0)
|
51
|
+
# With full depolarizing, state is mixed, EV = 0
|
52
|
+
assert final_obs.expectation_value().evaluate({"p": 1.0}) == pytest.approx(0.0)
|
53
|
+
|
54
|
+
|
55
|
+
def test_reset():
|
56
|
+
circuit = SymbolicCircuit(1)
|
57
|
+
circuit.add_operation("H", 0)
|
58
|
+
circuit.reset()
|
59
|
+
obs = SymbolicObservable("Z")
|
60
|
+
final_obs = circuit.run(obs)
|
61
|
+
assert final_obs[0] == obs[0]
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import pytest
|
2
|
+
from pyrauli import SymbolicCoefficient
|
3
|
+
from math import pi, cos, sin, sqrt
|
4
|
+
import sympy
|
5
|
+
|
6
|
+
|
7
|
+
def test_init():
|
8
|
+
assert SymbolicCoefficient(1.0).to_string() == "1.000"
|
9
|
+
assert SymbolicCoefficient("x").to_string() == "x"
|
10
|
+
|
11
|
+
|
12
|
+
def test_to_string():
|
13
|
+
assert SymbolicCoefficient(1.234567).to_string("{:.2f}") == "1.23"
|
14
|
+
x = SymbolicCoefficient("x")
|
15
|
+
expr = x * 2 + 3
|
16
|
+
assert expr.to_string() == "(x * 2.000) + 3.000"
|
17
|
+
|
18
|
+
|
19
|
+
def test_evaluate():
|
20
|
+
assert SymbolicCoefficient(1.0).evaluate() == 1.0
|
21
|
+
x = SymbolicCoefficient("x")
|
22
|
+
with pytest.raises(ValueError):
|
23
|
+
x.evaluate() # Test for unbound variable
|
24
|
+
assert x.evaluate({"x": 2.0}) == 2.0
|
25
|
+
|
26
|
+
|
27
|
+
def test_symbolic_evaluate():
|
28
|
+
x = SymbolicCoefficient("x")
|
29
|
+
y = SymbolicCoefficient("y")
|
30
|
+
expr = x + y
|
31
|
+
new_expr = expr.symbolic_evaluate({"x": 1})
|
32
|
+
assert new_expr.to_string() == "1.000 + y"
|
33
|
+
assert new_expr.evaluate({"y": 2}) == 3
|
34
|
+
|
35
|
+
|
36
|
+
def test_unary_ops():
|
37
|
+
x = SymbolicCoefficient("x")
|
38
|
+
assert (-x).evaluate({"x": 1}) == -1
|
39
|
+
assert x.cos().evaluate({"x": 0}) == cos(0)
|
40
|
+
assert x.sin().evaluate({"x": pi / 2}) == sin(pi / 2)
|
41
|
+
assert x.sqrt().evaluate({"x": 4}) == sqrt(4)
|
42
|
+
|
43
|
+
|
44
|
+
def test_binary_ops():
|
45
|
+
x = SymbolicCoefficient("x")
|
46
|
+
y = SymbolicCoefficient("y")
|
47
|
+
assert (x + y).evaluate({"x": 1, "y": 2}) == 3
|
48
|
+
assert (x - 3).evaluate({"x": 1}) == -2
|
49
|
+
assert (x * y).evaluate({"x": 2, "y": 3}) == 6
|
50
|
+
assert (4 / y).evaluate({"y": 2}) == 2
|
51
|
+
|
52
|
+
|
53
|
+
def test_long_expression():
|
54
|
+
x = SymbolicCoefficient("x")
|
55
|
+
y = SymbolicCoefficient("y")
|
56
|
+
expr = 2 * (x + y) - (x / 3)
|
57
|
+
assert expr.evaluate({"x": 3, "y": 1}) == pytest.approx(7.0)
|
58
|
+
|
59
|
+
|
60
|
+
def test_simplified():
|
61
|
+
x = SymbolicCoefficient("x")
|
62
|
+
expr = (x + 0) * 1
|
63
|
+
simplified_expr = expr.simplified()
|
64
|
+
assert simplified_expr.to_string() == "x"
|
65
|
+
|
66
|
+
|
67
|
+
def test_to_sympy():
|
68
|
+
x = SymbolicCoefficient("x")
|
69
|
+
y = SymbolicCoefficient("y")
|
70
|
+
expr = 2 * (x + y)
|
71
|
+
sympy_expr = expr.to_sympy()
|
72
|
+
|
73
|
+
sx = sympy.Symbol("x")
|
74
|
+
sy = sympy.Symbol("y")
|
75
|
+
|
76
|
+
assert str(sympy_expr) == "2.0*x + 2.0*y"
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import pytest
|
2
|
+
from pyrauli import (
|
3
|
+
SymbolicObservable,
|
4
|
+
SymbolicCoefficient,
|
5
|
+
SymbolicPauliTerm,
|
6
|
+
SymbolicWeightTruncator,
|
7
|
+
PauliGate,
|
8
|
+
CliffordGate,
|
9
|
+
UnitalNoise,
|
10
|
+
)
|
11
|
+
from math import pi
|
12
|
+
|
13
|
+
|
14
|
+
def test_init():
|
15
|
+
SymbolicObservable("IXYZ")
|
16
|
+
SymbolicObservable("IXYZ", SymbolicCoefficient("a"))
|
17
|
+
SymbolicObservable(["IXYZ", "ZZZZ"])
|
18
|
+
SymbolicObservable([SymbolicPauliTerm("X", "a"), SymbolicPauliTerm("Z", "b")])
|
19
|
+
|
20
|
+
|
21
|
+
def test_apply_pauli():
|
22
|
+
obs = SymbolicObservable("X")
|
23
|
+
obs.apply_pauli(PauliGate.Z, 0)
|
24
|
+
assert str(obs[0]) == "1 * -1 X"
|
25
|
+
|
26
|
+
|
27
|
+
def test_apply_clifford():
|
28
|
+
obs = SymbolicObservable("X")
|
29
|
+
obs.apply_clifford(CliffordGate.H, 0)
|
30
|
+
assert str(obs[0]) == "1 * 1 Z"
|
31
|
+
|
32
|
+
|
33
|
+
def test_apply_cx():
|
34
|
+
obs = SymbolicObservable("IX")
|
35
|
+
obs.apply_cx(0, 1)
|
36
|
+
assert str(obs[0]) == "1 * 1 IX"
|
37
|
+
|
38
|
+
|
39
|
+
def test_apply_rz():
|
40
|
+
obs = SymbolicObservable("X")
|
41
|
+
obs.apply_rz(0, "theta")
|
42
|
+
assert obs.size() == 2
|
43
|
+
assert str(obs[0]) == "1 * cos(theta) X"
|
44
|
+
assert str(obs[1]) == "1 * -sin(theta) Y"
|
45
|
+
|
46
|
+
|
47
|
+
def test_apply_unital_noise():
|
48
|
+
obs = SymbolicObservable("X")
|
49
|
+
obs.apply_unital_noise(UnitalNoise.Depolarizing, 0, "p")
|
50
|
+
assert str(obs[0].coefficient.simplified()) == "1 - p"
|
51
|
+
|
52
|
+
|
53
|
+
def test_apply_amplitude_damping():
|
54
|
+
obs = SymbolicObservable("Z")
|
55
|
+
obs.apply_amplitude_damping(0, "p")
|
56
|
+
assert obs.size() == 2
|
57
|
+
|
58
|
+
|
59
|
+
def test_expectation_value():
|
60
|
+
obs = SymbolicObservable("Z")
|
61
|
+
assert obs.expectation_value().evaluate() == 1
|
62
|
+
obs = SymbolicObservable("X", "a")
|
63
|
+
assert obs.expectation_value().evaluate({"a": 1}) == 0
|
64
|
+
|
65
|
+
|
66
|
+
def test_merge():
|
67
|
+
obs = SymbolicObservable([SymbolicPauliTerm("X", "a"), SymbolicPauliTerm("X", "a")])
|
68
|
+
obs.merge()
|
69
|
+
obs.simplify()
|
70
|
+
assert obs.size() == 1
|
71
|
+
assert obs[0].coefficient.to_string() == "a + a"
|
72
|
+
|
73
|
+
|
74
|
+
def test_truncate():
|
75
|
+
obs = SymbolicObservable([SymbolicPauliTerm("X", 0.1), SymbolicPauliTerm("Y", 0.9)])
|
76
|
+
truncator = SymbolicWeightTruncator(0) # Removes all non-identity
|
77
|
+
obs.truncate(truncator)
|
78
|
+
assert obs.size() == 0
|
79
|
+
|
80
|
+
|
81
|
+
def test_simplify():
|
82
|
+
obs = SymbolicObservable([SymbolicPauliTerm("X", "a"), SymbolicPauliTerm("X", 1.0)])
|
83
|
+
obs.merge()
|
84
|
+
assert obs[0].coefficient.to_string() == "a + 1.000"
|
85
|
+
obs.simplify()
|
86
|
+
assert obs[0].coefficient.to_string() == "1.000 + a" # No change
|
87
|
+
obs.simplify({"a": 2.0})
|
88
|
+
assert obs[0].coefficient.to_string() == "3.000"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|