From e425f5274a35ae62bf582ed52993d9ec37709e07 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 7 Aug 2025 10:30:24 +0200 Subject: [PATCH 1/3] feat: allowing to set even not trainable gates --- src/qibo/backends/abstract.py | 130 ++++++++++++++++++++++++++ src/qibo/hamiltonians/hamiltonians.py | 3 +- src/qibo/models/circuit.py | 26 ++++-- src/qibo/symbols.py | 3 +- 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index 6e63de00cc..5b4d7f6bf8 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -1,7 +1,10 @@ import abc from typing import Optional, Union +from qibo import gates from qibo.config import raise_error +from qibo.hamiltonians import SymbolicHamiltonian +from qibo.symbols import Z class Backend(abc.ABC): @@ -416,6 +419,133 @@ def calculate_expectation_density_matrix( """Calculate expectation value of a density matrix given the observable matrix.""" raise_error(NotImplementedError) + def calculate_expectation_from_samples_symbolic( + self, hamiltonian, frequencies: dict, qubit_map: Union[tuple[int], list[int]] + ): + """ + Calculate the expectation value from the samples. + The observable has to be diagonal in the computational basis. + + Args: + hamiltonian (SymbolicHamiltonian): observable to calculate the expectation value of. + freq (dict): input frequencies of the samples. + qubit_map (list): qubit map. + + Returns: + (float): the calculated expectation value. + """ + for term in hamiltonian.terms: + # pylint: disable=E1101 + for factor in term.factors: + if not isinstance(factor, Z): + raise_error( + NotImplementedError, "Observable is not a Pauli-Z string." + ) + + if qubit_map is None: + qubit_map = list(range(hamiltonian.nqubits)) + + keys = list(frequencies.keys()) + counts = list(frequencies.values()) + counts = self.cast(counts, dtype=self.np.float64) / sum(counts) + expvals = [] + for term in hamiltonian.terms: + qubits = { + factor.target_qubit for factor in term.factors if factor.name[0] != "I" + } + expvals.extend( + [ + term.coefficient.real + * (-1) ** [state[qubit_map.index(q)] for q in qubits].count("1") + for state in keys + ] + ) + expvals = self.cast(expvals, dtype=counts.dtype).reshape( + len(hamiltonian.terms), len(frequencies) + ) + return self.np.sum(expvals @ counts) + hamiltonian.constant.real + + def calculate_expectation_from_circuit( + self, hamiltonian, circuit, nshots: int = 1000 + ): + """ + Calculate the expectation value from a circuit. + This even works for observables not completely diagonal in the computational + basis, but only diagonal at a term level in a defined basis. Namely, for + an observable of the form :math:``H = \\sum_i H_i``, where each :math:``H_i`` + consists in a `n`-qubits pauli operator :math:`P_0 \\otimes P_1 \\otimes \\cdots \\otimes P_n`, + the expectation value is computed by rotating the input circuit in the suitable + basis for each term :math:``H_i`` thus extracting the `term-wise` expectations + that are then summed to build the global expectation value. + Each term of the observable is treated separately, by measuring in the correct + basis and re-executing the circuit. + + Args: + hamiltonian (SymbolicHamiltonian): observable to calculate the expectation value of. + circuit (Circuit): input circuit. + nshots (int): number of shots, defaults to 1000. + + Returns: + (float): the calculated expectation value. + """ + # from qibo import gates + # from qibo.symbols import Z + # from qibo.hamiltonians import SymbolicHamiltonian + + rotated_circuits = [] + Z_observables = [] + qubit_maps = [] + # loop over the terms that can be diagonalized simultaneously + for terms in hamiltonian.diagonal_terms: + # for each term that can be diagonalized simultaneously + # extract the coefficient, Z observable and qubit map + # the basis rotation, instead, will be the same + tmp_obs = [] + measurements = {} + for term in terms: + # Only care about non-I terms + non_identity_factors = [] + # prepare the measurement basis and append it to the circuit + for factor in term.factors: + if factor.name[0] != "I": + non_identity_factors.append(factor) + q = factor.target_qubit + if q not in measurements: + measurements[q] = gates.M(q, basis=factor.gate.__class__) + + # build diagonal observable + # including the coefficient + symb_obs = prod( + Z(factor.target_qubit) for factor in non_identity_factors + ) + symb_obs = SymbolicHamiltonian( + term.coefficient * symb_obs, + nqubits=circuit.nqubits, + backend=self, + ) + tmp_obs.append(symb_obs) + + # Get the qubits we want to measure for each term + qubit_maps.append(sorted(measurements.keys())) + Z_observables.append(tmp_obs) + + circ_copy = circuit.copy(True) + circ_copy.add(list(measurements.values())) + rotated_circuits.append(circ_copy) + + # execute the circuits + results = self.execute_circuits(rotated_circuits, nshots=nshots) + + # construct the expectation value for each diagonal term + # and sum all together + expval = 0.0 + for res, obs, qmap in zip(results, Z_observables, qubit_maps): + freq = res.frequencies() + expval += sum( + self.calculate_expectation_from_samples(o, freq, qmap) for o in obs + ) + return hamiltonian.constant + expval + @abc.abstractmethod def calculate_matrix_exp( self, diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 0a3a7dd8ea..66db309843 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -9,7 +9,8 @@ import numpy as np import sympy -from qibo.backends import Backend, _check_backend +import qibo.backends.abstract.Backend +from qibo.backends import _check_backend from qibo.config import log, raise_error from qibo.hamiltonians.abstract import AbstractHamiltonian from qibo.hamiltonians.terms import SymbolicTerm diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py index 7c5d1ea591..dbbff01428 100644 --- a/src/qibo/models/circuit.py +++ b/src/qibo/models/circuit.py @@ -749,18 +749,21 @@ def gates_of_type(self, gate: Union[str, type]) -> List[Tuple[int, gates.Gate]]: return [(i, g) for i, g in enumerate(self.queue) if isinstance(g, gate)] - def _set_parameters_list(self, parameters, n): + def _set_parameters_list(self, parameters, n, include_not_trainable: bool = False): """Helper method for ``set_parameters`` when a list is given. Also works if ``parameters`` is ``np.ndarray`` or ``tf.Tensor``. """ - if n == len(self.trainable_gates): - for i, gate in enumerate(self.trainable_gates): + _gates = ( + self.parametrized_gates if include_not_trainable else self.trainable_gates + ) + if n == len(_gates): + for i, gate in enumerate(_gates): gate.parameters = parameters[i] - elif n == self.trainable_gates.nparams: + elif n == _gates.nparams: parameters = list(parameters) k = 0 - for i, gate in enumerate(self.trainable_gates): + for i, gate in enumerate(_gates): if gate.nparams == 1: gate.parameters = parameters[i + k] else: @@ -770,10 +773,10 @@ def _set_parameters_list(self, parameters, n): raise_error( ValueError, f"Given list of parameters has length {n} while " - + f"the circuit contains {len(self.trainable_gates)} parametrized gates.", + + f"the circuit contains {len(_gates)} parametrized gates.", ) - def set_parameters(self, parameters): + def set_parameters(self, parameters, include_not_trainable: bool = False): """Updates the parameters of the circuit's parametrized gates. For more information on how to use this method we refer to the @@ -819,8 +822,11 @@ def set_parameters(self, parameters): params = [0.123, 0.456, 0.789, 0.321] circuit.set_parameters(params) """ + _gates = ( + self.parametrized_gates if include_not_trainable else self.trainable_gates + ) if isinstance(parameters, dict): - diff = set(parameters.keys()) - self.trainable_gates.set + diff = set(parameters.keys()) - _gates.set if diff: raise_error( KeyError, @@ -836,7 +842,9 @@ def set_parameters(self, parameters): nparams = int(parameters.shape[0]) except AttributeError: nparams = len(parameters) - self._set_parameters_list(parameters, nparams) + self._set_parameters_list( + parameters, nparams, include_not_trainable=include_not_trainable + ) else: raise_error(TypeError, f"Invalid type of parameters {type(parameters)}.") diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index af40f65287..265e02ae52 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -3,8 +3,9 @@ import numpy as np import sympy +import qibo.backends.abstract.Backend from qibo import gates -from qibo.backends import Backend, _check_backend, get_backend, matrices +from qibo.backends import _check_backend, get_backend, matrices from qibo.config import raise_error From 0c8c44c5cc0ca768268cbe0d7e8b981fb68ea069 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 7 Aug 2025 11:43:59 +0200 Subject: [PATCH 2/3] fix: remove commits from other pr --- src/qibo/backends/abstract.py | 130 -------------------------- src/qibo/hamiltonians/hamiltonians.py | 2 - src/qibo/symbols.py | 3 +- 3 files changed, 1 insertion(+), 134 deletions(-) diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index 5b4d7f6bf8..6e63de00cc 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -1,10 +1,7 @@ import abc from typing import Optional, Union -from qibo import gates from qibo.config import raise_error -from qibo.hamiltonians import SymbolicHamiltonian -from qibo.symbols import Z class Backend(abc.ABC): @@ -419,133 +416,6 @@ def calculate_expectation_density_matrix( """Calculate expectation value of a density matrix given the observable matrix.""" raise_error(NotImplementedError) - def calculate_expectation_from_samples_symbolic( - self, hamiltonian, frequencies: dict, qubit_map: Union[tuple[int], list[int]] - ): - """ - Calculate the expectation value from the samples. - The observable has to be diagonal in the computational basis. - - Args: - hamiltonian (SymbolicHamiltonian): observable to calculate the expectation value of. - freq (dict): input frequencies of the samples. - qubit_map (list): qubit map. - - Returns: - (float): the calculated expectation value. - """ - for term in hamiltonian.terms: - # pylint: disable=E1101 - for factor in term.factors: - if not isinstance(factor, Z): - raise_error( - NotImplementedError, "Observable is not a Pauli-Z string." - ) - - if qubit_map is None: - qubit_map = list(range(hamiltonian.nqubits)) - - keys = list(frequencies.keys()) - counts = list(frequencies.values()) - counts = self.cast(counts, dtype=self.np.float64) / sum(counts) - expvals = [] - for term in hamiltonian.terms: - qubits = { - factor.target_qubit for factor in term.factors if factor.name[0] != "I" - } - expvals.extend( - [ - term.coefficient.real - * (-1) ** [state[qubit_map.index(q)] for q in qubits].count("1") - for state in keys - ] - ) - expvals = self.cast(expvals, dtype=counts.dtype).reshape( - len(hamiltonian.terms), len(frequencies) - ) - return self.np.sum(expvals @ counts) + hamiltonian.constant.real - - def calculate_expectation_from_circuit( - self, hamiltonian, circuit, nshots: int = 1000 - ): - """ - Calculate the expectation value from a circuit. - This even works for observables not completely diagonal in the computational - basis, but only diagonal at a term level in a defined basis. Namely, for - an observable of the form :math:``H = \\sum_i H_i``, where each :math:``H_i`` - consists in a `n`-qubits pauli operator :math:`P_0 \\otimes P_1 \\otimes \\cdots \\otimes P_n`, - the expectation value is computed by rotating the input circuit in the suitable - basis for each term :math:``H_i`` thus extracting the `term-wise` expectations - that are then summed to build the global expectation value. - Each term of the observable is treated separately, by measuring in the correct - basis and re-executing the circuit. - - Args: - hamiltonian (SymbolicHamiltonian): observable to calculate the expectation value of. - circuit (Circuit): input circuit. - nshots (int): number of shots, defaults to 1000. - - Returns: - (float): the calculated expectation value. - """ - # from qibo import gates - # from qibo.symbols import Z - # from qibo.hamiltonians import SymbolicHamiltonian - - rotated_circuits = [] - Z_observables = [] - qubit_maps = [] - # loop over the terms that can be diagonalized simultaneously - for terms in hamiltonian.diagonal_terms: - # for each term that can be diagonalized simultaneously - # extract the coefficient, Z observable and qubit map - # the basis rotation, instead, will be the same - tmp_obs = [] - measurements = {} - for term in terms: - # Only care about non-I terms - non_identity_factors = [] - # prepare the measurement basis and append it to the circuit - for factor in term.factors: - if factor.name[0] != "I": - non_identity_factors.append(factor) - q = factor.target_qubit - if q not in measurements: - measurements[q] = gates.M(q, basis=factor.gate.__class__) - - # build diagonal observable - # including the coefficient - symb_obs = prod( - Z(factor.target_qubit) for factor in non_identity_factors - ) - symb_obs = SymbolicHamiltonian( - term.coefficient * symb_obs, - nqubits=circuit.nqubits, - backend=self, - ) - tmp_obs.append(symb_obs) - - # Get the qubits we want to measure for each term - qubit_maps.append(sorted(measurements.keys())) - Z_observables.append(tmp_obs) - - circ_copy = circuit.copy(True) - circ_copy.add(list(measurements.values())) - rotated_circuits.append(circ_copy) - - # execute the circuits - results = self.execute_circuits(rotated_circuits, nshots=nshots) - - # construct the expectation value for each diagonal term - # and sum all together - expval = 0.0 - for res, obs, qmap in zip(results, Z_observables, qubit_maps): - freq = res.frequencies() - expval += sum( - self.calculate_expectation_from_samples(o, freq, qmap) for o in obs - ) - return hamiltonian.constant + expval - @abc.abstractmethod def calculate_matrix_exp( self, diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 66db309843..9a39f5325b 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -9,8 +9,6 @@ import numpy as np import sympy -import qibo.backends.abstract.Backend -from qibo.backends import _check_backend from qibo.config import log, raise_error from qibo.hamiltonians.abstract import AbstractHamiltonian from qibo.hamiltonians.terms import SymbolicTerm diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index 265e02ae52..af40f65287 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -3,9 +3,8 @@ import numpy as np import sympy -import qibo.backends.abstract.Backend from qibo import gates -from qibo.backends import _check_backend, get_backend, matrices +from qibo.backends import Backend, _check_backend, get_backend, matrices from qibo.config import raise_error From f91f5dd720a9cfa9fab39216916e68cc132ff95d Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 7 Aug 2025 11:45:39 +0200 Subject: [PATCH 3/3] fix: restored backend import --- src/qibo/hamiltonians/hamiltonians.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 9a39f5325b..0a3a7dd8ea 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -9,6 +9,7 @@ import numpy as np import sympy +from qibo.backends import Backend, _check_backend from qibo.config import log, raise_error from qibo.hamiltonians.abstract import AbstractHamiltonian from qibo.hamiltonians.terms import SymbolicTerm