# This code is part of Qiskit.
#
# (C) Copyright IBM 2020.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

r"""
Gradients (:mod:`qiskit.opflow.gradients`)
==========================================

Given an operator that represents either a quantum state resp. an expectation value, the gradient
framework enables the evaluation of gradients, natural gradients, Hessians, as well as the Quantum
Fisher Information.

Suppose a parameterized quantum state `|ψ(θ)〉 = V(θ)|ψ〉` with input state `|ψ〉` and parameterized
Ansatz `V(θ)`, and an Operator `O(ω)`.

**Gradients**

We want to compute one of:
* :math:`d⟨ψ(θ)|O(ω)|ψ(θ)〉/ dω`
* :math:`d⟨ψ(θ)|O(ω)|ψ(θ)〉/ dθ`
* :math:`d⟨ψ(θ)|i〉⟨i|ψ(θ)〉/ dθ`

The last case corresponds to the gradient w.r.t. the sampling probabilities of `|ψ(θ)`.
These gradients can be computed with different methods, i.e. a parameter shift, a linear combination
of unitaries and a finite difference method.

**Examples**

.. code-block::

   x = Parameter('x')
   ham = x * X
   a = Parameter('a')

   q = QuantumRegister(1)
   qc = QuantumCircuit(q)
   qc.h(q)
   qc.p(params[0], q[0])
   op = ~StateFn(ham) @ CircuitStateFn(primitive=qc, coeff=1.)

   value_dict = {x: 0.1, a: np.pi / 4}

   ham_grad = Gradient(grad_method='param_shift').convert(operator=op, params=[x])
   ham_grad.assign_parameters(value_dict).eval()

   state_grad = Gradient(grad_method='lin_comb').convert(operator=op, params=[a])
   state_grad.assign_parameters(value_dict).eval()

   prob_grad = Gradient(grad_method='fin_diff').convert(
      operator=CircuitStateFn(primitive=qc, coeff=1.), params=[a]
   )
   prob_grad.assign_parameters(value_dict).eval()

**Hessians**

We want to compute one of:
* :math:`d^2⟨ψ(θ)|O(ω)|ψ(θ)〉/ dω^2`
* :math:`d^2⟨ψ(θ)|O(ω)|ψ(θ)〉/ dθ^2`
* :math:`d^2⟨ψ(θ)|O(ω)|ψ(θ)〉/ dθ dω`
* :math:`d^2⟨ψ(θ)|i〉⟨i|ψ(θ)〉/ dθ^2`

The last case corresponds to the Hessian w.r.t. the sampling probabilities of `|ψ(θ)〉`.
Just as the first order gradients, the Hessians can be evaluated with different methods, i.e. a
parameter shift, a linear combination of unitaries and a finite difference method.
Given a tuple of parameters ``Hessian().convert(op, param_tuple)`` returns the value for the second
order derivative.
If a list of parameters is given ``Hessian().convert(op, param_list)`` returns the full Hessian for
all the given parameters according to the given parameter order.

**QFI**

The Quantum Fisher Information `QFI` is a metric tensor which is representative for the
representation capacity of a parameterized quantum state `|ψ(θ)〉 = V(θ)|ψ〉` generated by an
input state `|ψ〉` and a parameterized Ansatz `V(θ)`.
The entries of the `QFI` for a pure state read
:math:`\mathrm{QFI}_{kl} = 4 \mathrm{Re}[〈∂kψ|∂lψ〉−〈∂kψ|ψ〉〈ψ|∂lψ〉]`.

Just as for the previous derivative types, the QFI can be computed using different methods: a full
representation based on a linear combination of unitaries implementation, a block-diagonal and a
diagonal representation based on an overlap method.

**Examples**

.. code-block::

   q = QuantumRegister(1)
   qc = QuantumCircuit(q)
   qc.h(q)
   qc.p(params[0], q[0])
   op = ~StateFn(ham) @ CircuitStateFn(primitive=qc, coeff=1.)

   value_dict = {x: 0.1, a: np.pi / 4}

   qfi = QFI('lin_comb_full').convert(
         operator=CircuitStateFn(primitive=qc, coeff=1.), params=[a]
   )
   qfi.assign_parameters(value_dict).eval()

**NaturalGradients**

The natural gradient is a special gradient method which re-scales a gradient w.r.t. a state
parameter with the inverse of the corresponding Quantum Fisher Information (QFI)
:math:`\mathrm{QFI}^{-1} d⟨ψ(θ)|O(ω)|ψ(θ)〉/ dθ`.
Hereby, we can choose a gradient as well as a QFI method and a regularization method which is used
together with a least square solver instead of exact inversion of the QFI:

**Examples**

.. code-block::

   op = ~StateFn(ham) @ CircuitStateFn(primitive=qc, coeff=1.)
   nat_grad = NaturalGradient(grad_method='lin_comb,
                              qfi_method='lin_comb_full',
                              regularization='ridge').convert(operator=op, params=params)

The derivative classes come with a `gradient_wrapper()` function which returns the corresponding
callable and are thus compatible with the optimizers.

.. currentmodule:: qiskit.opflow.gradients

Base Classes
------------

.. autosummary::
   :toctree: ../stubs/
   :template: autosummary/class_no_inherited_members.rst

   DerivativeBase
   GradientBase
   HessianBase
   QFIBase

Converters
----------

.. autosummary::
   :toctree: ../stubs/
   :template: autosummary/class_no_inherited_members.rst

   CircuitGradient
   CircuitQFI

Derivatives
-----------

.. autosummary::
   :toctree: ../stubs/
   :template: autosummary/class_no_inherited_members.rst

   Gradient
   Hessian
   NaturalGradient
   QFI

"""

from .circuit_gradients.circuit_gradient import CircuitGradient
from .circuit_qfis.circuit_qfi import CircuitQFI
from .derivative_base import DerivativeBase
from .gradient_base import GradientBase
from .gradient import Gradient
from .natural_gradient import NaturalGradient
from .hessian_base import HessianBase
from .hessian import Hessian
from .qfi_base import QFIBase
from .qfi import QFI

__all__ = [
    "DerivativeBase",
    "CircuitGradient",
    "GradientBase",
    "Gradient",
    "NaturalGradient",
    "HessianBase",
    "Hessian",
    "QFIBase",
    "QFI",
    "CircuitQFI",
]
