Repository: qiboteam/qibo
Pull request #1649: [unitaryHack 2025] Graph state encoding
Author: login: victor-onofre
Labels: name: run-workflow
description: Forces github workflow execution
name: unitaryHACK 2025
description: Issues related to the unitaryHACK 2025
Created: 2025-05-28T08:16:54Z | Merged: 2025-05-30T13:06:02Z

Implementation of the graph state encoding in the `models.encodings`. This PR closes issue #1621 



Diff:
diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py
index bd6a0be7b4..8cb65207fd 100644
--- a/src/qibo/models/encodings.py
+++ b/src/qibo/models/encodings.py
@@ -1555,3 +1555,37 @@ def _add_scs_gate(circuit: Circuit, n: int, k: int):
             gates.RY(target_qubit, theta).controlled_by(control_qubit, last_qubit)
         )
         circuit.add(gates.CNOT(target_qubit, last_qubit))
+
+
+def graph_state(matrix, backend=None, **kwargs):
+    """Create circuit encoding an undirected graph state given its adjacency matrix.
+
+    Args:
+        matrix (ndarray or list): Adjacency matrix of the graph.
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    backend = _check_backend(backend)
+
+    if isinstance(matrix, list):
+        matrix = backend.cast(matrix, dtype=int)
+
+    if not backend.np.allclose(matrix, matrix.T):
+        raise_error(
+            ValueError,
+            f"``matrix`` is not symmetric, not representing an undirected graph",
+        )
+
+    nqubits = len(matrix)
+
+    circuit = Circuit(nqubits, **kwargs)
+    circuit.add(gates.H(qubit) for qubit in range(nqubits))
+
+    # since the matrix is symmetric, we only need the upper triangular part
+    rows, columns = backend.np.nonzero(backend.np.triu(matrix))
+    circuit.add(gates.CZ(int(ind_r), int(ind_c)) for ind_r, ind_c in zip(rows, columns))
+
+    return circuit
diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py
index 8c2845493c..04d7932148 100644
--- a/tests/test_models_encodings.py
+++ b/tests/test_models_encodings.py
@@ -4,6 +4,7 @@
 from functools import reduce
 from itertools import combinations
 
+import networkx as nx
 import numpy as np
 import pytest
 from scipy.optimize import curve_fit
@@ -18,6 +19,7 @@
     dicke_state,
     entangling_layer,
     ghz_state,
+    graph_state,
     hamming_weight_encoder,
     phase_encoder,
     sparse_encoder,
@@ -493,3 +495,61 @@ def test_dicke_state(backend, nqubits, weight, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix, expects_error, circuit1, circuit2",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False, True, False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+            False,
+            True,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        (
+            [[0, 1, 0], [0, 0, 1], [0, 1, 0]],
+            True,
+            False,
+            False,
+        ),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True, False, False),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix, expects_error, circuit1, circuit2):
+
+    if expects_error:
+        # We expect a ValueError for non-symmetric matrices
+        with pytest.raises(ValueError):
+            graph_state(matrix, backend=backend)
+
+    else:
+        if circuit1:
+            nqubits = 3
+            target = Circuit(nqubits)
+            target.add(gates.H(qubit) for qubit in range(nqubits))
+            target.add(gates.CZ(0, 1))
+            target.add(gates.CZ(1, 2))
+
+        if circuit2:
+            nqubits = 5
+            target = Circuit(nqubits)
+            target.add(gates.H(qubit) for qubit in range(nqubits))
+            target.add(gates.CZ(0, 1))
+            target.add(gates.CZ(0, 4))
+            target.add(gates.CZ(1, 2))
+            target.add(gates.CZ(2, 3))
+            target.add(gates.CZ(3, 4))
+
+        circuit = graph_state(matrix)
+        backend.assert_circuitclose(circuit, target)


Reviews:
user: login: renatomello
body: Thank you for this. I made some suggestions on how to improve the function and docstrings, especially with respect to backend compatibility.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T10:03:57Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T11:26:53Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T12:23:40Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T12:32:27Z
user: login: victor-onofre
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T12:34:23Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T14:38:57Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T14:56:58Z
user: login: victor-onofre
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T15:33:16Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T18:39:00Z
user: login: victor-onofre
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T08:35:13Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T09:19:09Z
user: login: victor-onofre
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T09:41:34Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:49:55Z
user: login: renatomello
body: This looks good to me. The PR will be merged after a second reviewer approves it.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:51:48Z
user: login: AlejandroSopena
body: Thanks. Everything looks good. Just two comments:
Please undo the changes to `qibo-draw-circuit-matplotlib.ipynb`.
GPU tests are failing. I suggest the solution below.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T10:00:58Z
user: login: renatomello
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T12:31:18Z
user: login: AlejandroSopena
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T13:05:44Z

Review comments:
diff_hunk: @@ -1483,3 +1483,39 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(adj_matrix, **kwargs):
+    """Circuit that creates the graph state of a given Adjacency matrix
+
+    Args:
+        adj_matrix (ndarray): Adjacency matrix
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
path: src/qibo/models/encodings.py
user: login: renatomello
body: ```suggestion
def graph_state(adj_matrix, backend=None, **kwargs):
    """Create circuit encoding an undirected graph state given its adjacency matrix.

    Args:
        matrix (ndarray or list): Adjacency matrix of the graph.
        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.

    Returns:
        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
    """
```
created_at: 2025-05-28T09:43:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,39 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(adj_matrix, **kwargs):
+    """Circuit that creates the graph state of a given Adjacency matrix
+
+    Args:
+        adj_matrix (ndarray): Adjacency matrix
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    if not isinstance(adj_matrix, np.ndarray):
+        raise_error(
+            TypeError,
+            f"Input given as type {type(adj_matrix)}. Input must be a NumPy array.",
+        )
path: src/qibo/models/encodings.py
user: login: renatomello
body: Since `qibo` works with platforms other than `numpy` (e.g. `cupy`, `torch`, `tensorflow`, etc), we use the definition of a backend for compatibility.

```suggestion
    backend = _check_backend(backend)

    if isinstance(matrix, list):
        matrix = backend.cast(matrix, dtype=type(matrix[0]))
```
created_at: 2025-05-28T09:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,39 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(adj_matrix, **kwargs):
+    """Circuit that creates the graph state of a given Adjacency matrix
+
+    Args:
+        adj_matrix (ndarray): Adjacency matrix
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    if not isinstance(adj_matrix, np.ndarray):
+        raise_error(
+            TypeError,
+            f"Input given as type {type(adj_matrix)}. Input must be a NumPy array.",
+        )
+
+    if not np.allclose(adj_matrix, adj_matrix.T):
+        raise_error(
+            ValueError,
+            f"Matrix is not symmetric.",
+        )
path: src/qibo/models/encodings.py
user: login: renatomello
body: ```suggestion
    if not backend.assert_allclose(matrix, matrix.T):
        raise_error(
            ValueError,
            f"``matrix`` is not symmetric, not representing an undirected graph.",
        )
```
created_at: 2025-05-28T09:50:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,39 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(adj_matrix, **kwargs):
+    """Circuit that creates the graph state of a given Adjacency matrix
+
+    Args:
+        adj_matrix (ndarray): Adjacency matrix
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    if not isinstance(adj_matrix, np.ndarray):
+        raise_error(
+            TypeError,
+            f"Input given as type {type(adj_matrix)}. Input must be a NumPy array.",
+        )
+
+    if not np.allclose(adj_matrix, adj_matrix.T):
+        raise_error(
+            ValueError,
+            f"Matrix is not symmetric.",
+        )
+
+    num_qubits = len(adj_matrix)
+    circuit = Circuit(num_qubits, **kwargs)
+
+    for qubits in range(num_qubits):
+        circuit.add(gates.H(qubits))
path: src/qibo/models/encodings.py
user: login: renatomello
body: ```suggestion
    nqubits = len(matrix)

    circuit = Circuit(nqubits, **kwargs)
    circuit.add(gates.H(qubit) for qubits in range(nqubits))
```
created_at: 2025-05-28T09:55:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,39 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(adj_matrix, **kwargs):
+    """Circuit that creates the graph state of a given Adjacency matrix
+
+    Args:
+        adj_matrix (ndarray): Adjacency matrix
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    if not isinstance(adj_matrix, np.ndarray):
+        raise_error(
+            TypeError,
+            f"Input given as type {type(adj_matrix)}. Input must be a NumPy array.",
+        )
+
+    if not np.allclose(adj_matrix, adj_matrix.T):
+        raise_error(
+            ValueError,
+            f"Matrix is not symmetric.",
+        )
+
+    num_qubits = len(adj_matrix)
+    circuit = Circuit(num_qubits, **kwargs)
+
+    for qubits in range(num_qubits):
+        circuit.add(gates.H(qubits))
+
+    for a in range(num_qubits):
+        for b in range(a + 1, num_qubits):
+            if adj_matrix[a][b] == 1:
+                circuit.add(gates.CZ(a, b))
path: src/qibo/models/encodings.py
user: login: renatomello
body: ```suggestion
    # since the matrix is symmetric, we only need the upper triangular part
    rows, columns = backend.np.nonzero(backend.np.triu(matrix))
    circuit.add(gates.CZ(ind_r, ind_c) for ind_r, ind_c in zip(rows, columns))
```
created_at: 2025-05-28T09:59:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,46 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+# Helper to create a symmetric adjacency matrix
+def create_symmetric_adj(size=3):
+    G = nx.generators.cycle_graph(size)
+    return nx.adjacency_matrix(G).todense()
+
+
+# A non-numpy array input
+non_numpy_input = [[1, 2], [2, 1]]
+
+
+@pytest.mark.parametrize(
+    "matrix_input, expected_error",
+    [
+        # Valid symmetric matrices (should NOT raise an error)
+        (create_symmetric_adj(5), None),
+        (np.array([[1, 2], [2, 1]]), None),  # Manually created symmetric
+        (np.identity(3), None),  # Identity matrix is symmetric
+        # Invalid inputs (should raise errors)
+        (non_numpy_input, TypeError),  # Not a numpy array
+        (None, TypeError),  # None input
+        (123, TypeError),  # Integer input
+        (
+            np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=float),
+            ValueError,
+        ),  # non-symmetric
+        (np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]), ValueError),  # non-symmetric
+    ],
+)
+def test_check_symmetric(matrix_input, expected_error):
+    if expected_error is None:
+        # For valid inputs, no error should be raised
+        try:
+            graph_state(matrix_input)
+            # If a print statement was inside, it would execute here.
+            # For tests, just successfully completing without error is the pass condition.
+        except Exception as e:
+            pytest.fail(f"Expected no error but got {type(e).__name__}: {e}")
+    else:
+        # For invalid inputs, the specific error should be raised
+        with pytest.raises(expected_error) as excinfo:
path: tests/test_models_encodings.py
user: login: renatomello
body: You could look at examples of tests in the module to see how to better write tests, for instance, using `pytest.raises` instead of a `try/except`. For instance,

https://github.com/qiboteam/qibo/blob/5fe998de11072407e12275819803e6fed49caf26/tests/test_models_encodings.py#L101
created_at: 2025-05-28T10:02:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,37 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(adj_matrix, backend=None, **kwargs):
+    """Create circuit encoding an undirected graph state given its adjacency matrix.
+
+    Args:
+        matrix (ndarray or list): Adjacency matrix of the graph.
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    backend = _check_backend(backend)
+
+    if isinstance(matrix, list):
+        matrix = backend.cast(matrix, dtype=type(matrix[0]))
+
+    if not backend.assert_allclose(matrix, matrix.T):
+        raise_error(
+            ValueError,
+            f"``matrix`` is not symmetric, not representing an undirected graph.",
+        )
+
+    nqubits = len(matrix)
+
+    circuit = Circuit(nqubits, **kwargs)
+    circuit.add(gates.H(qubit) for qubits in range(nqubits))
path: src/qibo/models/encodings.py
user: login: renatomello
body: ```suggestion
    circuit.add(gates.H(qubit) for qubit in range(nqubits))
```
created_at: 2025-05-28T11:26:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,37 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(matrix, backend=None, **kwargs):
+    """Create circuit encoding an undirected graph state given its adjacency matrix.
+
+    Args:
+        matrix (ndarray or list): Adjacency matrix of the graph.
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    backend = _check_backend(backend)
+
+    if isinstance(matrix, list):
+        matrix = backend.cast(matrix, dtype=type(matrix[0]))
+
+    if not backend.assert_allclose(np.array(matrix), np.array(matrix).T):
path: src/qibo/models/encodings.py
user: login: renatomello
body: ```suggestion
    if not backend.np.allclose(matrix, matrix.T):
```
created_at: 2025-05-28T12:23:39Z
author_association: CONTRIBUTOR
path: examples/circuit-draw-mpl/qibo-draw-circuit-matplotlib.ipynb
user: login: renatomello
body: Why is this file being changed?
created_at: 2025-05-28T12:32:27Z
author_association: CONTRIBUTOR
path: examples/circuit-draw-mpl/qibo-draw-circuit-matplotlib.ipynb
user: login: victor-onofre
body: My bad, I was doing some checks in the function using that notebook. I deleted my testing 
created_at: 2025-05-28T12:34:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,58 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        ([[0, 1, 0], [0, 0, 1], [0, 1, 0]], True),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix_data, expects_error):
+    matrix = np.array(matrix_data)
path: tests/test_models_encodings.py
user: login: renatomello
body: ```suggestion
    matrix = backend.cast(matrix_data, dtype=int)
```
created_at: 2025-05-28T14:38:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,58 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        ([[0, 1, 0], [0, 0, 1], [0, 1, 0]], True),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix_data, expects_error):
+    matrix = np.array(matrix_data)
+
+    if expects_error:
+        # We expect a ValueError for non-symmetric matrices
+        with pytest.raises(ValueError):
+            graph_state(matrix, backend=backend)
+
+    # If no error is expected, proceed with normal circuit construction and checks
+    nqubits = len(matrix)
+
+    # Create the graph state circuit
+    circuit = graph_state(matrix, backend=backend)
+
+    # Assertions about the constructed circuit
+    assert circuit.nqubits == nqubits
+
+    # Verify initial Hadamard gates
+    h_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.H))
+    assert h_gates_count == nqubits
+
+    # Verify CZ gates
+    cz_gates_expected = 0
+    # Count non-zero elements in the upper triangular part of the matrix
+    for a in range(nqubits):
+        for b in range(a + 1, nqubits):
+            if matrix[a, b] != 0:
+                cz_gates_expected += 1
+
+    cz_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.CZ))
+    assert cz_gates_count == cz_gates_expected
path: tests/test_models_encodings.py
user: login: renatomello
body: I think here you could hardcode the correct solution and use the `backend.assert_circuitclose` function.

https://github.com/qiboteam/qibo/blob/5fe998de11072407e12275819803e6fed49caf26/src/qibo/backends/abstract.py#L413
created_at: 2025-05-28T14:56:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,58 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        ([[0, 1, 0], [0, 0, 1], [0, 1, 0]], True),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix_data, expects_error):
+    matrix = np.array(matrix_data)
+
+    if expects_error:
+        # We expect a ValueError for non-symmetric matrices
+        with pytest.raises(ValueError):
+            graph_state(matrix, backend=backend)
+
+    # If no error is expected, proceed with normal circuit construction and checks
+    nqubits = len(matrix)
+
+    # Create the graph state circuit
+    circuit = graph_state(matrix, backend=backend)
+
+    # Assertions about the constructed circuit
+    assert circuit.nqubits == nqubits
+
+    # Verify initial Hadamard gates
+    h_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.H))
+    assert h_gates_count == nqubits
+
+    # Verify CZ gates
+    cz_gates_expected = 0
+    # Count non-zero elements in the upper triangular part of the matrix
+    for a in range(nqubits):
+        for b in range(a + 1, nqubits):
+            if matrix[a, b] != 0:
+                cz_gates_expected += 1
+
+    cz_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.CZ))
+    assert cz_gates_count == cz_gates_expected
path: tests/test_models_encodings.py
user: login: victor-onofre
body: No problem, I can do this tomorrow morning if you think it is necessary 
created_at: 2025-05-28T15:33:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,58 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        ([[0, 1, 0], [0, 0, 1], [0, 1, 0]], True),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix_data, expects_error):
+    matrix = np.array(matrix_data)
+
+    if expects_error:
+        # We expect a ValueError for non-symmetric matrices
+        with pytest.raises(ValueError):
+            graph_state(matrix, backend=backend)
+
+    # If no error is expected, proceed with normal circuit construction and checks
+    nqubits = len(matrix)
+
+    # Create the graph state circuit
+    circuit = graph_state(matrix, backend=backend)
+
+    # Assertions about the constructed circuit
+    assert circuit.nqubits == nqubits
+
+    # Verify initial Hadamard gates
+    h_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.H))
+    assert h_gates_count == nqubits
+
+    # Verify CZ gates
+    cz_gates_expected = 0
+    # Count non-zero elements in the upper triangular part of the matrix
+    for a in range(nqubits):
+        for b in range(a + 1, nqubits):
+            if matrix[a, b] != 0:
+                cz_gates_expected += 1
+
+    cz_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.CZ))
+    assert cz_gates_count == cz_gates_expected
path: tests/test_models_encodings.py
user: login: renatomello
body: No worries, after that I think we'll be ready.
created_at: 2025-05-28T18:39:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,58 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        ([[0, 1, 0], [0, 0, 1], [0, 1, 0]], True),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix_data, expects_error):
+    matrix = np.array(matrix_data)
+
+    if expects_error:
+        # We expect a ValueError for non-symmetric matrices
+        with pytest.raises(ValueError):
+            graph_state(matrix, backend=backend)
+
+    # If no error is expected, proceed with normal circuit construction and checks
+    nqubits = len(matrix)
+
+    # Create the graph state circuit
+    circuit = graph_state(matrix, backend=backend)
+
+    # Assertions about the constructed circuit
+    assert circuit.nqubits == nqubits
+
+    # Verify initial Hadamard gates
+    h_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.H))
+    assert h_gates_count == nqubits
+
+    # Verify CZ gates
+    cz_gates_expected = 0
+    # Count non-zero elements in the upper triangular part of the matrix
+    for a in range(nqubits):
+        for b in range(a + 1, nqubits):
+            if matrix[a, b] != 0:
+                cz_gates_expected += 1
+
+    cz_gates_count = sum(1 for gate in circuit.queue if isinstance(gate, gates.CZ))
+    assert cz_gates_count == cz_gates_expected
path: tests/test_models_encodings.py
user: login: victor-onofre
body: Hi @renatomello, i did the modifications. The pytest files passed locally, I think all looks good. Let me know what you think. Thanks!
created_at: 2025-05-29T08:35:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,26 +491,27 @@ def test_graph_state(backend, matrix_data, expects_error):
             graph_state(matrix, backend=backend)
 
     else:
-        # If no error is expected, proceed with normal circuit construction and checks
-        nqubits = len(matrix)
+        if circuit1:
+            nqubits = 3
+            circuit = Circuit(nqubits)
+            circuit.add(gates.H(qubit) for qubit in range(nqubits))
+            circuit.add(gates.CZ(0, 1))
+            circuit.add(gates.CZ(1, 2))
+
+            circuit_target1 = graph_state(matrix)
+            backend.assert_circuitclose(circuit, circuit_target1)
+        
+        if circuit2:
+            nqubits2 = 5
+            circuit2 = Circuit(nqubits2)
+            circuit2.add(gates.H(qubit) for qubit in range(nqubits2))
+            circuit2.add(gates.CZ(0, 1))
+            circuit2.add(gates.CZ(0, 4))
+            circuit2.add(gates.CZ(1, 2))
+            circuit2.add(gates.CZ(2, 3))
+            circuit2.add(gates.CZ(3, 4))
+
+            circuit_target2 = graph_state(matrix)
+            backend.assert_circuitclose(circuit2, circuit_target2)
path: tests/test_models_encodings.py
user: login: renatomello
body: ```suggestion
        if circuit1:
            nqubits = 3
            target = Circuit(nqubits)
            target.add(gates.H(qubit) for qubit in range(nqubits))
            target.add(gates.CZ(0, 1))
            target.add(gates.CZ(1, 2))
        
        if circuit2:
            nqubits = 5
            target = Circuit(nqubits)
            target.add(gates.H(qubit) for qubit in range(nqubits))
            target.add(gates.CZ(0, 1))
            target.add(gates.CZ(0, 4))
            target.add(gates.CZ(1, 2))
            target.add(gates.CZ(2, 3))
            target.add(gates.CZ(3, 4))

        circuit = graph_state(matrix)
        backend.assert_circuitclose(circuit, target)
```
created_at: 2025-05-29T09:19:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,26 +491,27 @@ def test_graph_state(backend, matrix_data, expects_error):
             graph_state(matrix, backend=backend)
 
     else:
-        # If no error is expected, proceed with normal circuit construction and checks
-        nqubits = len(matrix)
+        if circuit1:
+            nqubits = 3
+            circuit = Circuit(nqubits)
+            circuit.add(gates.H(qubit) for qubit in range(nqubits))
+            circuit.add(gates.CZ(0, 1))
+            circuit.add(gates.CZ(1, 2))
+
+            circuit_target1 = graph_state(matrix)
+            backend.assert_circuitclose(circuit, circuit_target1)
+        
+        if circuit2:
+            nqubits2 = 5
+            circuit2 = Circuit(nqubits2)
+            circuit2.add(gates.H(qubit) for qubit in range(nqubits2))
+            circuit2.add(gates.CZ(0, 1))
+            circuit2.add(gates.CZ(0, 4))
+            circuit2.add(gates.CZ(1, 2))
+            circuit2.add(gates.CZ(2, 3))
+            circuit2.add(gates.CZ(3, 4))
+
+            circuit_target2 = graph_state(matrix)
+            backend.assert_circuitclose(circuit2, circuit_target2)
path: tests/test_models_encodings.py
user: login: victor-onofre
body: It makes sense, it's a cleaner way of implementing the check. I pushed the modifications
created_at: 2025-05-29T09:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -454,3 +456,62 @@ def test_ghz_circuit(backend, nqubits, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error, circuit1, circuit2",
+    [
+        # Test Case 1: 3-qubit graph
+        ([[0, 1, 0], [1, 0, 1], [0, 1, 0]], False, True, False),
+        # Test Case 2: 5-qubit  graph
+        (
+            [
+                [0, 1, 0, 0, 1],
+                [1, 0, 1, 0, 0],
+                [0, 1, 0, 1, 0],
+                [0, 0, 1, 0, 1],
+                [1, 0, 0, 1, 0],
+            ],
+            False,
+            False,
+            True,
+        ),
+        # Test Case 3: Non-symmetric matrix (expected error)
+        (
+            [[0, 1, 0], [0, 0, 1], [0, 1, 0]],
+            True,
+            False,
+            False,
+        ),  # matrix[1,0] != matrix[0,1]
+        # Test Case 4: Non-symmetric matrix (expected error)
+        ([[0, 1], [0, 0]], True, False, False),  # matrix[1,0] != matrix[0,1]
+    ],
+)
+def test_graph_state(backend, matrix_data, expects_error, circuit1, circuit2):
+    matrix = backend.cast(matrix_data, dtype=int)
path: tests/test_models_encodings.py
user: login: renatomello
body: ```suggestion
```

Removing this line will fix the coverage failure.
created_at: 2025-05-30T08:49:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1483,3 +1483,37 @@ def _get_phase_gate_correction_sparse(
         gate = _get_phase_gate_correction(last_string, phis[-1])
 
     return gate
+
+
+def graph_state(matrix, backend=None, **kwargs):
+    """Create circuit encoding an undirected graph state given its adjacency matrix.
+
+    Args:
+        matrix (ndarray or list): Adjacency matrix of the graph.
+        kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+            For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+    Returns:
+        :class:`qibo.models.circuit.Circuit`:  Circuit of the graph state with the given Adjacency matrix.
+    """
+    backend = _check_backend(backend)
+
+    if isinstance(matrix, list):
+        matrix = backend.cast(matrix, dtype=int)
+
+    if not backend.np.allclose(matrix, matrix.T):
+        raise_error(
+            ValueError,
+            f"``matrix`` is not symmetric, not representing an undirected graph",
+        )
+
+    nqubits = len(matrix)
+
+    circuit = Circuit(nqubits, **kwargs)
+    circuit.add(gates.H(qubit) for qubit in range(nqubits))
+
+    # since the matrix is symmetric, we only need the upper triangular part
+    rows, columns = backend.np.nonzero(backend.np.triu(matrix))
+    circuit.add(gates.CZ(ind_r, ind_c) for ind_r, ind_c in zip(rows, columns))
path: src/qibo/models/encodings.py
user: login: AlejandroSopena
body: ```suggestion
    circuit.add(gates.CZ(int(ind_r), int(ind_c)) for ind_r, ind_c in zip(rows, columns))
```

Without this GPU tests fail
created_at: 2025-05-30T09:56:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -493,3 +495,61 @@ def test_dicke_state(backend, nqubits, weight, density_matrix):
             target = backend.np.outer(target, backend.np.conj(target.T))
 
         backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize(
+    "matrix_data, expects_error, circuit1, circuit2",
path: tests/test_models_encodings.py
user: login: renatomello
body: ```suggestion
    "matrix, expects_error, circuit1, circuit2",
```
created_at: 2025-05-30T12:31:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-28T09:23:42Z
body: ## [Codecov](https://app.codecov.io/gh/qiboteam/qibo/pull/1649?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.13%. Comparing base [(`b059919`)](https://app.codecov.io/gh/qiboteam/qibo/commit/b0599197de3c27b9969353181d4662a57e6d9a8e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam) to head [(`4a680b5`)](https://app.codecov.io/gh/qiboteam/qibo/commit/4a680b573f7066c8283132e6aa2f9641b70087e6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam).
> Report is 31 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #1649   +/-   ##
=======================================
  Coverage   99.13%   99.13%           
=======================================
  Files          80       80           
  Lines       12150    12162   +12     
=======================================
+ Hits        12045    12057   +12     
  Misses        105      105           
```

| [Flag](https://app.codecov.io/gh/qiboteam/qibo/pull/1649/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/qiboteam/qibo/pull/1649/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam) | `99.13% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/qiboteam/qibo/pull/1649?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qiboteam).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: victor-onofre
created_at: 2025-05-28T12:00:22Z
body: Hi @renatomello, thanks for the quick response and the comments. I did the modifications as suggested, but it seems I have something weird with the line ` if not backend.assert_allclose(np.array(matrix), np.array(matrix).T)`, sometimes it shows an error when it should not. Let me know what you think now, and I will keep checking the reason for the error.Thanks! 

I think the error is in my local libraries given that the commit passed the checks 
author_association: CONTRIBUTOR
user: login: renatomello
created_at: 2025-05-28T12:24:37Z
body: > Hi @renatomello, thanks for the quick response and the comments. I did the modifications as suggested, but it seems I have something weird with the line ` if not backend.assert_allclose(np.array(matrix), np.array(matrix).T)`, sometimes it shows an error when it should not. Let me know what you think now, and I will keep checking the reason for the error.Thanks!
> 
> I think the error is in my local libraries given that the commit passed the checks

Sorry, that was an error I introduced in one of my suggestions. I just suggested the fix though.
author_association: CONTRIBUTOR
user: login: victor-onofre
created_at: 2025-05-28T12:33:30Z
body: No problem @renatomello, i think i fixed the error with this:
```
  if backend.assert_allclose(np.array(matrix, dtype=float), np.array(matrix, dtype=float).T):
        raise_error(
            ValueError,
            f"``matrix`` is not symmetric, not representing an undirected graph.",
        )
```
If I understand correctly, you have your own numpy functions in the backend

author_association: CONTRIBUTOR
user: login: victor-onofre
created_at: 2025-05-28T13:17:20Z
body: @renatomello I think i need help with the testing function, I don't understand where I have an error. I did the modification for the value error with `  if not backend.np.allclose(matrix, matrix.T):`, but still have some errors in the pytest. Sorry for the trouble! 
author_association: CONTRIBUTOR
user: login: renatomello
created_at: 2025-05-28T14:44:49Z
body: 


> @renatomello I think i need help with the testing function, I don't understand where I have an error. I did the modification for the value error with ` if not backend.np.allclose(matrix, matrix.T):`, but still have some errors in the pytest. Sorry for the trouble!

I think you're missing an `else` after the `if expects_error`.
author_association: CONTRIBUTOR
user: login: victor-onofre
created_at: 2025-05-28T15:31:47Z
body: > > @renatomello I think i need help with the testing function, I don't understand where I have an error. I did the modification for the value error with ` if not backend.np.allclose(matrix, matrix.T):`, but still have some errors in the pytest. Sorry for the trouble!
> 
> I think you're missing an `else` after the `if expects_error`.

You are right, i did the change and the tests passed. A really simple error
author_association: CONTRIBUTOR
user: login: victor-onofre
created_at: 2025-05-29T13:51:02Z
body: There was a warning in the [codecov check.](https://app.codecov.io/gh/qiboteam/qibo/pull/1649) It didn't like this line:
 ` matrix = backend.cast(matrix, dtype=type(matrix[0][0]))` ,
 I changed it to:
`matrix = backend.cast(matrix, dtype=int)`

I hope this fixes the problem
author_association: CONTRIBUTOR
user: login: victor-onofre
created_at: 2025-05-30T11:32:33Z
body: Hi @AlejandroSopena, I have undone the changes to the notebook, and I saw you commit the changes to avoid GPU issues. Let me know if there are more problems. Thanks!
author_association: CONTRIBUTOR