# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2021, 2023.
#
# 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.
"""The ElectronicDipoleMoment property."""
from __future__ import annotations
from typing import Mapping, MutableMapping, Optional, Tuple, cast
import qiskit_nature # pylint: disable=unused-import
from qiskit_nature.second_q.operators import ElectronicIntegrals, FermionicOp
# A dipole moment, when present as X, Y and Z components will normally have float values for all the
# components. However when using Z2Symmetries, if the dipole component operator does not commute
# with the symmetry then no evaluation is done and None will be used as the 'value' indicating no
# measurement of the observable took place
DipoleTuple = Tuple[Optional[float], Optional[float], Optional[float]]
[docs]class ElectronicDipoleMoment:
r"""The ElectronicDipoleMoment property.
This Property implements the operator which evaluates the **electronic** dipole moment, based on
the electronic integrals:
.. math::
\vec{d}_{pq} = \int \phi_{p} \vec{r} \phi_{q} ,
where the integral is over the all space. The operator can then be expressed as:
.. math::
\hat{d} = \sum_{p, q} d^x_{pq} a^\dagger_p a_q ,
where :math:`x` can be any of the three Cartesian axes (:math:`\{x, y, z\}`).
Just like in the :class:`qiskit_nature.second_q.hamiltonians.ElectronicEnergy`, the nuclear
contribution is stored **separately** in the :attr:`nuclear_dipole_moment` attribute and will
**not** be included into the generated operator. It is however possible, to manually add this
constant shift to the electronic dipole components as a constant term in the internal
:class:`qiskit_nature.second_q.operators.ElectronicIntegrals` instances. Assuming you have
obtained an ``ElectronicDipoleMoment`` instance (for example from one of the classical code
drivers), you can add the nuclear component to be included in the qubit operator like so:
.. code-block:: python
from qiskit_nature.second_q.operators import PolynomialTensor
# you have obtained your dipole moment property and store it in this variable
dipole: ElectronicDipoleMoment
nuclear_dip = dipole.nuclear_dipole_moment
dipole.x_dipole.alpha += PolynomialTensor({"": nuclear_dip[0]})
dipole.y_dipole.alpha += PolynomialTensor({"": nuclear_dip[1]})
dipole.z_dipole.alpha += PolynomialTensor({"": nuclear_dip[2]})
The following attributes can be set via the initializer but can also be read and updated once
the ``ElectronicDipoleMoment`` object has been constructed.
Attributes:
x_dipole (ElectronicIntegrals): the ``ElectronicIntegrals`` for the :math:`x`-axis component.
y_dipole (ElectronicIntegrals): the ``ElectronicIntegrals`` for the :math:`y`-axis component.
z_dipole (ElectronicIntegrals): the ``ElectronicIntegrals`` for the :math:`z`-axis component.
constants (MutableMapping[str, DipoleTuple]): a mapping of constant dipole offsets, not
mapped to the qubit operator. Each entry must be a tuple of length three (for the three
Cartesian axes).
reverse_dipole_sign: whether or not to reverse the sign of the computed electronic dipole
moment when adding it to the :attr:`nuclear_dipole_moment` to obtain the total.
"""
def __init__(
self,
x_dipole: ElectronicIntegrals,
y_dipole: ElectronicIntegrals,
z_dipole: ElectronicIntegrals,
*,
constants: MutableMapping[str, DipoleTuple] = None,
reverse_dipole_sign: bool = False,
) -> None:
"""
Args:
x_dipole: the :class:`qiskit_nature.second_q.operators.ElectronicIntegrals` for the
:math:`x`-axis component.
y_dipole: the :class:`qiskit_nature.second_q.operators.ElectronicIntegrals` for the
:math:`y`-axis component.
z_dipole: the :class:`qiskit_nature.second_q.operators.ElectronicIntegrals` for the
:math:`z`-axis component.
constants: a mapping of constant dipole offsets, not mapped to the qubit operator.
Each entry must be a tuple of length three (for the three Cartesian axes).
reverse_dipole_sign: whether or not to reverse the dipole sign.
"""
self.x_dipole = x_dipole
self.y_dipole = y_dipole
self.z_dipole = z_dipole
self.constants = constants if constants is not None else {}
self.reverse_dipole_sign = reverse_dipole_sign
@property
def nuclear_dipole_moment(self) -> DipoleTuple | None:
"""The nuclear dipole moment.
See :attr:`qiskit_nature.second_q.hamiltonians.ElectronicEnergy.nuclear_repulsion_energy`
for an example on how to add the constant terms as offsets to the internal
:class:`qiskit_nature.second_q.operators.ElectronicIntegrals`.
"""
return self.constants.get("nuclear_dipole_moment", None)
@nuclear_dipole_moment.setter
def nuclear_dipole_moment(self, d_nuc: DipoleTuple) -> None:
self.constants["nuclear_dipole_moment"] = d_nuc
[docs] def second_q_ops(self) -> Mapping[str, FermionicOp]:
"""Returns the second quantized dipole moment operators.
Returns:
A mapping of strings to `FermionicOp` objects.
"""
ops = {}
ops["XDipole"] = FermionicOp.from_polynomial_tensor(self.x_dipole.second_q_coeffs())
ops["YDipole"] = FermionicOp.from_polynomial_tensor(self.y_dipole.second_q_coeffs())
ops["ZDipole"] = FermionicOp.from_polynomial_tensor(self.z_dipole.second_q_coeffs())
return ops
[docs] def interpret(
self, result: "qiskit_nature.second_q.problems.EigenstateResult" # type: ignore[name-defined]
) -> None:
"""Interprets an :class:`qiskit_nature.second_q.problems.EigenstateResult`.
In particular, this extracts the evaluated electronic dipole moment values from the
auxiliary operator eigenvalues and this adds the constant dipole shifts stored in this
property to the result object.
Args:
result: the result to add meaning to.
"""
result.nuclear_dipole_moment = self.nuclear_dipole_moment
result.reverse_dipole_sign = self.reverse_dipole_sign
result.computed_dipole_moment = []
result.extracted_transformer_dipoles = []
if result.aux_operators_evaluated is None:
return
for aux_op_eigenvalues in result.aux_operators_evaluated:
if not isinstance(aux_op_eigenvalues, dict):
continue
dipole_moment = [None] * 3
for idx, axis in enumerate("XYZ"):
moment = aux_op_eigenvalues.get(f"{axis}Dipole", None)
if moment is not None:
dipole_moment[idx] = moment.real
result.computed_dipole_moment.append(cast(DipoleTuple, tuple(dipole_moment)))
result.extracted_transformer_dipoles.append(
{
name: constant
for name, constant in self.constants.items()
if name != "nuclear_dipole_moment"
}
)