import pennylane as qml
from pennylane import numpy as qnp
import qnetvo
from .qnodes import qubit_probs_qnode_fn
from .optimize import optimize
[docs]def qubit_covariance_matrix_fn(prep_node, meas_wires=None, dev_kwargs={}, qnode_kwargs={}):
"""Generates a function that evaluates the covariance matrix for local
qubit measurements.
Each local qubit is measured in the :math:`z`-basis and is preced by an arbitrary
qubit rotation as defined in PennyLane, |rot_ref|_.
Using the joint probability distribution :math:`\{P(x_i)\}_i` constructed from the quantum circuit evaluation,
we can evaluate the **covariance matrix** of an :math:`n`-qubit system as
.. math::
\\text{Cov}(\\{P(x_i)\\}_{i}) = \\begin{pmatrix}
\\text{Var}(x_1) & \\text{Cov}(x_1,x_2) & \\dots & \\text{Cov}(x_1, x_n) \\\\
\\text{Cov}(x_2, x_1) & \\text{Var}(x_2) & \\dots & \\text{Cov}(x_2, x_n) \\\\
\\vdots & & \\ddots & \\vdots \\\\
\\text{Cov}(x_n, x_1) & \\dots & \\text{Cov}(x_n, x_{n-1} & \\text{Var}(x_1, x_n) \\\\
\\end{pmatrix}
where for two random variables :math:`x_i` and :math:`x_j`, the covariance is define
:math:`\\text{Cov}(x_i,x_j) = \\langle (x_i - \\langle x_i \\rangle) (x_j - \\langle x_j \\rangle) \\rangle`
and the variance is defined :math:`\\text{Var}(x_i) = \\text{Cov}(x_i, x_i)`.
Note that the covariance matrix is symmetric because :math:`\\text{Cov}(x_i, x_j) = \\text{Cov}(x_j, x_i)`.
.. |rot_ref| replace:: ``qml.Rot()``
.. _rot_ref: https://pennylane.readthedocs.io/en/stable/code/api/pennylane.Rot.html?highlight=rot#pennylane.Rot
:param prep_node: A network node that prepares the quantum state to evaluate.
:type prep_node: qnetvo.PrepareNode
:param meas_wires: The wires to measure when evaluating the covariance matrix. If ``meas_wires`` are not specified,
all wires in the prepare node are considered. This can be used to ignore ancillary qubits.
:type meas_wires: list[int]
:param dev_kwargs: Keyword arguments passed to the PennyLane device constructor.
:type dev_kwargs: dict
:param qnode_kwargs: Keyword arguments passed to the PennyLane qnode constructor.
:type qnode_kwargs: dict
:returns: A function, ``covariance_matrix(meas_settings)`` that takes as input a
``list[float]`` of length ``3 * num_wires``.
:rtype: function
"""
wires = meas_wires if meas_wires else prep_node.wires
probs_qnode, dev = qubit_probs_qnode_fn(
prep_node, meas_wires=meas_wires, dev_kwargs=dev_kwargs, qnode_kwargs=qnode_kwargs
)
return lambda meas_settings: qml.math.cov_matrix(
probs_qnode(meas_settings),
[qml.PauliZ(wire) for wire in wires],
wires=qml.wires.Wires(wires),
)
[docs]def qubit_covariance_cost_fn(prep_node, meas_wires=None, dev_kwargs={}, qnode_kwargs={}):
"""Constructs a cost function that, when minimized, yields the maximal
distance between the covariance matrix of the `prep_node` and the origin.
That is, :math:`\\text{Cost}(\\Theta) = -\\text{Tr}[\\text{Cov}(\\{P(x_i|\\vec{\\theta}_i)\\}_i)^T \\text{Cov}(\\{P(x_i)\\}_i)^T]`
where the ``meas_settings`` are :math:`\\Theta = (\\vec{\\theta}_i\\in\\mathbb{R}^3)_{i=1}^n`.
:param prep_node: A network node that prepares the quantum state to evaluate.
:type prep_node: qnetvo.PrepareNode
:param meas_wires: The wires to measure when evaluating the covariance matrix. If ``meas_wires`` are not specified,
all wires in the prepare node are considered. This can be used to ignore ancillary qubits.
:type meas_wires: list[int]
:param qnode_kwargs: Keyword arguments passed to the PennyLane qnode constructor.
:type qnode_kwargs: dict
:param dev_kwargs: Keyword arguments passed to the PennyLane device constructor.
:type dev_kwargs: dict
:returns: A function evaluated as ``cost(meas_settings)`` that takes as input a
``list[float]`` of length ``3 * num_wires``.
:rtype: function
"""
cov_mat = qubit_covariance_matrix_fn(
prep_node, meas_wires=meas_wires, dev_kwargs=dev_kwargs, qnode_kwargs=qnode_kwargs
)
def qubit_covariance_cost(meas_settings):
mat = cov_mat(meas_settings)
return -qml.math.trace(mat.T @ mat)
return qubit_covariance_cost
[docs]def optimize_covariance_matrix(
prep_node,
meas_wires=None,
dev_kwargs={},
qnode_kwargs={},
**opt_kwargs,
):
"""Optimizes the arbitrary qubit measurements to maximize the distance between
the covariance matrix and the origin.
The optimization is performed using the :meth:`qnetti.optimize` method where
the cost function is constructed using the :meth:`qubit_covariance_cost_fn` method.
:param prep_node: A network node that prepares the quantum state to evaluate.
:type prep_node: qnetvo.PrepareNode
:param meas_wires: The wires to measure when evaluating the covariance matrix. If ``meas_wires`` are not specified,
all wires in the prepare node are considered. This can be used to ignore ancillary qubits.
:type meas_wires: list[int]
:param dev_kwargs: Keyword arguments passed to the PennyLane device constructor.
:type dev_kwargs: dict
:param qnode_kwargs: Keyword arguments passed to the PennyLane qnode constructor.
:type qnode_kwargs: dict
:param step_size: The step to take in the direction of steepest descent. Default ``step_size=0.1``.
:type step_size: float
:param num_steps: The number of iterations of gradient descent to perform. Default ``num_steps=20``.
:type num_steps: int
:param verbose: If ``True``, the iteration step and cost will be printed every 5 iterations.
:type verbose: bool
:returns: The first element of the returned tuple is the optimal covariance matrix while
the second element is the dictionary returned from the :meth:`qnetti.optimize` method.
:rtype: tuple[matrix, dict]
"""
num_wires = len(meas_wires if meas_wires else prep_node.wires)
init_settings = 2 * qnp.pi * qnp.random.rand(3 * num_wires, requires_grad=True)
cov_cost = qubit_covariance_cost_fn(
prep_node, meas_wires=meas_wires, dev_kwargs=dev_kwargs, qnode_kwargs=qnode_kwargs
)
opt_dict = optimize(
cov_cost,
init_settings,
**opt_kwargs,
)
cov_mat_fn = qubit_covariance_matrix_fn(
prep_node, meas_wires=meas_wires, dev_kwargs=dev_kwargs, qnode_kwargs=qnode_kwargs
)
cov_mat = cov_mat_fn(opt_dict["opt_settings"])
return cov_mat, opt_dict