From a7e87794ff687074122c7747585f4de133d5f17d Mon Sep 17 00:00:00 2001 From: Zoufalc <40824883+Zoufalc@users.noreply.github.com> Date: Tue, 29 Mar 2022 18:44:20 +0200 Subject: [PATCH] Gradient Framework Update - Lin. Comb. for Imag Parts (#7632) * updates gradient framework * Include Unittests and Reno * fix lint * make black fix * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update releasenotes/notes/imag_gradients-3dabcd11343062a8.yaml Co-authored-by: Julien Gacon * Including Juliens comments1 * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: dlasecki * Integration of review comment continued * include review unittest tbc * Include Phase Fix unittest * QFI check and get real part * black and minor fix * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: Julien Gacon * Update test/python/opflow/test_gradients.py Co-authored-by: Julien Gacon * include review feedback * fix typo * white spaces * lint fix * fix unittest * make black * make black 2 * lint fix * nat grad * fix lint * white spaces * white spaces 2 * white spaces 4 * black * code changes for aux meas op * incorporate unittests and fixes for trace with -Y * fix lint * fix lint * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * Update test/python/opflow/test_gradients.py Co-authored-by: dlasecki * Update test/python/opflow/test_gradients.py Co-authored-by: dlasecki * Update test/python/opflow/test_gradients.py Co-authored-by: dlasecki * Update test/python/opflow/test_gradients.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: dlasecki * Update releasenotes/notes/imag_gradients-3dabcd11343062a8.yaml Co-authored-by: dlasecki * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: dlasecki * Update qiskit/opflow/gradients/natural_gradient.py Co-authored-by: dlasecki * global variables instead of hardcoded values * minor fixes * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * include review cryoris * make black * ValueError * make black * remove print * fix aux meas op propagation * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: dlasecki * minor style changes * delete testing file * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Julien Gacon * Update qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py Co-authored-by: Julien Gacon * make black * Update qiskit/opflow/gradients/circuit_gradients/lin_comb.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * feedback Steve * remove lambda * minor fixes * move aux meas op into init * make black * remove unneccessary ValueError * super init * fix typo * fix typo Co-authored-by: Julien Gacon Co-authored-by: dlasecki Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../gradients/circuit_gradients/lin_comb.py | 172 +++++++++---- .../circuit_gradients/param_shift.py | 4 +- .../gradients/circuit_qfis/lin_comb_full.py | 88 ++++--- qiskit/opflow/gradients/derivative_base.py | 1 + qiskit/opflow/gradients/gradient.py | 17 +- qiskit/opflow/gradients/natural_gradient.py | 188 +++++++++----- .../imag_gradients-3dabcd11343062a8.yaml | 34 +++ test/python/opflow/test_gradients.py | 236 +++++++++++++++++- 8 files changed, 597 insertions(+), 143 deletions(-) create mode 100644 releasenotes/notes/imag_gradients-3dabcd11343062a8.yaml diff --git a/qiskit/opflow/gradients/circuit_gradients/lin_comb.py b/qiskit/opflow/gradients/circuit_gradients/lin_comb.py index 0ed89c9dc961..0fa8ada96cab 100644 --- a/qiskit/opflow/gradients/circuit_gradients/lin_comb.py +++ b/qiskit/opflow/gradients/circuit_gradients/lin_comb.py @@ -16,7 +16,7 @@ from copy import deepcopy from functools import partial from itertools import product -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union, Callable import scipy import numpy as np @@ -53,7 +53,7 @@ from ...list_ops.list_op import ListOp from ...list_ops.composed_op import ComposedOp from ...list_ops.summed_op import SummedOp -from ...operator_globals import Z, I, One, Zero +from ...operator_globals import Z, I, Y, One, Zero from ...primitive_ops.primitive_op import PrimitiveOp from ...state_fns.state_fn import StateFn from ...state_fns.circuit_state_fn import CircuitStateFn @@ -62,6 +62,7 @@ from ...state_fns.sparse_vector_state_fn import SparseVectorStateFn from ...exceptions import OpflowError from .circuit_gradient import CircuitGradient +from ...converters import PauliBasisChange class LinComb(CircuitGradient): @@ -97,7 +98,25 @@ class LinComb(CircuitGradient): "z", } - # pylint: disable=signature-differs + # pylint: disable=signature-differs, arguments-differ + def __init__(self, aux_meas_op: OperatorBase = Z): + """ + Args: + aux_meas_op: The operator that the auxiliary qubit is measured with respect to. + For ``aux_meas_op = Z`` we compute 2Re[(dω⟨ψ(ω)|)O(θ)|ψ(ω)〉], + for ``aux_meas_op = -Y`` we compute 2Im[(dω⟨ψ(ω)|)O(θ)|ψ(ω)〉], and + for ``aux_meas_op = Z - 1j * Y`` we compute 2(dω⟨ψ(ω)|)O(θ)|ψ(ω)〉. + Raises: + ValueError: If the provided auxiliary measurement operator is not supported. + """ + super().__init__() + if aux_meas_op not in [Z, -Y, (Z - 1j * Y)]: + raise ValueError( + "This auxiliary measurement operator is currently not supported. Please choose " + "either Z, -Y, or Z - 1j * Y. " + ) + self._aux_meas_op = aux_meas_op + def convert( self, operator: OperatorBase, @@ -119,12 +138,10 @@ def convert( If a Tuple[ParameterExpression, ParameterExpression] or List[Tuple[ParameterExpression, ParameterExpression]] is given, then the 2nd order derivative of the operator is calculated. - Returns: An operator corresponding to the gradient resp. Hessian. The order is in accordance with the order of the given parameters. """ - return self._prepare_operator(operator, params) # pylint: disable=too-many-return-statements @@ -142,16 +159,15 @@ def _prepare_operator( """Traverse ``operator`` to get back the adapted operator representing the gradient. Args: - operator: The operator we are taking the gradient of: ⟨ψ(ω)|O(θ)|ψ(ω)〉 - params: The parameters we are taking the gradient wrt: ω - If a ParameterExpression, ParameterVector or List[ParameterExpression] is given, - then the 1st order derivative of the operator is calculated. - If a Tuple[ParameterExpression, ParameterExpression] or - List[Tuple[ParameterExpression, ParameterExpression]] - is given, then the 2nd order derivative of the operator is calculated. - + operator: The operator we are taking the gradient of: ⟨ψ(ω)|O(θ)|ψ(ω)〉. + params: The parameters we are taking the gradient wrt: ω. + If a ``ParameterExpression```, ``ParameterVector`` or ``List[ParameterExpression]`` + is given, then the 1st order derivative of the operator is calculated. + If a ``Tuple[ParameterExpression, ParameterExpression]`` or + ``List[Tuple[ParameterExpression, ParameterExpression]]`` + is given, then the 2nd order derivative of the operator is calculated. Returns: - Adapted operator. + The adapted operator. Measurement operators are attached with an additional Z term acting on an additional working qubit. Quantum states - which must be given as circuits - are adapted. An additional @@ -178,6 +194,8 @@ def _prepare_operator( if not isinstance(operator[-1], StateFn) or operator[-1].is_measurement: raise ValueError("The given operator does not correspond to an expectation value") if operator[0].is_measurement: + meas = deepcopy(operator.oplist[0]) + meas = meas.primitive * meas.coeff if len(operator.oplist) == 2: state_op = operator[1] if not isinstance(state_op, StateFn): @@ -190,7 +208,9 @@ def _prepare_operator( ): return self._gradient_states( - state_op, meas_op=(2 * ~StateFn(Z) ^ operator[0]), target_params=params + state_op, + meas_op=(2 * meas), + target_params=params, ) elif isinstance(params, tuple) or ( isinstance(params, list) @@ -198,7 +218,7 @@ def _prepare_operator( ): return self._hessian_states( state_op, - meas_op=(4 * ~StateFn(Z ^ I) ^ operator[0]), + meas_op=(4 * (I ^ meas)), target_params=params, ) # type: ignore else: @@ -221,7 +241,7 @@ def _prepare_operator( return state_op.traverse( partial( self._gradient_states, - meas_op=(2 * ~StateFn(Z) ^ operator[0]), + meas_op=(2 * meas), target_params=params, ) ) @@ -232,14 +252,14 @@ def _prepare_operator( return state_op.traverse( partial( self._hessian_states, - meas_op=(4 * ~StateFn(Z ^ I) ^ operator[0]), + meas_op=(4 * I ^ meas), target_params=params, ) ) raise OpflowError( - "The linear combination gradient does only support the computation " - "of 1st gradients and 2nd order gradients." + "The linear combination gradient only supports the " + "computation of 1st and 2nd order gradients." ) else: return operator.traverse(partial(self._prepare_operator, params=params)) @@ -274,6 +294,7 @@ def get_result(item): item = item.primitive if isinstance(item, VectorStateFn): item = item.primitive.data + if isinstance(item, dict): prob_dict = {} for key, val in item.items(): @@ -328,6 +349,10 @@ def get_result(item): partial_trace(lin_comb_op.dot(np.outer(item, np.conj(item))), [0, 1]).data ) ) + elif isinstance(item, scipy.sparse.spmatrix): + # Generate the operator which computes the linear combination + trace = _z_exp(item) + return trace elif isinstance(item, dict): prob_dict = {} for key, val in item.values(): @@ -366,11 +391,9 @@ def _gate_gradient_dict(gate: Gate) -> List[Tuple[List[complex], List[Instructio Args: gate: The gate for which the derivative is being computed. - Returns: - The coefficients and the gates used for the metric computation for each parameter of - the respective gates. - [([a^0], [V^0]) ..., ([a^k], [V^k])] - + Returns: + The coefficients and the gates used for the metric computation for each parameter of + the respective gates ``[([a^0], [V^0]) ..., ([a^k], [V^k])]``. Raises: OpflowError: If the input gate is controlled by another state but '|1>^{\otimes k}' @@ -478,7 +501,6 @@ def apply_grad_gate( trim_after_grad_gate=False, ): """Util function to apply a gradient gate for the linear combination of unitaries method. - Replaces the ``gate`` instance in ``circuit`` with ``grad_gate`` using ``qr_superpos`` as superposition qubit. Also adds the appropriate sign-fix gates on the superposition qubit. @@ -593,10 +615,65 @@ def apply_grad_gate( return out + def _aux_meas_basis_trafo( + self, aux_meas_op: OperatorBase, state: StateFn, state_op: StateFn, combo_fn: Callable + ) -> ListOp: + """ + This function applies the necessary basis transformation to measure the quantum state in + a different basis -- given by the auxiliary measurement operator ``aux_meas_op``. + + Args: + aux_meas_op: The auxiliary measurement operator defines the necessary measurement basis. + state: This operator represents the gradient or Hessian before the basis transformation. + state_op: The operator representing the quantum state for which we compute the gradient + or Hessian. + combo_fn: This ``combo_fn`` defines whether the target is a gradient or Hessian. + + + Returns: + Operator representing the gradient or Hessian. + + Raises: + ValueError: If ``aux_meas_op`` is neither ``Z`` nor ``-Y`` nor ``Z - 1j * Y``. + + """ + if aux_meas_op == Z - 1j * Y: + state_z = ListOp( + [state], + combo_fn=partial(combo_fn, state_op=state_op), + ) + pbc = PauliBasisChange(replacement_fn=PauliBasisChange.measurement_replacement_fn) + pbc = pbc.convert(-Y ^ (I ^ (state.num_qubits - 1))) + state_y = pbc[-1] @ state + state_y = ListOp( + [state_y], + combo_fn=partial(combo_fn, state_op=state_op), + ) + return state_z - 1j * state_y + + elif aux_meas_op == -Y: + pbc = PauliBasisChange(replacement_fn=PauliBasisChange.measurement_replacement_fn) + pbc = pbc.convert(aux_meas_op ^ (I ^ (state.num_qubits - 1))) + state = pbc[-1] @ state + return -1 * ListOp( + [state], + combo_fn=partial(combo_fn, state_op=state_op), + ) + elif aux_meas_op == Z: + return ListOp( + [state], + combo_fn=partial(combo_fn, state_op=state_op), + ) + else: + raise ValueError( + f"The auxiliary measurement operator passed {aux_meas_op} is not supported. " + "Only Y, Z, or Z - 1j * Y are valid." + ) + def _gradient_states( self, state_op: StateFn, - meas_op: Union[OperatorBase, bool] = True, + meas_op: Optional[OperatorBase] = None, target_params: Optional[Union[Parameter, List[Parameter]]] = None, open_ctrl: bool = False, trim_after_grad_gate: bool = False, @@ -619,13 +696,15 @@ def _gradient_states( Raises: AquaError: If one of the circuits could not be constructed. TypeError: If the operators is of unsupported type. + ValueError: If the auxiliary operator preparation fails. """ - qr_superpos = QuantumRegister(1) - state_qc = QuantumCircuit(*state_op.primitive.qregs, qr_superpos) - state_qc.h(qr_superpos) # unroll separately from the H gate since we need the H gate to be the first # operation in the data attributes of the circuit unrolled = self._transpile_to_supported_operations(state_op.primitive, self.SUPPORTED_GATES) + qr_superpos = QuantumRegister(1) + state_qc = QuantumCircuit(*state_op.primitive.qregs, qr_superpos) + state_qc.h(qr_superpos) + state_qc.compose(unrolled, inplace=True) # Define the working qubit to realize the linear combination of unitaries @@ -654,7 +733,7 @@ def _gradient_states( open_ctrl, trim_after_grad_gate, ) - # apply final hadamard on superposition qubit + # apply final Hadamard on superposition qubit grad_circuit.h(qr_superpos) # compute the correct coefficient and append to list of circuits @@ -665,10 +744,13 @@ def _gradient_states( param_expression = gate.params[idx] if isinstance(meas_op, OperatorBase): - state = meas_op @ state - elif meas_op is True: - state = ListOp( - [state], combo_fn=partial(self._grad_combo_fn, state_op=state_op) + state = ( + StateFn(self._aux_meas_op ^ meas_op, is_measurement=True) @ state + ) + + else: + state = self._aux_meas_basis_trafo( + self._aux_meas_op, state, state_op, self._grad_combo_fn ) if param_expression != param: # parameter is not identity, apply chain rule @@ -706,6 +788,7 @@ def _hessian_states( Raises: AquaError: If one of the circuits could not be constructed. TypeError: If ``operator`` is of unsupported type. + ValueError: If the auxiliary operator preparation fails. """ if not isinstance(target_params, list): target_params = [target_params] @@ -721,7 +804,7 @@ def _hessian_states( qr_add1 = QuantumRegister(1, "s1") state_qc = QuantumCircuit(*state_op.primitive.qregs, qr_add0, qr_add1) - # add hadamards + # add Hadamards state_qc.h(qr_add0) state_qc.h(qr_add1) @@ -753,7 +836,7 @@ def _hessian_states( grad_circuit, gate_b, idx_b, grad_gate_b, grad_coeff_b, qr_add1 ) - # final hadamards and CZ + # final Hadamards and CZ hessian_circuit.h(qr_add0) hessian_circuit.cz(qr_add1[0], qr_add0[0]) hessian_circuit.h(qr_add1) @@ -763,13 +846,13 @@ def _hessian_states( state = CircuitStateFn(hessian_circuit, coeff=coeff) if meas_op is not None: - state = meas_op @ state + state = ( + StateFn(self._aux_meas_op ^ meas_op, is_measurement=True) + @ state + ) else: - # special operator for probability gradients - # uses combo_fn on list op with a single operator - state = ListOp( - [state], - combo_fn=partial(self._hess_combo_fn, state_op=state_op), + state = self._aux_meas_basis_trafo( + self._aux_meas_op, state, state_op, self._hess_combo_fn ) # Chain Rule Parameter Expression @@ -792,7 +875,8 @@ def _hessian_states( def _z_exp(spmatrix): - """Compute the sampling probabilities of the qubits after applying Z on the ancilla.""" + """Compute the sampling probabilities of the qubits after applying measurement on the + auxiliary qubit.""" dok = spmatrix.todok() num_qubits = int(np.log2(dok.shape[1])) diff --git a/qiskit/opflow/gradients/circuit_gradients/param_shift.py b/qiskit/opflow/gradients/circuit_gradients/param_shift.py index dfe2e60e50dd..a49e1027ed9f 100644 --- a/qiskit/opflow/gradients/circuit_gradients/param_shift.py +++ b/qiskit/opflow/gradients/circuit_gradients/param_shift.py @@ -107,6 +107,7 @@ def convert( Returns: An operator corresponding to the gradient resp. Hessian. The order is in accordance with the order of the given parameters. + Raises: OpflowError: If the parameters are given in an invalid format. @@ -146,6 +147,7 @@ def _parameter_shift( operator: The operator containing circuits we are taking the derivative of. params: The parameters (ω) we are taking the derivative with respect to. If a ParameterVector is provided, each parameter will be shifted. + Returns: param_shifted_op: An operator object which evaluates to the respective gradients. @@ -285,7 +287,7 @@ def _prob_combo_fn( TypeError: if ``x`` is not DictStateFn, VectorStateFn or their list. """ - # In the probability gradient case, the amplitudes still need to be converted + # Note: In the probability gradient case, the amplitudes still need to be converted # into sampling probabilities. def get_primitives(item): diff --git a/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py b/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py index b2271470feba..41ee30711505 100644 --- a/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py +++ b/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py @@ -18,6 +18,7 @@ from qiskit.circuit import QuantumCircuit, QuantumRegister, ParameterVector, ParameterExpression from qiskit.utils.arithmetic import triu_to_dense +from ...operator_base import OperatorBase from ...list_ops.list_op import ListOp from ...list_ops.summed_op import SummedOp from ...operator_globals import I, Z, Y @@ -31,10 +32,35 @@ class LinCombFull(CircuitQFI): r"""Compute the full Quantum Fisher Information (QFI). Given a pure, parameterized quantum state this class uses the linear combination of unitaries - approach, requiring one additional working qubit. See also :class:`~qiskit.opflow.QFI`. """ + # pylint: disable=signature-differs, arguments-differ + def __init__( + self, + aux_meas_op: OperatorBase = Z, + phase_fix: bool = True, + ): + """ + Args: + aux_meas_op: The operator that the auxiliary qubit is measured with respect to. + For ``aux_meas_op = Z`` we compute 4Re[(dω⟨ψ(ω)|)O(θ)|ψ(ω)〉], + for ``aux_meas_op = -Y`` we compute 4Im[(dω⟨ψ(ω)|)O(θ)|ψ(ω)〉], and + for ``aux_meas_op = Z - 1j * Y`` we compute 4(dω⟨ψ(ω)|)O(θ)|ψ(ω)〉. + phase_fix: Whether or not to compute and add the additional phase fix term + Re[(dω⟨<ψ(ω)|)|ψ(ω)><ψ(ω)|(dω|ψ(ω))>]. + Raises: + ValueError: If the provided auxiliary measurement operator is not supported. + """ + super().__init__() + if aux_meas_op not in [Z, -Y, (Z - 1j * Y)]: + raise ValueError( + "This auxiliary measurement operator is currently not supported. Please choose " + "either Z, -Y, or Z - 1j * Y. " + ) + self._aux_meas_op = aux_meas_op + self._phase_fix = phase_fix + def convert( self, operator: CircuitStateFn, @@ -45,7 +71,6 @@ def convert( operator: The operator corresponding to the quantum state :math:`|\psi(\omega)\rangle` for which we compute the QFI. params: The parameters :math:`\omega` with respect to which we are computing the QFI. - Returns: A ``ListOp[ListOp]`` where the operator at position ``[k][l]`` corresponds to the matrix element :math:`k, l` of the QFI. @@ -54,11 +79,9 @@ def convert( TypeError: If ``operator`` is an unsupported type. """ # QFI & phase fix observable - qfi_observable = ~StateFn(4 * Z ^ (I ^ operator.num_qubits)) - phase_fix_observable = StateFn( - (Z + 1j * Y) ^ (I ^ operator.num_qubits), is_measurement=True + qfi_observable = StateFn( + 4 * self._aux_meas_op ^ (I ^ operator.num_qubits), is_measurement=True ) - # see https://arxiv.org/pdf/quant-ph/0108146.pdf # Check if the given operator corresponds to a quantum state given as a circuit. if not isinstance(operator, CircuitStateFn): @@ -73,20 +96,23 @@ def convert( elif isinstance(params, ParameterVector): params = params[:] # unroll to list - # First, the operators are computed which can compensate for a potential phase-mismatch - # between target and trained state, i.e.〈ψ|∂lψ〉 - gradient_states = LinComb()._gradient_states( - operator, - meas_op=phase_fix_observable, - target_params=params, - open_ctrl=False, - trim_after_grad_gate=True, - ) - # if type(gradient_states) in [ListOp, SummedOp]: # pylint: disable=unidiomatic-typecheck - if type(gradient_states) == ListOp: - phase_fix_states = gradient_states.oplist - else: - phase_fix_states = [gradient_states] + if self._phase_fix: + # First, the operators are computed which can compensate for a potential phase-mismatch + # between target and trained state, i.e.〈ψ|∂lψ〉 + phase_fix_observable = I ^ operator.num_qubits + gradient_states = LinComb(aux_meas_op=(Z - 1j * Y))._gradient_states( + operator, + meas_op=phase_fix_observable, + target_params=params, + open_ctrl=False, + trim_after_grad_gate=True, + ) + + # pylint: disable=unidiomatic-typecheck + if type(gradient_states) == ListOp: + phase_fix_states = gradient_states.oplist + else: + phase_fix_states = [gradient_states] # Get 4 * Re[〈∂kψ|∂lψ] qfi_operators = [] @@ -179,15 +205,19 @@ def convert( # Compute −4 * Re(〈∂kψ|ψ〉〈ψ|∂lψ〉) def phase_fix_combo_fn(x): - return 4 * (-0.5) * (x[0] * np.conjugate(x[1]) + x[1] * np.conjugate(x[0])) - - phase_fix = ListOp( - [phase_fix_states[i], phase_fix_states[j]], combo_fn=phase_fix_combo_fn - ) - # Add the phase fix quantities to the entries of the QFI - # Get 4 * Re[〈∂kψ|∂lψ〉−〈∂kψ|ψ〉〈ψ|∂lψ〉] - qfi_ops += [SummedOp(qfi_op) + phase_fix] + return -4 * np.real(x[0] * np.conjugate(x[1])) + + if self._phase_fix: + phase_fix_op = ListOp( + [phase_fix_states[i], phase_fix_states[j]], combo_fn=phase_fix_combo_fn + ) + # Add the phase fix quantities to the entries of the QFI + # Get 4 * Re[〈∂kψ|∂lψ〉−〈∂kψ|ψ〉〈ψ|∂lψ〉] + qfi_ops += [SummedOp(qfi_op) + phase_fix_op] + else: + qfi_ops += [SummedOp(qfi_op)] qfi_operators.append(ListOp(qfi_ops)) - # Return the full QFI + + # Return estimate of the full QFI -- A QFI is by definition positive semi-definite. return ListOp(qfi_operators, combo_fn=triu_to_dense) diff --git a/qiskit/opflow/gradients/derivative_base.py b/qiskit/opflow/gradients/derivative_base.py index 6f407ba37eb2..9979a498c8dc 100644 --- a/qiskit/opflow/gradients/derivative_base.py +++ b/qiskit/opflow/gradients/derivative_base.py @@ -158,6 +158,7 @@ def _erase_operator_coeffs(cls, operator: OperatorBase) -> OperatorBase: Returns: An operator which is equal to the input operator but whose coefficients have all been set to 1.0 + Raises: TypeError: If unknown operator type is reached. """ diff --git a/qiskit/opflow/gradients/gradient.py b/qiskit/opflow/gradients/gradient.py index dc2610ae1d23..6571ffbde316 100644 --- a/qiskit/opflow/gradients/gradient.py +++ b/qiskit/opflow/gradients/gradient.py @@ -108,6 +108,12 @@ def is_coeff_c(coeff, c): return expr == c return coeff == c + def is_coeff_c_abs(coeff, c): + if isinstance(coeff, ParameterExpression): + expr = coeff._symbol_expr + return np.abs(expr) == c + return np.abs(coeff) == c + if isinstance(params, (ParameterVector, list)): param_grads = [self.get_gradient(operator, param) for param in params] # If get_gradient returns None, then the corresponding parameter was probably not @@ -126,10 +132,17 @@ def is_coeff_c(coeff, c): # By now params is a single parameter param = params # Handle Product Rules - if not is_coeff_c(operator._coeff, 1.0): + if not is_coeff_c(operator._coeff, 1.0) and not is_coeff_c(operator._coeff, 1.0j): # Separate the operator from the coefficient coeff = operator._coeff op = operator / coeff + if np.iscomplex(coeff): + from .circuit_gradients.lin_comb import LinComb + + if isinstance(self.grad_method, LinComb): + op *= 1j + coeff /= 1j + # Get derivative of the operator (recursively) d_op = self.get_gradient(op, param) # ..get derivative of the coeff @@ -152,7 +165,7 @@ def is_coeff_c(coeff, c): if isinstance(operator, ComposedOp): # Gradient of an expectation value - if not is_coeff_c(operator._coeff, 1.0): + if not is_coeff_c_abs(operator._coeff, 1.0): raise OpflowError( "Operator pre-processing failed. Coefficients were not properly " "collected inside the ComposedOp." diff --git a/qiskit/opflow/gradients/natural_gradient.py b/qiskit/opflow/gradients/natural_gradient.py index f1b814126c4f..13c16f8385a4 100644 --- a/qiskit/opflow/gradients/natural_gradient.py +++ b/qiskit/opflow/gradients/natural_gradient.py @@ -30,6 +30,11 @@ from .gradient_base import GradientBase from .qfi import QFI +# Error tolerance variable +ETOL = 1e-8 +# Cut-off ratio for small singular values for least square solver +RCOND = 1e-2 + class NaturalGradient(GradientBase): r"""Convert an operator expression to the first-order gradient. @@ -120,33 +125,79 @@ def convert( # Instantiate the QFI metric which is used to re-scale the gradient metric = self._qfi_method.convert(operator[-1], params) * 0.25 - # Define the function which compute the natural gradient from the gradient and the QFI. def combo_fn(x): - c = np.real(x[0]) - a = np.real(x[1]) - if self.regularization: - # If a regularization method is chosen then use a regularized solver to - # construct the natural gradient. - nat_grad = NaturalGradient._regularized_sle_solver( - a, c, regularization=self.regularization - ) - else: - try: - # Try to solve the system of linear equations Ax = C. - nat_grad = np.linalg.solve(a, c) - except np.linalg.LinAlgError: # singular matrix - nat_grad = np.linalg.lstsq(a, c)[0] - return np.real(nat_grad) + return self.nat_grad_combo_fn(x, self.regularization) # Define the ListOp which combines the gradient and the QFI according to the combination # function defined above. return ListOp([grad, metric], combo_fn=combo_fn) + @staticmethod + def nat_grad_combo_fn(x: tuple, regularization: Optional[str] = None) -> np.ndarray: + r""" + Natural Gradient Function Implementation. + + Args: + x: Iterable consisting of Gradient, Quantum Fisher Information. + regularization: Regularization method. + + Returns: + Natural Gradient. + + Raises: + ValueError: If the gradient has imaginary components that are non-negligible. + + """ + gradient = x[0] + metric = x[1] + if np.amax(np.abs(np.imag(gradient))) > ETOL: + raise ValueError( + "The imaginary part of the gradient are non-negligible. The largest absolute " + f"imaginary value in the gradient is {np.amax(np.abs(np.imag(gradient)))}. " + "Please increase the number of shots." + ) + gradient = np.real(gradient) + + if np.amax(np.abs(np.imag(metric))) > ETOL: + raise ValueError( + "The imaginary part of the metric are non-negligible. The largest " + "absolute imaginary value in the gradient is " + f"{np.amax(np.abs(np.imag(metric)))}. Please " + "increase the number of shots." + ) + metric = np.real(metric) + + if regularization is not None: + # If a regularization method is chosen then use a regularized solver to + # construct the natural gradient. + nat_grad = NaturalGradient._regularized_sle_solver( + metric, gradient, regularization=regularization + ) + else: + # Check if numerical instabilities lead to a metric which is not positive semidefinite + w, v = np.linalg.eigh(metric) + + if not all(ew >= (-1) * ETOL for ew in w): + raise ValueError( + f"The underlying metric has at least one Eigenvalue < -{ETOL}. " + f"The smallest Eigenvalue is {np.amin(w)} " + "Please use a regularized least-square solver for this problem or " + "increase the number of backend shots.", + ) + if not all(ew >= 0 for ew in w): + # If not all eigenvalues are non-negative, set them to a small positive + # value + w = [max(ETOL, ew) for ew in w] + # Recompose the adapted eigenvalues with the eigenvectors to get a new metric + metric = np.real(v @ np.diag(w) @ np.linalg.inv(v)) + nat_grad = np.linalg.lstsq(metric, gradient, rcond=RCOND)[0] + return nat_grad + @property def qfi_method(self) -> CircuitQFI: """Returns ``CircuitQFI``. - Returns: ``CircuitQFI`` + Returns: ``CircuitQFI``. """ return self._qfi_method.qfi_method @@ -162,8 +213,8 @@ def regularization(self) -> Optional[str]: @staticmethod def _reg_term_search( - a: np.ndarray, - c: np.ndarray, + metric: np.ndarray, + gradient: np.ndarray, reg_method: Callable[[np.ndarray, np.ndarray, float], float], lambda1: float = 1e-3, lambda4: float = 1.0, @@ -171,23 +222,24 @@ def _reg_term_search( ) -> Tuple[float, np.ndarray]: """ This method implements a search for a regularization parameter lambda by finding for the - corner of the L-curve + corner of the L-curve. More explicitly, one has to evaluate a suitable lambda by finding a compromise between the error in the solution and the norm of the regularization. This function implements a method presented in `A simple algorithm to find the L-curve corner in the regularization of inverse problems ` + Args: - a: see (1) and (2) - c: see (1) and (2) - reg_method: Given A, C and lambda the regularization method must return x_lambda - - see (2) - lambda1: left starting point for L-curve corner search - lambda4: right starting point for L-curve corner search - tol: termination threshold + metric: See (1) and (2). + gradient: See (1) and (2). + reg_method: Given the metric, gradient and lambda the regularization method must return + ``x_lambda`` - see (2). + lambda1: Left starting point for L-curve corner search. + lambda4: Right starting point for L-curve corner search. + tol: Termination threshold. Returns: - regularization coefficient, solution to the regularization inverse problem + Regularization coefficient which is the solution to the regularization inverse problem. """ def _get_curvature(x_lambda: List) -> float: @@ -196,8 +248,8 @@ def _get_curvature(x_lambda: List) -> float: Menger, K. (1930). Untersuchungen ̈uber Allgemeine Metrik. Math. Ann.,103(1), 466–501 Args: - x_lambda: [[x_lambdaj], [x_lambdak], [x_lambdal]] - lambdaj < lambdak < lambdal + ``x_lambda: [[x_lambdaj], [x_lambdak], [x_lambdal]]`` + ``lambdaj < lambdak < lambdal`` Returns: Menger Curvature @@ -207,10 +259,12 @@ def _get_curvature(x_lambda: List) -> float: eta = [] for x in x_lambda: try: - eps.append(np.log(np.linalg.norm(np.matmul(a, x) - c) ** 2)) + eps.append(np.log(np.linalg.norm(np.matmul(metric, x) - gradient) ** 2)) except ValueError: - eps.append(np.log(np.linalg.norm(np.matmul(a, np.transpose(x)) - c) ** 2)) - eta.append(np.log(max(np.linalg.norm(x) ** 2, 1e-6))) + eps.append( + np.log(np.linalg.norm(np.matmul(metric, np.transpose(x)) - gradient) ** 2) + ) + eta.append(np.log(max(np.linalg.norm(x) ** 2, ETOL))) p_temp = 1 c_k = 0 for i in range(3): @@ -231,7 +285,7 @@ def get_lambda2_lambda3(lambda1, lambda4): lambda_ = [lambda1, lambda2, lambda3, lambda4] x_lambda = [] for lam in lambda_: - x_lambda.append(reg_method(a, c, lam)) + x_lambda.append(reg_method(metric, gradient, lam)) counter = 0 while (lambda_[3] - lambda_[0]) / lambda_[3] >= tol: counter += 1 @@ -244,7 +298,7 @@ def get_lambda2_lambda3(lambda1, lambda4): x_lambda[2] = x_lambda[1] lambda2, _ = get_lambda2_lambda3(lambda_[0], lambda_[3]) lambda_[1] = lambda2 - x_lambda[1] = reg_method(a, c, lambda_[1]) + x_lambda[1] = reg_method(metric, gradient, lambda_[1]) c_3 = _get_curvature(x_lambda[1:]) if c_2 > c_3: @@ -256,7 +310,7 @@ def get_lambda2_lambda3(lambda1, lambda4): x_lambda[2] = x_lambda[1] lambda2, _ = get_lambda2_lambda3(lambda_[0], lambda_[3]) lambda_[1] = lambda2 - x_lambda[1] = reg_method(a, c, lambda_[1]) + x_lambda[1] = reg_method(metric, gradient, lambda_[1]) else: lambda_mc = lambda_[2] x_mc = x_lambda[2] @@ -266,14 +320,14 @@ def get_lambda2_lambda3(lambda1, lambda4): x_lambda[1] = x_lambda[2] _, lambda3 = get_lambda2_lambda3(lambda_[0], lambda_[3]) lambda_[2] = lambda3 - x_lambda[2] = reg_method(a, c, lambda_[2]) + x_lambda[2] = reg_method(metric, gradient, lambda_[2]) return lambda_mc, x_mc @staticmethod @_optionals.HAS_SKLEARN.require_in_call def _ridge( - a: np.ndarray, - c: np.ndarray, + metric: np.ndarray, + gradient: np.ndarray, lambda_: float = 1.0, lambda1: float = 1e-4, lambda4: float = 1e-1, @@ -291,9 +345,10 @@ def _ridge( x_lambda = arg min{||Ax-C||^2 + lambda*||x||_2^2} (3) `Scikit Learn Ridge Regression ` + Args: - a: see (1) and (2) - c: see (1) and (2) + metric: See (1) and (2). + gradient: See (1) and (2). lambda_ : regularization parameter used if auto_search = False lambda1: left starting point for L-curve corner search lambda4: right starting point for L-curve corner search @@ -332,15 +387,15 @@ def reg_method(a, c, alpha): return reg.coef_ lambda_mc, x_mc = NaturalGradient._reg_term_search( - a, c, reg_method, lambda1=lambda1, lambda4=lambda4, tol=tol_search + metric, gradient, reg_method, lambda1=lambda1, lambda4=lambda4, tol=tol_search ) return lambda_mc, np.transpose(x_mc) @staticmethod @_optionals.HAS_SKLEARN.require_in_call def _lasso( - a: np.ndarray, - c: np.ndarray, + metric: np.ndarray, + gradient: np.ndarray, lambda_: float = 1.0, lambda1: float = 1e-4, lambda4: float = 1e-1, @@ -363,8 +418,8 @@ def _lasso( ` Args: - a: mxn matrix - c: m vector + metric: Matrix of size mxn. + gradient: Vector of size m. lambda_ : regularization parameter used if auto_search = False lambda1: left starting point for L-curve corner search lambda4: right starting point for L-curve corner search @@ -410,15 +465,15 @@ def reg_method(a, c, alpha): return reg.coef_ lambda_mc, x_mc = NaturalGradient._reg_term_search( - a, c, reg_method, lambda1=lambda1, lambda4=lambda4, tol=tol_search + metric, gradient, reg_method, lambda1=lambda1, lambda4=lambda4, tol=tol_search ) return lambda_mc, x_mc @staticmethod def _regularized_sle_solver( - a: np.ndarray, - c: np.ndarray, + metric: np.ndarray, + gradient: np.ndarray, regularization: str = "perturb_diag", lambda1: float = 1e-3, lambda4: float = 1.0, @@ -428,9 +483,10 @@ def _regularized_sle_solver( ) -> np.ndarray: """ Solve a linear system of equations with a regularization method and automatic lambda fitting + Args: - a: mxn matrix - c: m vector + metric: Matrix of size mxn. + gradient: Vector of size m. regularization: Regularization scheme to be used: 'ridge', 'lasso', 'perturb_diag_elements' or 'perturb_diag' lambda1: left starting point for L-curve corner search (for 'ridge' and 'lasso') @@ -444,47 +500,51 @@ def _regularized_sle_solver( """ if regularization == "ridge": - _, x = NaturalGradient._ridge(a, c, lambda1=lambda1) + _, x = NaturalGradient._ridge(metric, gradient, lambda1=lambda1) elif regularization == "lasso": - _, x = NaturalGradient._lasso(a, c, lambda1=lambda1) + _, x = NaturalGradient._lasso(metric, gradient, lambda1=lambda1) elif regularization == "perturb_diag_elements": alpha = 1e-7 - while np.linalg.cond(a + alpha * np.diag(a)) > tol_cond_a: + while np.linalg.cond(metric + alpha * np.diag(metric)) > tol_cond_a: alpha *= 10 # include perturbation in A to avoid singularity - x, _, _, _ = np.linalg.lstsq(a + alpha * np.diag(a), c, rcond=None) + x, _, _, _ = np.linalg.lstsq(metric + alpha * np.diag(metric), gradient, rcond=None) elif regularization == "perturb_diag": alpha = 1e-7 - while np.linalg.cond(a + alpha * np.eye(len(c))) > tol_cond_a: + while np.linalg.cond(metric + alpha * np.eye(len(gradient))) > tol_cond_a: alpha *= 10 # include perturbation in A to avoid singularity - x, _, _, _ = np.linalg.lstsq(a + alpha * np.eye(len(c)), c, rcond=None) + x, _, _, _ = np.linalg.lstsq( + metric + alpha * np.eye(len(gradient)), gradient, rcond=None + ) else: # include perturbation in A to avoid singularity - x, _, _, _ = np.linalg.lstsq(a, c, rcond=None) + x, _, _, _ = np.linalg.lstsq(metric, gradient, rcond=None) if np.linalg.norm(x) > tol_norm_x[1] or np.linalg.norm(x) < tol_norm_x[0]: if regularization == "ridge": lambda1 = lambda1 / 10.0 - _, x = NaturalGradient._ridge(a, c, lambda1=lambda1, lambda4=lambda4) + _, x = NaturalGradient._ridge(metric, gradient, lambda1=lambda1, lambda4=lambda4) elif regularization == "lasso": lambda1 = lambda1 / 10.0 - _, x = NaturalGradient._lasso(a, c, lambda1=lambda1) + _, x = NaturalGradient._lasso(metric, gradient, lambda1=lambda1) elif regularization == "perturb_diag_elements": - while np.linalg.cond(a + alpha * np.diag(a)) > tol_cond_a: + while np.linalg.cond(metric + alpha * np.diag(metric)) > tol_cond_a: if alpha == 0: alpha = 1e-7 else: alpha *= 10 # include perturbation in A to avoid singularity - x, _, _, _ = np.linalg.lstsq(a + alpha * np.diag(a), c, rcond=None) + x, _, _, _ = np.linalg.lstsq(metric + alpha * np.diag(metric), gradient, rcond=None) else: if alpha == 0: alpha = 1e-7 else: alpha *= 10 - while np.linalg.cond(a + alpha * np.eye(len(c))) > tol_cond_a: + while np.linalg.cond(metric + alpha * np.eye(len(gradient))) > tol_cond_a: # include perturbation in A to avoid singularity - x, _, _, _ = np.linalg.lstsq(a + alpha * np.eye(len(c)), c, rcond=None) + x, _, _, _ = np.linalg.lstsq( + metric + alpha * np.eye(len(gradient)), gradient, rcond=None + ) alpha *= 10 return x diff --git a/releasenotes/notes/imag_gradients-3dabcd11343062a8.yaml b/releasenotes/notes/imag_gradients-3dabcd11343062a8.yaml new file mode 100644 index 000000000000..a7bf800776d0 --- /dev/null +++ b/releasenotes/notes/imag_gradients-3dabcd11343062a8.yaml @@ -0,0 +1,34 @@ +--- +features: + - | + Enable the calculation of the imaginary part of expectation value gradients. + Using a different measurement basis, i.e. ``-Y`` instead of ``Z``, we can measure the + imaginary part of gradients using the existing :class:`~qiskit.circuit.opflow.Gradient` and + :class:`~qiskit.circuit.opflow.QFI` classes. + The measurement basis can be set with the ``aux_meas_op`` argument. + + For the gradients, ``aux_meas_op = Z`` computes ``0.5Re[(⟨ψ(ω)|)O(θ)|dωψ(ω)〉]`` + and ``aux_meas_op = -Y`` computes ``0.5Im[(⟨ψ(ω)|)O(θ)|dωψ(ω)〉]``. + For the QFIs, ``aux_meas_op = Z`` computes ``4Re[(dω⟨<ψ(ω)|)(dω|ψ(ω)〉)]`` + and ``aux_meas_op = -Y`` computes ``4Im[(dω⟨<ψ(ω)|)(dω|ψ(ω)〉)]``. + example:: + + from qiskit import QuantumRegister, QuantumCircuit + from qiskit.opflow import CircuitStateFn, Y + from qiskit.opflow.gradients.circuit_gradients import LinComb + from qiskit.circuit import Parameter + + a = Parameter("a") + b = Parameter("b") + params = [a, b] + + q = QuantumRegister(1) + qc = QuantumCircuit(q) + qc.h(q) + qc.rz(params[0], q[0]) + qc.rx(params[1], q[0]) + op = CircuitStateFn(primitive=qc, coeff=1.0) + + aux_meas_op = -Y + + prob_grad = LinComb(aux_meas_op=aux_meas_op).convert(operator=op, params=params) diff --git a/test/python/opflow/test_gradients.py b/test/python/opflow/test_gradients.py index 890c10281d69..f66d1cbd9971 100644 --- a/test/python/opflow/test_gradients.py +++ b/test/python/opflow/test_gradients.py @@ -40,6 +40,7 @@ ) from qiskit.opflow.gradients import Gradient, NaturalGradient, Hessian from qiskit.opflow.gradients.qfi import QFI +from qiskit.opflow.gradients.circuit_gradients import LinComb from qiskit.opflow.gradients.circuit_qfis import LinCombFull, OverlapBlockDiag, OverlapDiag from qiskit.circuit import Parameter from qiskit.circuit import ParameterVector @@ -685,6 +686,87 @@ def test_natural_gradient4(self, grad_method, qfi_method, regularization): except MissingOptionalLibraryError as ex: self.skipTest(str(ex)) + def test_gradient_p_imag(self): + """Test the imaginary state gradient for p + |psi(a)> = 1/sqrt(2)[[1, exp(ia)]] + = iexp(-ia)/2 <1|H(|0>+exp(ia)|1>) + Im() = 0.5 cos(a). + """ + ham = X + a = Parameter("a") + params = a + q = QuantumRegister(1) + qc = QuantumCircuit(q) + qc.h(q) + qc.p(a, q[0]) + op = ~StateFn(ham) @ CircuitStateFn(primitive=qc, coeff=1.0) + + state_grad = LinComb(aux_meas_op=(-1) * Y).convert(operator=op, params=params) + values_dict = [{a: np.pi / 4}, {a: 0}, {a: np.pi / 2}] + correct_values = [1 / np.sqrt(2), 1, 0] + + for i, value_dict in enumerate(values_dict): + np.testing.assert_array_almost_equal( + state_grad.assign_parameters(value_dict).eval(), correct_values[i], decimal=1 + ) + + def test_qfi_p_imag(self): + """Test the imaginary state QFI for RXRY""" + x = Parameter("x") + y = Parameter("y") + circuit = QuantumCircuit(1) + circuit.ry(y, 0) + circuit.rx(x, 0) + state = StateFn(circuit) + + dx = ( + lambda x, y: (-1) + * 0.5j + * np.array( + [ + [ + -1j * np.sin(x / 2) * np.cos(y / 2) + np.cos(x / 2) * np.sin(y / 2), + np.cos(x / 2) * np.cos(y / 2) - 1j * np.sin(x / 2) * np.sin(y / 2), + ] + ] + ) + ) + dy = ( + lambda x, y: (-1) + * 0.5j + * np.array( + [ + [ + -1j * np.cos(x / 2) * np.sin(y / 2) + np.sin(x / 2) * np.cos(y / 2), + 1j * np.cos(x / 2) * np.cos(y / 2) - 1 * np.sin(x / 2) * np.sin(y / 2), + ] + ] + ) + ) + + state_grad = LinCombFull(aux_meas_op=-1 * Y, phase_fix=False).convert( + operator=state, params=[x, y] + ) + values_dict = [{x: 0, y: np.pi / 4}, {x: 0, y: np.pi / 2}, {x: np.pi / 2, y: 0}] + + for value_dict in values_dict: + x_ = list(value_dict.values())[0] + y_ = list(value_dict.values())[1] + correct_values = [ + [ + 4 * np.imag(np.dot(dx(x_, y_), np.conj(np.transpose(dx(x_, y_))))[0][0]), + 4 * np.imag(np.dot(dy(x_, y_), np.conj(np.transpose(dx(x_, y_))))[0][0]), + ], + [ + 4 * np.imag(np.dot(dy(x_, y_), np.conj(np.transpose(dx(x_, y_))))[0][0]), + 4 * np.imag(np.dot(dy(x_, y_), np.conj(np.transpose(dy(x_, y_))))[0][0]), + ], + ] + + np.testing.assert_array_almost_equal( + state_grad.assign_parameters(value_dict).eval(), correct_values, decimal=3 + ) + @unittest.skipIf(not optionals.HAS_JAX, "Skipping test due to missing jax module.") @idata(product(["lin_comb", "param_shift", "fin_diff"], [True, False])) @unpack @@ -1197,6 +1279,29 @@ def test_qfi_simple(self, method): actual = qfi.assign_parameters(value_dict).eval() np.testing.assert_array_almost_equal(actual, correct_values[i], decimal=1) + def test_qfi_phase_fix(self): + """Test the phase-fix argument in a QFI calculation + + QFI = [[1, 0], [0, 1]]. + """ + # create the circuit + a, b = Parameter("a"), Parameter("b") + qc = QuantumCircuit(1) + qc.h(0) + qc.rz(a, 0) + qc.rx(b, 0) + + # convert the circuit to a QFI object + op = CircuitStateFn(qc) + qfi = LinCombFull(phase_fix=False).convert(operator=op, params=[a, b]) + + # test for different values + value_dict = {a: np.pi / 4, b: 0.1} + correct_values = [[1, 0], [0, 1]] + + actual = qfi.assign_parameters(value_dict).eval() + np.testing.assert_array_almost_equal(actual, correct_values, decimal=2) + def test_qfi_maxcut(self): """Test the QFI for a simple MaxCut problem. @@ -1322,9 +1427,8 @@ def test_qfi_circuit_shared_params(self): for i, (circuit_set, list_op) in enumerate(zip(circuit_sets, list_ops)): for j, (reference, composed_op) in enumerate(zip(circuit_set, list_op)): with self.subTest(f"set {i} circuit {j}"): - self.assertEqual( - base.compose(composed_op[1].primitive), base.compose(reference) - ) + primitive = composed_op[1].primitive + self.assertEqual(base.compose(primitive), base.compose(reference)) def test_overlap_qfi_bound_parameters(self): """Test the overlap QFI works on a circuit with multi-parameter bound gates.""" @@ -1356,6 +1460,132 @@ def test_overlap_qfi_raises_on_unsupported_gate(self): with self.assertRaises(NotImplementedError): _ = QFI("overlap_diag").convert(StateFn(circuit), [x]) + @data(-Y, Z - 1j * Y) + def test_aux_meas_op(self, aux_meas_op): + """Test various auxiliary measurement operators for probability gradients with LinComb + Gradient. + + """ + + a = Parameter("a") + b = Parameter("b") + params = [a, b] + + q = QuantumRegister(1) + qc = QuantumCircuit(q) + qc.h(q) + qc.rz(params[0], q[0]) + qc.rx(params[1], q[0]) + + op = CircuitStateFn(primitive=qc, coeff=1.0) + + shots = 10000 + + prob_grad = LinComb(aux_meas_op=aux_meas_op).convert(operator=op, params=params) + value_dicts = [{a: [np.pi / 4], b: [0]}, {a: [np.pi / 2], b: [np.pi / 4]}] + if aux_meas_op == -Y: + correct_values = [ + [[-0.5, 0.5], [-1 / (np.sqrt(2) * 2), -1 / (np.sqrt(2) * 2)]], + [[-1 / (np.sqrt(2) * 2), 1 / (np.sqrt(2) * 2)], [0, 0]], + ] + else: + correct_values = [ + [[-0.5j, 0.5j], [(1 - 1j) / (np.sqrt(2) * 2), (-1 - 1j) / (np.sqrt(2) * 2)]], + [ + [-1j / (np.sqrt(2) * 2), 1j / (np.sqrt(2) * 2)], + [1 / (np.sqrt(2) * 2), -1 / (np.sqrt(2) * 2)], + ], + ] + + for backend_type in ["qasm_simulator", "statevector_simulator"]: + + for j, value_dict in enumerate(value_dicts): + + q_instance = QuantumInstance( + backend=BasicAer.get_backend(backend_type), shots=shots + ) + result = ( + CircuitSampler(backend=q_instance) + .convert(prob_grad, params=value_dict) + .eval()[0] + ) + if backend_type == "qasm_simulator": # sparse result + result = [result[0].toarray()[0], result[1].toarray()[0]] + for i, item in enumerate(result): + np.testing.assert_array_almost_equal(item, correct_values[j][i], decimal=1) + + def test_unsupported_aux_meas_op(self): + """Test error for unsupported auxiliary measurement operator in LinComb Gradient. + + dp0/da = cos(a)sin(b) / 2 + dp1/da = - cos(a)sin(b) / 2 + dp0/db = sin(a)cos(b) / 2 + dp1/db = - sin(a)cos(b) / 2 + """ + + a = Parameter("a") + b = Parameter("b") + params = [a, b] + + q = QuantumRegister(1) + qc = QuantumCircuit(q) + qc.h(q) + qc.rz(params[0], q[0]) + qc.rx(params[1], q[0]) + + op = CircuitStateFn(primitive=qc, coeff=1.0) + + shots = 8000 + + aux_meas_op = X + + with self.assertRaises(ValueError): + prob_grad = LinComb(aux_meas_op=aux_meas_op).convert(operator=op, params=params) + value_dict = {a: [np.pi / 4], b: [0]} + + backend = BasicAer.get_backend("qasm_simulator") + q_instance = QuantumInstance(backend=backend, shots=shots) + CircuitSampler(backend=q_instance).convert(prob_grad, params=value_dict).eval() + + def test_nat_grad_error(self): + """Test the NaturalGradient throws an Error. + + dp0/da = cos(a)sin(b) / 2 + dp1/da = - cos(a)sin(b) / 2 + dp0/db = sin(a)cos(b) / 2 + dp1/db = - sin(a)cos(b) / 2 + """ + method = "lin_comb" + a = Parameter("a") + b = Parameter("b") + params = [a, b] + + qc = QuantumCircuit(2) + qc.h(1) + qc.h(0) + qc.sdg(1) + qc.cz(0, 1) + qc.ry(params[0], 0) + qc.rz(params[1], 0) + qc.h(1) + + obs = (Z ^ X) - (Y ^ Y) + op = StateFn(obs, is_measurement=True) @ CircuitStateFn(primitive=qc) + + backend_type = "qasm_simulator" + shots = 1 + value = [0, np.pi / 2] + + backend = BasicAer.get_backend(backend_type) + q_instance = QuantumInstance( + backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 + ) + grad = NaturalGradient(grad_method=method).gradient_wrapper( + operator=op, bind_params=params, backend=q_instance + ) + with self.assertRaises(ValueError): + grad(value) + if __name__ == "__main__": unittest.main()