Repository: qiboteam/qibo

Issue:
Expectation for observable does not take into account constants
I was fiddling around with a QAOA MaxCut Hamiltonian by feeding it into the ZNE as observable and stumbled upon some weird stuff.

The QAOA MaxCut Hamiltonian has the form $H = \sum_{<i,j> \in E} 0.5 ( 1 - Z_i Z_j)$, where the $Z_i$ and $Z_j$ are $Z$ operators on the $i$-th and $j$-th qubits. 

For a simple fully connected 3 nodes graph (thus 3 qubits), $H$ has the form $H = 0.5 (1-Z(0) Z(1)) + 0.5  (1-Z(1) Z(2)) + 0.5 (1-Z(2) Z(0))$. We can simplify this to be $H = 1.5 - 0.5 Z(0) Z(1) - 0.5 Z(1) Z(2) - 0.5 Z(2) Z(0)$. 

To compute the expectation value of the observable, we input observable $H = 1.5 - 0.5 Z(0) Z(1) - 0.5 Z(1) Z(2) - 0.5 Z(2) Z(0)$. The answer turns out wrong as the computation doesn't take into account the constant, which in this case is $1.5$. It does take into account the $0.5$ for each $Z(i) Z(j)$. 

Therefore to properly compute H, we need to do two separate computations: First to compute $0.5 Z(0) Z(1) - 0.5 Z(1) Z(2) - 0.5 Z(2) Z(0)$, then we subtract it from $1.5$. 
 
I was just wondering if this should be an issue because it's not intuitive to do two separate computations for a Hamiltonian that has constants.

Discussion:

Patch:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index aa20172f8..296254ee4 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -15,7 +15,7 @@ jobs:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
         python-version: [ 3.9, '3.10', '3.11', '3.12']
-    uses: qiboteam/workflows/.github/workflows/deploy-pip-poetry.yml@v1
+    uses: qiboteam/workflows/.github/workflows/deploy-pip-poetry.yml@main
     with:
       os: ${{ matrix.os }}
       python-version: ${{ matrix.python-version }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index ab3f1beb0..c0b9fdf54 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -28,7 +28,7 @@ jobs:
 
   deploy-docs:
     needs: [evaluate-label]
-    uses: qiboteam/workflows/.github/workflows/deploy-ghpages-latest-stable.yml@v1
+    uses: qiboteam/workflows/.github/workflows/deploy-ghpages-latest-stable.yml@main
     with:
       python-version: "3.10"
       package-manager: "poetry"
diff --git a/.github/workflows/rules.yml b/.github/workflows/rules.yml
index 1308a9180..6d2abafcc 100644
--- a/.github/workflows/rules.yml
+++ b/.github/workflows/rules.yml
@@ -14,8 +14,8 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.9, "3.10", "3.11", "3.12"]
-    uses: qiboteam/workflows/.github/workflows/rules-poetry.yml@v1
+        python-version: [ 3.9, '3.10', '3.11', '3.12']
+    uses: qiboteam/workflows/.github/workflows/rules-poetry.yml@main
     with:
       os: ${{ matrix.os }}
       python-version: ${{ matrix.python-version }}
diff --git a/.github/workflows/selfhosted.yml b/.github/workflows/selfhosted.yml
index d46b55142..ccce3531c 100644
--- a/.github/workflows/selfhosted.yml
+++ b/.github/workflows/selfhosted.yml
@@ -12,7 +12,7 @@ concurrency:
 jobs:
   build:
     if: contains(join(github.event.pull_request.labels.*.name), 'run-on')
-    uses: qiboteam/workflows/.github/workflows/selfhosted.yml@v1
+    uses: qiboteam/workflows/.github/workflows/selfhosted.yml@main
     with:
       used-labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}
       python-version: "3.10"
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 335bbf749..000000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# Qibo Code of Conduct
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to make participation in our project and our
-community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, gender identity and expression, level of
-experience, nationality, personal appearance, race, religion, or sexual identity
-and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-*   Using welcoming and inclusive language.
-*   Being respectful of differing viewpoints and experiences.
-*   Gracefully accepting constructive criticism.
-*   Focusing on what is best for the community.
-*   Showing empathy towards other community members.
-
-Examples of unacceptable behavior by participants include:
-
-*   The use of sexualized language or imagery and unwelcome sexual attention or
-    advances.
-*   Trolling, insulting/derogatory comments, and personal or political attacks.
-*   Public or private harassment.
-*   Publishing others' private information, such as a physical or electronic
-    address, without explicit permission.
-*   Conduct which could reasonably be considered inappropriate for the forum in
-    which it occurs.
-
-All Qibo forums and spaces are meant for professional interactions, and any
-behavior which could reasonably be considered inappropriate in a professional
-setting is unacceptable.
-
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, or to ban temporarily or permanently any
-contributor for other behaviors that they deem inappropriate, threatening,
-offensive, or harmful.
-
-
-## Scope
-
-This Code of Conduct applies to all content on https://qibo.science, Qibo's
-GitHub organization, or any other official Qibo web presence allowing for
-community interactions, as well as at all official Qibo events, whether offline
-or online.
-
-The Code of Conduct also applies within project spaces and in public spaces
-whenever an individual is representing Qibo or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-or de facto representative at an online or offline event.
-
-
-## Conflict Resolution
-
-Conflicts in an open source project can take many forms, from someone having a
-bad day and using harsh and hurtful language in the issue queue, to more serious
-instances such as sexist/racist statements or threats of violence, and
-everything in between.
-
-If the behavior is threatening or harassing, or for other reasons requires
-immediate escalation, please see below.
-
-However, for the vast majority of issues, we aim to empower individuals to first
-resolve conflicts themselves, asking for help when needed, and only after that
-fails to escalate further. This approach gives people more control over the
-outcome of their dispute.
-
-If you are experiencing or witnessing conflict, we ask you to use the following
-escalation strategy to address the conflict:
-
-1.  Address the perceived conflict directly with those involved, preferably in a
-    real-time medium.
-2.  If this fails, get a third party (e.g. a mutual friend, and/or someone with
-    background on the issue, but not involved in the conflict) to intercede.
-3.  If you are still unable to resolve the conflict, and you believe it rises to
-    harassment or another code of conduct violation, report it.
-
-## Reporting Violations
-
-Violations of the Code of Conduct can be reported to Qibo's Project Coordinator,
-Stefano Carrazza (stefano.carrazza@unimi.it). The Project Coordinator will
-determine whether the Code of Conduct was violated, and will issue an
-appropriate sanction, possibly including a written warning or expulsion from the
-project, project sponsored spaces, or project forums. We ask that you make a
-good-faith effort to resolve your conflict via the conflict resolution policy
-before submitting a report.
-
-Violations of the Code of Conduct can occur in any setting, even those unrelated
-to the project. We will only consider complaints about conduct that has occurred
-within one year of the report.
-
-
-## Enforcement
-
-If the Project Coordinator receive a report alleging a violation of the Code of
-Conduct, the Project Coordinator will notify the accused of the report, and
-provide them an opportunity to discuss the report before a sanction is issued.
-The Project Coordinator will do his utmost to keep the reporter anonymous. If
-the act is ongoing (such as someone engaging in harassment), or involves a
-threat to anyone's safety (e.g. threats of violence), the Project Coordinator
-may issue sanctions without notice.
-
-
-## Attribution
-
-This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
-available at https://contributor-covenant.org/version/1/4.
diff --git a/poetry.lock b/poetry.lock
index d8a82c8df..ff6c77d0f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
 [[package]]
 name = "absl-py"
@@ -211,13 +211,13 @@ files = [
 
 [[package]]
 name = "certifi"
-version = "2024.7.4"
+version = "2024.6.2"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
-    {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+    {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+    {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
 ]
 
 [[package]]
@@ -2262,7 +2262,6 @@ description = "Clang Python Bindings, mirrored from the official LLVM repo: http
 optional = false
 python-versions = "*"
 files = [
-    {file = "libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a"},
     {file = "libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5"},
     {file = "libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8"},
     {file = "libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b"},
@@ -2998,7 +2997,6 @@ description = "Nvidia JIT LTO Library"
 optional = false
 python-versions = ">=3"
 files = [
-    {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_aarch64.whl", hash = "sha256:004186d5ea6a57758fd6d57052a123c73a4815adf365eb8dd6a85c9eaa7535ff"},
     {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d9714f27c1d0f0895cd8915c07a87a1d0029a0aa36acaf9156952ec2a8a12189"},
     {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-win_amd64.whl", hash = "sha256:c3401dc8543b52d3a8158007a0c1ab4e9c768fcbd24153a48c86972102197ddd"},
 ]
diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py
index b270c5d00..a2fc70d4b 100644
--- a/src/qibo/hamiltonians/hamiltonians.py
+++ b/src/qibo/hamiltonians/hamiltonians.py
@@ -328,8 +328,8 @@ class SymbolicHamiltonian(AbstractHamiltonian):
             self.nqubits = nqubits
 
     @property
-    def dense(self):
-        """Creates the equivalent :class:`qibo.hamiltonians.MatrixHamiltonian`."""
+    def dense(self) -> "MatrixHamiltonian":
+        """Creates the equivalent Hamiltonian matrix."""
         if self._dense is None:
             log.warning(
                 "Calculating the dense form of a symbolic Hamiltonian. "
@@ -380,8 +380,9 @@ class SymbolicHamiltonian(AbstractHamiltonian):
 
     @property
     def terms(self):
-        """List of :class:`qibo.core.terms.HamiltonianTerm` objects
-        of which the Hamiltonian is a sum of.
+        """List of terms of which the Hamiltonian is a sum of.
+
+        Terms will be objects of type :class:`qibo.core.terms.HamiltonianTerm`.
         """
         if self._terms is None:
             # Calculate terms based on ``self.form``
@@ -407,7 +408,10 @@ class SymbolicHamiltonian(AbstractHamiltonian):
 
     @property
     def matrix(self):
-        """Returns the full ``(2 ** nqubits, 2 ** nqubits)`` matrix representation."""
+        """Returns the full matrix representation.
+
+        Consisting of ``(2 ** nqubits, 2 ** nqubits)`` elements.
+        """
         return self.dense.matrix
 
     def eigenvalues(self, k=6):
@@ -495,17 +499,16 @@ class SymbolicHamiltonian(AbstractHamiltonian):
 
         return result
 
-    def _calculate_dense_from_form(self):
-        """Calculates equivalent :class:`qibo.core.hamiltonians.Hamiltonian` using symbolic form.
+    def _calculate_dense_from_form(self) -> Hamiltonian:
+        """Calculates equivalent Hamiltonian using symbolic form.
+
         Useful when the term representation is not available.
         """
         matrix = self._get_symbol_matrix(self.form)
         return Hamiltonian(self.nqubits, matrix, backend=self.backend)
 
-    def _calculate_dense_from_terms(self):
-        """Calculates equivalent :class:`qibo.core.hamiltonians.Hamiltonian`
-        using the term representation.
-        """
+    def _calculate_dense_from_terms(self) -> Hamiltonian:
+        """Calculates equivalent Hamiltonian using the term representation."""
         if 2 * self.nqubits > len(EINSUM_CHARS):  # pragma: no cover
             # case not tested because it only happens in large examples
             raise_error(NotImplementedError, "Not enough einsum characters.")
@@ -547,7 +550,6 @@ class SymbolicHamiltonian(AbstractHamiltonian):
                 raise_error(NotImplementedError, "Z^k is not implemented since Z^2=I.")
         keys = list(freq.keys())
         counts = np.array(list(freq.values())) / sum(freq.values())
-        coeff = list(self.form.as_coefficients_dict().values())
         qubits = []
         for term in terms:
             qubits_term = []
@@ -566,8 +568,8 @@ class SymbolicHamiltonian(AbstractHamiltonian):
                 if subk.count(1) % 2 == 1:
                     expval_k = -1
                 expval_q += expval_k * counts[i]
-            expval += expval_q * float(coeff[j])
-        return expval
+            expval += expval_q * self.terms[j].coefficient.real
+        return expval + self.constant.real
 
     def __add__(self, o):
         if isinstance(o, self.__class__):
@@ -682,7 +684,10 @@ class SymbolicHamiltonian(AbstractHamiltonian):
         return new_ham
 
     def apply_gates(self, state, density_matrix=False):
-        """Applies gates corresponding to the Hamiltonian terms to a given state.
+        """Applies gates corresponding to the Hamiltonian terms.
+
+        Gates are applied to the given state.
+
         Helper method for ``__matmul__``.
         """
         total = 0


Test patch:
diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py
index d9a3b597e..9ddc54531 100644
--- a/tests/test_hamiltonians_symbolic.py
+++ b/tests/test_hamiltonians_symbolic.py
@@ -3,6 +3,7 @@
 import numpy as np
 import pytest
 import sympy
+from pytest import approx
 
 from qibo import Circuit, gates, hamiltonians
 from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
@@ -385,3 +386,13 @@ def test_trotter_hamiltonian_operation_errors(backend):
     h2 = hamiltonians.XXZ(3, dense=False, backend=backend)
     with pytest.raises(NotImplementedError):
         h = h1 @ h2
+
+
+def test_symbolic_hamiltonian_with_constant(backend):
+    c = Circuit(1)
+    c.add(gates.H(0))
+    c.add(gates.M(0))
+    h = hamiltonians.SymbolicHamiltonian(1e6 - Z(0), backend=backend)
+
+    result = c.execute(nshots=10000)
+    assert result.expectation_from_samples(h) == approx(1e6, rel=1e-5, abs=0.0)