Source code for qmetro.protocols.mop

from __future__ import annotations

from collections.abc import Iterable
from warnings import warn, catch_warnings, filterwarnings

import cvxpy as cp
import numpy as np

from ..param_channel import ParamChannel
from ..qtools import minimize_alpha, comb_variables, krauses_kron, hc

from .errors import EnvDimsError
from .warnings import ENV_FOR_SINGLE, COMB_FOR_SINGLE




[docs] def mop_channel_qfi(channel: ParamChannel, env_inp_state: np.ndarray | None = None, **kwargs ) -> float: """ Computes quantum Fisher information for a single parametrized channel using the minimization over purifications (MOP) method :cite:`dulian2025,Demkowicz2012,kurdzialek2024`. Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. In case this argument is a comb created from single channels the whole comb will be treated as a single channel. env_inp_state : np.ndarray | None, optional Density matrix of the initial state of the environment. If None then it becomes a maximally mixed state. **kwargs Additional keyword arguments passed to the CVXPY ``solve`` method (see `docs <https://www.cvxpy.org/tutorial/solvers/index.html>`_). Returns ------- qfi : float maximal QFI of a single channel. """ if not channel.trivial_env: warn(ENV_FOR_SINGLE) channel = channel.trace_env(env_inp_state) if channel.is_comb: warn(COMB_FOR_SINGLE) return 4 * minimize_alpha(*channel.dkrauses(), **kwargs)
[docs] def mop_parallel_qfi(channel: ParamChannel, number_of_channels: int, env_inp_state: np.ndarray | None = None, **kwargs) -> float: """ Computes quantum Fisher information in a scenario with parallel channels :cite:`dulian2025,Demkowicz2012`. Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. number_of_channels : int, optional Number of channel uses. In case ``channel`` is a comb created from ``m`` single channels the total number of channels will be equal to ``number_of_channels * m``. env_inp_state : np.ndarray | None, optional Density matrix of the initial state of the environment. If None then it becomes a maximally mixed state. **kwargs Additional keyword arguments passed to the CVXPY ``solve`` method (see `docs <https://www.cvxpy.org/tutorial/solvers/index.html>`_). Returns ------- qfi : float Qunatum Fisher information. """ if (channel.env_inp_dim != channel.env_out_dim and number_of_channels > 1): raise EnvDimsError(channel.env_inp_dim, channel.env_out_dim) with catch_warnings(): filterwarnings('ignore', message=COMB_FOR_SINGLE) comb = channel.markov_series(number_of_channels) comb = comb.trace_env(env_inp_state) return mop_channel_qfi(comb, **kwargs)
[docs] def mop_adaptive_qfi(channel: ParamChannel, number_of_channels: int, env_control: bool | tuple[bool, bool] = False, env_inp_state: np.ndarray | None = None, input_pure_qfi: float = 0, **kwargs) -> float: """ Computes the comb QFI using minimization over purifications method (MOP) :cite:`dulian2025,kurdzialek2024,Altherr2021`. This function computes the QFI of comb consisting of ``number_of_channels`` parameter-encoding channels connected with their environments. Notice that ``channel`` may be a single channel or a comb created by the user. The resulting QFI is achievable when arbitrary adaptive control can be applied in each step. Control may act between channels, but also between different teeth of ``channel``, when ``channel`` represents a comb. Parameters ---------- channel: ParamChannel Parametrized channel characterizing signal encoding. Might be a single channel or a comb. number_of_channels : int Number of elementary channels/combs linked to create the full comb of interest. env_control: bool | tuple[bool, bool], optional If True, then adaptive control may also act on first input and last output environment. If tuple, then its elements refer to input and output environment respectively. env_inp_state : np.ndarray | None, optional Density matrix of the initial state of the environment. If None then it becomes a maximally mixed state. This argument is ignored when environment input is controlled. input_pure_qfi : float, optional The QFI of an additional pure parameter-dependent state. This state can be treated as a first tooth of estimated comb. By default, there is no such a state (input_pure_qfi = 0). Non-zero values are typically used to calculate bounds. **kwargs Additional keyword arguments passed to the CVXPY ``solve`` method (see `docs <https://www.cvxpy.org/tutorial/solvers/index.html>`_). Returns ------- float Quantum Fisher Information optimized over all comb controls. """ env_inp_dim = channel.env_inp_dim env_out_dim = channel.env_out_dim # Processing env_control parameter if isinstance(env_control, Iterable): env_inp_control, env_out_control = env_control else: env_inp_control = env_out_control = env_control # Creating the full comb for which the QFI will be computed comb_channel = channel.markov_series(number_of_channels) # Contracting input state with input environment (if env not # controlled) if not env_inp_control and not comb_channel.trivial_env_inp: comb_channel = comb_channel.trace_env_inp(env_inp_state) #number of teeth of created comb teeth_number = len(comb_channel.input_spaces) #names of all input and output spaces according to causal order #these lists include environment if controlled if env_inp_control: #CAREFUL: order matters in the line below! input_spaces = [comb_channel.env_inp] + comb_channel.input_spaces else: input_spaces = comb_channel.input_spaces if env_out_control: #CAREFUL: order matters in the line below! output_spaces = comb_channel.output_spaces + [comb_channel.env_out] else: output_spaces = comb_channel.output_spaces # creating list of dimension of all spaces according to causal order dims = [] for i in range(teeth_number): dims.append(comb_channel.input_dims[i]) dims.append(comb_channel.output_dims[i]) # environment is merged with first input/ last output space if env_inp_control: dims[0] *= env_inp_dim if env_out_control: dims[-1] *= env_out_dim # Kraus operators are created with reverse order of input and output # spaces krauses_comb, dkrauses_comb = comb_channel.tensor().dkrauses( input_spaces[::-1], output_spaces[::-1] ) #add additional pure state to existing Krauses if input_pure_qfi > 0 if input_pure_qfi != 0: # Input pure state and its derivative Ku = [np.array([[1], [0]])] dKu = [np.array([[0], [np.sqrt(input_pure_qfi) / 2]])] # Creating Kraus operators of extended comb (with input state) krauses_comb, dkrauses_comb = krauses_kron( krauses_comb, dkrauses_comb, Ku, dKu ) #updating dimensions dims = np.concatenate([[1, 2], dims]) # Number of Kraus operators and total dimension num_kraus = len(krauses_comb) total_dim = np.prod(dims) # Decomposition vectors of Choi operator C_vectors = np.array([K.flatten() for K in krauses_comb]) dC_vectors = np.array([dK.flatten() for dK in dkrauses_comb]) # Formulate the SDP problem dims_traced_output = list(dims).copy() dims_traced_output[-1] = 1 #Comb variables and constraints #trace_var: variable, plays the role of lambda from [1] combs, constraints, trace_var = comb_variables( tuple(dims_traced_output), trace_constraint=None ) # h matrix generating different Kraus representation h = cp.Variable((num_kraus, num_kraus), hermitian=True) # Create matrix containing `c` vectors (last dimension of `C` traced # out) horizontal `c` vectors are stacked vertically c_list_shape = num_kraus * dims[-1], total_dim // dims[-1] clist = np.reshape(C_vectors, c_list_shape) #the same for derivatives dclist = np.reshape(dC_vectors, c_list_shape) #derivatives transformed by matrix `h` tclist = dclist - 1j * cp.kron(h, np.eye(dims[-1])) @ clist #Create block matrix `A` (constrained to be positive) A00 = np.eye(num_kraus * dims[-1]) A01 = tclist A10 = hc(A01) A11 = combs[-1] A = cp.bmat([[A00, A01], [A10, A11]]) constraints.append(A >> 0) # Define the objective and solve obj = cp.Minimize(trace_var) prob = cp.Problem(obj, constraints) sol = prob.solve(**kwargs) return 4 * sol