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.
Files changed (61) hide show
  1. {pyrauli-0.3.2 → pyrauli-0.4.0}/CMakeLists.txt +2 -1
  2. {pyrauli-0.3.2 → pyrauli-0.4.0}/PKG-INFO +4 -1
  3. pyrauli-0.4.0/benchmarks/test_benchmark_qaoa.py +43 -0
  4. pyrauli-0.4.0/docs/guides/how_to_symbolic.rst +99 -0
  5. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/index.rst +1 -0
  6. {pyrauli-0.3.2 → pyrauli-0.4.0}/pyproject.toml +5 -1
  7. {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/__init__.py +17 -0
  8. {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/_core/bindings.cpp +333 -7
  9. pyrauli-0.4.0/tests/snippets/test_symbolic_circuit_noise.py +22 -0
  10. pyrauli-0.4.0/tests/snippets/test_symbolic_circuit_snippet.py +19 -0
  11. pyrauli-0.4.0/tests/snippets/test_symbolic_coefficient_guide.py +53 -0
  12. pyrauli-0.4.0/tests/snippets/test_symbolic_observable_guide.py +46 -0
  13. pyrauli-0.4.0/tests/snippets/test_symbolic_sympy.py +24 -0
  14. pyrauli-0.4.0/tests/test_symbolic.py +12 -0
  15. pyrauli-0.4.0/tests/test_symbolic_circuit.py +61 -0
  16. pyrauli-0.4.0/tests/test_symbolic_coefficient.py +76 -0
  17. pyrauli-0.4.0/tests/test_symbolic_observable.py +88 -0
  18. {pyrauli-0.3.2 → pyrauli-0.4.0}/.clang-format +0 -0
  19. {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/benchmark.yml +0 -0
  20. {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/ci.yml +0 -0
  21. {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/doc.yml +0 -0
  22. {pyrauli-0.3.2 → pyrauli-0.4.0}/.github/workflows/pypi.yml +0 -0
  23. {pyrauli-0.3.2 → pyrauli-0.4.0}/.gitignore +0 -0
  24. {pyrauli-0.3.2 → pyrauli-0.4.0}/LICENSE +0 -0
  25. {pyrauli-0.3.2 → pyrauli-0.4.0}/README.md +0 -0
  26. {pyrauli-0.3.2 → pyrauli-0.4.0}/benchmarks/test_benchmark_circuit.py +0 -0
  27. {pyrauli-0.3.2 → pyrauli-0.4.0}/benchmarks/test_benchmark_observable.py +0 -0
  28. {pyrauli-0.3.2 → pyrauli-0.4.0}/benchmarks/test_benchmark_qiskit.py +0 -0
  29. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/.gitignore +0 -0
  30. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/Makefile +0 -0
  31. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/_static/.gitkeep +0 -0
  32. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/_templates/.gitkeep +0 -0
  33. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/conf.py +0 -0
  34. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/explanation/theory.rst +0 -0
  35. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_circuit.rst +0 -0
  36. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_complexity.rst +0 -0
  37. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_noise.rst +0 -0
  38. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_observables.rst +0 -0
  39. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/guides/how_to_qiskit.rst +0 -0
  40. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/make.bat +0 -0
  41. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/reference/api.rst +0 -0
  42. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/requirements.txt +0 -0
  43. {pyrauli-0.3.2 → pyrauli-0.4.0}/docs/tutorials/getting_started.rst +0 -0
  44. {pyrauli-0.3.2 → pyrauli-0.4.0}/examples/qiskit_backend.py +0 -0
  45. {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/backend.py +0 -0
  46. {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/converters.py +0 -0
  47. {pyrauli-0.3.2 → pyrauli-0.4.0}/src/pyrauli/estimator.py +0 -0
  48. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_basic_circuit.py +0 -0
  49. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_observable_evolution.py +0 -0
  50. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_qiskit_backend_usage.py +0 -0
  51. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/snippets/test_readme.py +0 -0
  52. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_backend.py +0 -0
  53. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_circuit.py +0 -0
  54. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_circuit_qiskit.py +0 -0
  55. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_noise_model.py +0 -0
  56. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_observable.py +0 -0
  57. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_observable_qiskit.py +0 -0
  58. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_pauli.py +0 -0
  59. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_policies.py +0 -0
  60. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_pyrauli.py +0 -0
  61. {pyrauli-0.3.2 → pyrauli-0.4.0}/tests/test_truncator.py +0 -0
@@ -15,7 +15,8 @@ FetchContent_Declare(
15
15
  FetchContent_Declare(
16
16
  propauli
17
17
  GIT_REPOSITORY https://github.com/zefresk/propauli.git
18
- GIT_TAG v2.1.0
18
+ #GIT_TAG v2.1.0
19
+ GIT_TAG symbolic
19
20
  )
20
21
 
21
22
  FetchContent_MakeAvailable(pybind11 propauli)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pyrauli
3
- Version: 0.3.2
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.
@@ -31,6 +31,7 @@ the library for the first time or need a specific technical detail.
31
31
  guides/how_to_complexity
32
32
  guides/how_to_qiskit
33
33
  guides/how_to_noise
34
+ guides/how_to_symbolic
34
35
 
35
36
  .. toctree::
36
37
  :maxdepth: 2
@@ -5,7 +5,7 @@ build-backend = "scikit_build_core.build"
5
5
 
6
6
  [project]
7
7
  name = "pyrauli"
8
- version = "0.3.2"
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, Truncator<coeff_t>, WeightTruncatorPtr>(
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, Truncator<coeff_t>, NeverTruncatorPtr>(m, "NeverTruncator",
213
- "A truncator that never removes any terms.")
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,12 @@
1
+ import pytest
2
+
3
+ from pyrauli import SymbolicCoefficient, SymbolicObservable
4
+
5
+ def test_coeff_ops():
6
+ x = SymbolicCoefficient(1.)
7
+ x *= 2.
8
+ x /= 2
9
+ x += 1
10
+ x -= 1
11
+ assert x.evaluate() == 1.
12
+
@@ -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