Source code for qmetro.protocols.iss

from __future__ import annotations

from collections.abc import Hashable
from itertools import cycle
from warnings import warn, filterwarnings, catch_warnings

import numpy as np

from ..iss_opt import iss_opt
from ..qmtensor import (
    SpaceDict, TensorNetwork, VarTensor, ConstTensor,
    mps_var_tnet, choi_identity, mpo_measure_var_tnet, measure_var,
    input_state_var, comb_var
)
from ..param_channel import ParamChannel

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




[docs] def iss_channel_qfi(channel: ParamChannel, ancilla_dim: int = 1, env_inp_state: np.ndarray | None = None, artificial_noise_after: bool | None = True, **kwargs ) -> tuple[float, list[float], np.ndarray, np.ndarray, bool]: """ Computes quantum Fisher information for a single parametrized channel using iterative see-saw (ISS) method :cite:`dulian2025,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. ancilla_dim : int, optional Dimension of the ancilla, by default 1. 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. artificial_noise_after : bool | None, optional Whether auxiliary noise is added after the channel: - True : auxiliary noise is added after the channel, - False : auxiliary noise is added before the channel, - None : auxiliary noise is not used. By default True. kwargs : Additional arguments passed on to :func:`iss_opt <qmetro.iss_opt.main.iss_opt>` see :class:`IssConfig <qmetro.iss_opt.iss_config.IssConfig>` for details. Returns ------- qfi : float Quantum Fisher information. qfis : list[float] QFI per algorithm iteration number. input_state : np.ndarray Density matrix of the optimal input state. The input space of the channel goes first and the ancilla second. sld : np.ndarray Symmetric loagarithmic derivative. The output space of the channel goes first and the ancilla second. status : bool True if the algorithm converged, False otherwise. """ if not channel.trivial_env: warn(ENV_FOR_SINGLE) channel = channel.trace_env(env_inp_state) if channel.is_comb: warn(COMB_FOR_SINGLE) output_dim = channel.output_dim input_dim = channel.input_dim sd = SpaceDict() INPUT = 'INPUT' OUTPUT = 'OUTPUT' ANCILLA = 'ANCILLA' sd[INPUT] = input_dim sd[OUTPUT] = output_dim sd[ANCILLA] = ancilla_dim RHO0 = 'RHO0' rho0 = input_state_var([INPUT, ANCILLA], RHO0, sd) chann_ten = channel.tensor([INPUT], [OUTPUT], sdict=sd, name='CHANNEL') MEASUREMENT = 'MEASUREMENT' measure = measure_var([OUTPUT, ANCILLA], MEASUREMENT, sd) tn = rho0 * chann_ten * measure if artificial_noise_after is None: art_noise_spaces = [] elif artificial_noise_after: art_noise_spaces = [[OUTPUT]] else: art_noise_spaces = [[INPUT]] qfi, qfis, new_tn, status = iss_opt( tn, art_noise_spaces=art_noise_spaces, **kwargs ) rho0_arr = new_tn.tensors[RHO0].choi([INPUT, ANCILLA]) sld_arr = new_tn.tensors[MEASUREMENT].choi([OUTPUT, ANCILLA]) return qfi, qfis[0], rho0_arr, sld_arr, status
[docs] def iss_parallel_qfi(channel: ParamChannel, number_of_channels: int, ancilla_dim: int, artificial_noise_after: bool | None = None, env_inp_state: np.ndarray | None = None, **kwargs ) -> tuple[float, list[float], np.ndarray, np.ndarray, bool]: """ Computes quantum Fisher information for channels in parallel using iterative see-saw (ISS) algorithm :cite:`dulian2025,Chabuda2020`. In parallel strategy all channels are simultaneously probed by an entangled input state and their output is collectively measured. Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. number_of_channels : int 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`. ancilla_dim : int Dimension of the ancilla space. artificial_noise_after : bool | None, optional Whether auxiliary noise is added after the channel: - True : auxiliary noise is added after the channel, - False : auxiliary noise is added before the channel, - None : auxiliary noise is not used. By default None. 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 arguments passed on to `iss` optimization function. Returns ------- qfi : float Qunatum Fisher information. qfis : list[float] QFI per algorithm iteration number. input_state : np.ndarray Density matrix of the optimal input state. The input spaces of the channels go first and the ancilla goes last. sld : np.ndarray Symmetric loagarithmic derivative. The output spaces of the channels go first and the ancilla goes last. status : bool True if the algorithm converged, False otherwise. """ if (channel.env_inp_dim != channel.env_out_dim and number_of_channels > 1): raise EnvDimsError(channel.env_inp_dim, channel.env_out_dim) c = channel.markov_series(number_of_channels).trace_env(env_inp_state) c = c.merge_spaces() with catch_warnings(): filterwarnings('ignore', message=COMB_FOR_SINGLE) return iss_channel_qfi( c, ancilla_dim, None, artificial_noise_after, **kwargs )
[docs] def iss_tnet_parallel_qfi(channel: ParamChannel, number_of_channels: int, ancilla_dim: int, mps_bond_dim: int, measure_bond_dim: int, artificial_noise_after: bool | None = None, env_inp_state: np.ndarray | None = None, **kwargs ) -> tuple[float, list[float], list[np.ndarray], list[np.ndarray], bool]: """ Computes quantum Fisher information for channels in parallel using iterative see-saw algorithm and tensor networks :cite:`dulian2025,Chabuda2020`. In the parallel strategy all channels are simultaneously probed by an entangled input state and their output is collectively measured. In the approach with tensor networks the input state is expressed as a tensor network in a shape of a line where i-th tensor represents part of the input state that goes to the i-th probe channel and has is connected with two other tensors (one tensor in case of the first and the last one) with bond spaces/indices. The measurement and then the symmetric logarithmic derivative (SLD) matrix is expressed as an analogous tensor network (representing operators instead of states) called matrix product operator (MPO). Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. number_of_channels : int 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`. ancilla_dim : int Dimension of the ancilla space. mps_bond_dim : int Dimension of the bond space of the input state which is a matrix product state (MPS). measure_bond_dim : int Dimension of the bond space of the measurement (SLD) which is a matrix product operator. artificial_noise_after : bool | None, optional Whether auxiliary noise is added after the channel: - True : auxiliary noise is added after the channel, - False : auxiliary noise is added before the channel, - None : auxiliary noise is not used. By default None. 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 arguments passed on to `iss` optimization function. Returns ------- qfi : float Qunatum Fisher information. qfis : list[float] QFI per algorithm iteration number. input_state : list[np.array] The optimal matrix product state (MPS). The i-th tensor on the list - R_i is the input of the i-th channel except the last which is the part going to ancilla. Its indices are in the order: bond space connecting to the previous tensor (if it exists), input state of the channel/ancilla, bond space connecting to the next tensor (if it exists). sld : list[np.array] Matrix product operator (MPO) of the optimal symmetric logarithmic derivative (SLD). The i-th tensor on the list - L_i is sld MPO elemnt on the output of the i-th channel except the last one which is on ancilla. Its indices/spaces are in the order: bond space connecting to the previous tensor (if it exists), channel output or ancilla, bond space connecting to the next tensor (if it exists). status : bool True if the algorithm converged, False otherwise. """ if channel.env_inp_dim != channel.env_out_dim: if number_of_channels == 1: channel = channel.trace_env(env_inp_state) # To avoid error for env_inp_tensor declaration: env_inp_state = None else: raise EnvDimsError(channel.env_inp_dim, channel.env_out_dim) env_dim = channel.env_inp_dim n_combs = number_of_channels ch_per_comb = len(channel.input_spaces) n_ch = n_combs * ch_per_comb n_a = 1 n = n_ch + n_a sd = SpaceDict() inp = [('IN', i) for i in range(n_ch)] out = [('OUT', i) for i in range(n_ch)] for space, dim in zip(inp, cycle(channel.input_dims)): sd[space] = dim for space, dim in zip(out, cycle(channel.output_dims)): sd[space] = dim anc = sd.arrange_spaces(n_a, ancilla_dim, 'ANC') env = sd.arrange_spaces(n_combs + 1, env_dim, 'ENV') RHO0 = 'RHO0' mps, mps_names, mps_bonds = mps_var_tnet( inp + anc, RHO0, sd, mps_bond_dim ) ENV_INP_STATE = 'ENVIRONMENT INPUT STATE' if env_inp_state is None: env_inp_state = np.identity(env_dim) / env_dim env_inp_tensor = ConstTensor( [env[0]], sdict=sd, output_spaces=[env[0]], name=ENV_INP_STATE, choi=env_inp_state ) ENV_TRACE = 'ENVIRONMENT TRACE' env_trace = choi_identity([env[-1]], sdict=sd, name=ENV_TRACE) CHANNEL = 'CHANNEL' comb_tensors = [] comb_ten_names = [] for i in range(n_combs): x = i * ch_per_comb y = (i + 1) * ch_per_comb name = f'{CHANNEL}, {i}' comb_tensor = channel.tensor( inp[x:y], out[x:y], env[i], env[i+1], sd, name=name ) comb_ten_names.append(name) comb_tensors.append(comb_tensor) channels_tnet = TensorNetwork(comb_tensors, sd, CHANNEL) MEASUREMENT = 'MEASUREMENT' meas_mpo, meas_names, meas_bonds = mpo_measure_var_tnet( out + anc, MEASUREMENT, sd, measure_bond_dim ) tn = TensorNetwork( [mps, env_inp_tensor, channels_tnet, env_trace, meas_mpo], sdict=sd ) contraction_order = [] for i in range(n): contraction_order.append(mps_names[i]) if i == 0: contraction_order.append(ENV_INP_STATE) if i < n_ch and i % ch_per_comb == 0: j = int(i / ch_per_comb) contraction_order.append(comb_ten_names[j]) if j == n_combs - 1: contraction_order.append(ENV_TRACE) contraction_order.append(meas_names[i]) if artificial_noise_after is None: noise_spaces = [] elif artificial_noise_after: noise_spaces = [[out[i]] for i in range(n_ch)] else: noise_spaces = [[inp[i]] for i in range(n_ch)] qfi, qfis, new_tn, status = iss_opt( tn, art_noise_spaces=noise_spaces, contraction_order=contraction_order, **kwargs ) mps_result = [] sld_result = [] for i, (mps_name, sld_name) in enumerate(zip(mps_names, meas_names)): mps_el = new_tn.tensors[mps_name] sld_el = new_tn.tensors[sld_name] mps_spaces = [] sld_spaces = [] if i > 0: mps_spaces.append(mps_bonds[i - 1]) sld_spaces.append(meas_bonds[i - 1]) if i < len(mps_names) - 1: mps_spaces += [inp[i], mps_bonds[i]] sld_spaces += [out[i], meas_bonds[i]] else: mps_spaces.append(anc[0]) sld_spaces.append(anc[0]) mps_ten = mps_el.to_mps(mps_spaces)[-1] sld_ten = sld_el.reorder(sld_spaces).array mps_result.append(mps_ten) sld_result.append(sld_ten) return qfi, qfis[0], mps_result, sld_result, status
[docs] def iss_adaptive_qfi(channel : ParamChannel, number_of_channels: int, ancilla_dim: int, artificial_noise_after: bool | None = None, env_inp_state: np.ndarray | None = None, **kwargs ) -> tuple[float, list[float], np.ndarray, np.ndarray, bool]: """ Computes quantum Fisher information channels in an adaptive control system called quantum comb using iterative see-saw (ISS) algorithm :cite:`dulian2025,kurdzialek2024`. In this approach we consider n copies of a quantum channel Phi: Phi_i: L(env_i (x) in_i) -> L(env_i+1 (x) out_i) for i = 0, ..., n - 1, put in a quantum comb C from a set: Comb[(Null, in_0), (out_0, in_1), ..., (out_n-2, in_n-1 (x) anc)], and a measurement at the end: P: L(out_n-1 (x) anc) -> R, where (x) denotes a tensor product of Hilbert spaces. The input of the first environment space (env_0) is initialized with `env_inp_state` and the last environment spcace (env_n) is traced out. Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. In case this argument is a comb created from single channels the control operation (comb's tooth) will be put between every **single** channel. number_of_channels : int 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`. ancilla_dim : int Dimension of the ancilla space connecting controls (teeth), dim(anc). artificial_noise_after : bool, optional Whether auxiliary noise is added after the channel: - True : auxiliary noise is added after the channel, - False : auxiliary noise is added before the channel, - None : auxiliary noise is not used. By default True. 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. Returns ------- qfi : float Quantum Fisher information. qfis : list[floats] A list of quantum Fisher informations acvhieved in each iteration. The last value in the list is the final solution. comb : np.ndarray Choi matrix of the optimal comb with spaces in order: inp_0, ..., inp_n-1, anc, out_0, ..., out_n-2. L : np.ndarray Optimal syymetric logaritmic derivative (SLD) matrix with spaces in order: out_n-1, anc. status : bool True if the algorithm converged, False otherwise. """ if channel.env_inp_dim != channel.env_out_dim: if number_of_channels == 1: channel = channel.trace_env(env_inp_state) # To avoid error for env_inp_tensor declaration: env_inp_state = None else: raise EnvDimsError(channel.env_inp_dim, channel.env_out_dim) env_dim = channel.env_inp_dim # ch_comb for number "channel combs" i.e. combs of channels to # differentiate from comb of controls. n_ch_comb = number_of_channels ch_per_ch_comb = len(channel.input_spaces) n = n_ch_comb * ch_per_ch_comb sd = SpaceDict() inp = [('IN', i) for i in range(n)] out = [('OUT', i) for i in range(n)] for space, dim in zip(inp, cycle(channel.input_dims)): sd[space] = dim for space, dim in zip(out, cycle(channel.output_dims)): sd[space] = dim anc = 'ANC' sd[anc] = ancilla_dim env = sd.arrange_spaces(n_ch_comb + 1, env_dim, 'ENV') COMB = 'COMB' comb_structure = [] for i in range(n): tooth_inp = [] tooth_out = [inp[i]] if i > 0: tooth_inp.append(out[i - 1]) if i == n - 1: tooth_out.append(anc) comb_structure.append((tooth_inp, tooth_out)) comb = comb_var(comb_structure, sdict=sd, name=COMB) ENV_INP_STATE = 'ENVIRONMENT INPUT STATE' if env_inp_state is None: env_inp_state = np.identity(env_dim) / env_dim env_inp_tensor = ConstTensor( [env[0]], sdict=sd, output_spaces=[env[0]], name=ENV_INP_STATE, choi=env_inp_state ) ENV_TRACE = 'ENVIRONMENT TRACE' env_trace = choi_identity([env[-1]], sdict=sd, name=ENV_TRACE) CHANNEL = 'CHANNEL' ch_comb_ten_names = [] ch_comb_tensors = [] for i in range(n_ch_comb): x = i * ch_per_ch_comb y = (i + 1) * ch_per_ch_comb name = f'{CHANNEL}, {i}' ch_comb_tensor = channel.tensor( inp[x:y], out[x:y], env[i], env[i+1], sd, name=name ) ch_comb_ten_names.append(name) ch_comb_tensors.append(ch_comb_tensor) channels_tnet = TensorNetwork(ch_comb_tensors, sd, CHANNEL) MEASUREMENT = 'MEASUREMENT' m_tensor = measure_var([out[n - 1], anc], MEASUREMENT, sd) tn = comb * env_inp_tensor * channels_tnet * env_trace * m_tensor if artificial_noise_after is None: noise_spaces = [] elif artificial_noise_after: noise_spaces = [[out[i]] for i in range(n)] else: noise_spaces = [[inp[i]] for i in range(n)] contraction_order: list[str] = [ENV_INP_STATE] contraction_order.append(ch_comb_ten_names[0]) contraction_order.append(COMB) contraction_order += ch_comb_ten_names[1:] contraction_order.append(ENV_TRACE) contraction_order.append(MEASUREMENT) qfi, qfiss, new_tn, status = iss_opt( tn, art_noise_spaces=noise_spaces, contraction_order=contraction_order, **kwargs ) comb_arr = new_tn.tensors[COMB].choi(inp + [anc] + out[:-1]) sld_arr = new_tn.tensors[MEASUREMENT].choi([out[-1], anc]) return qfi, qfiss[0], comb_arr, sld_arr, status
[docs] def iss_tnet_adaptive_qfi(channel : ParamChannel, number_of_channels: int, ancilla_dim: int, unital_teeth: bool = False, initial_teeth: list[np.ndarray] | None = None, initial_sld: np.ndarray | None = None, artificial_noise_after: bool | None = True, fixed_teeth: list[tuple[int, np.ndarray]] | None = None, env_inp_state: np.ndarray | None = None, **kwargs ) -> tuple[ float, list[float], list[np.ndarray], np.ndarray, bool ]: """ Computes quantum Fisher information in an adaptive control system called quantum comb using iterative see-saw (ISS) algorithm :cite:`dulian2025,kurdzialek2024`. In this approach we consider n copies of a quantum channel Phi: Phi_i: L(env_i (x) in_i) -> L(env_i+1 (x) out_i) for i = 0, ..., n - 1, intertwined with n maps called teeth: - input state (or 0-th tooth): T_0: C -> L(in_0 (x) anc_0), - (proper) teeth: T_i: L(out_i-1 (x) anc_i-1) -> L(in_i (x) anc_i) for i = 1, ..., n - 1, which constitute an adaptive control system called quantum comb and a measurement at the end: P: L(out_n-1 (x) anc_n-1) -> R. The input of the first environment space (env_0) is initialized with `env_inp_state` and the last environment spcace (env_n) is traced out. Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. In case this argument is a comb created from single channels the control operation (tooth) will be put between every **single** channel. number_of_channels : int 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`. ancilla_dim : int Dimension of the ancilla space connecting controls (teeth), dim(anc_i). unital_teeth : bool, optional If True than all proper teeth (T_i with i>0) are constrained to be unital, that is they preserve identity matrix T_i(Id) = Id, by default False. initial_teeth : list[np.ndarray] | None, optional Initial values of teeth: [T_0, T_1, ...]. First element that is the value for the input state should be its density matrix with spaces in the order: (inp_0, anc_0). For proper teeth it should be Choi matrices with spaces in the order: (inp_i, anc_i, out_i-1, anc_i-1), by default None. initial_sld : np.ndarray | None, optional Initial value of SLD matrix with spaces in order (out_n-1, anc_n-1), by default None. artificial_noise_after : bool, optional Whether auxiliary noise is added after the channel: - True : auxiliary noise is added after the channel, - False : auxiliary noise is added before the channel, - None : auxiliary noise is not used. By default True. fixed_teeth : list[tuple[int, np.ndarray]] | None, optional Teeths to be fixed during optimization. Element (i, Ti) in the list means that tooth number i will be fixed to Ti. Note that this value will replace the value given in initial_teeth argument, by default None. 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. Returns ------- qfi : float Quantum Fisher information. qfis : list[floats] A list of quantum Fisher informations acvhieved in each iteration. The last value in the list is the final solution. Ts : list[np.ndarray] List of optimal teeth in the form of density matrix and Choi matrices. For the i-th element - T_i spaces are in order: - (inp_0, anc_0) for i=0, - (inp_i, anc_i, out_i-1, anc_i-1) for i>0. L : np.ndarray Optimal syymetric logaritmic derivative (SLD) matrix with spaces in order: out_n-1, anc. status : bool True if the algorithm converged, False otherwise. """ if channel.env_inp_dim != channel.env_out_dim: if number_of_channels == 1: channel = channel.trace_env(env_inp_state) # To avoid error for env_inp_tensor declaration: env_inp_state = None else: raise EnvDimsError(channel.env_inp_dim, channel.env_out_dim) if unital_teeth and channel.input_dims != channel.output_dims: raise UnitalDimsError(channel.input_dims, channel.output_dims) env_dim = channel.env_inp_dim # ch_comb for number "channel combs" i.e. combs of channels to # differentiate from comb of controls. n_ch_comb = number_of_channels ch_per_ch_comb = len(channel.input_spaces) n = n_ch_comb * ch_per_ch_comb sd = SpaceDict() inp = [('IN', i) for i in range(n)] out = [('OUT', i) for i in range(n)] for space, dim in zip(inp, cycle(channel.input_dims)): sd[space] = dim for space, dim in zip(out, cycle(channel.output_dims)): sd[space] = dim anc = sd.arrange_spaces(n, ancilla_dim, 'ANC') env = sd.arrange_spaces(n_ch_comb + 1, env_dim, 'ENV') fixed_teeth_dict = {} if fixed_teeth: for i, arr in fixed_teeth: fixed_teeth_dict[i] = arr COMB = 'COMB' teeth = [] teeth_names = [] for i in range(n): tooth_name = f'{COMB}, {i}' teeth_names.append(tooth_name) if i == 0: tooth_inp = [] else: tooth_inp = [out[i - 1], anc[i - 1]] tooth_out = [inp[i], anc[i]] if i not in fixed_teeth_dict: tooth_tensor = VarTensor( tooth_out + tooth_inp, sd, tooth_name, tooth_out, unital_teeth and i > 0 ) else: tooth_tensor = ConstTensor( tooth_out + tooth_inp, sdict=sd, name=tooth_name, output_spaces=tooth_out, choi=fixed_teeth_dict[i] ) teeth.append(tooth_tensor) comb = TensorNetwork(teeth, sd, COMB) ENV_INP_STATE = 'ENVIRONMENT INPUT STATE' if env_inp_state is None: env_inp_state = np.identity(env_dim) / env_dim env_inp_tensor = ConstTensor( [env[0]], sdict=sd, output_spaces=[env[0]], name=ENV_INP_STATE, choi=env_inp_state ) ENV_TRACE = 'ENVIRONMENT TRACE' env_trace = choi_identity([env[-1]], sdict=sd, name=ENV_TRACE) CHANNEL = 'CHANNEL' ch_comb_ten_names = [] ch_comb_tensors = [] for i in range(n_ch_comb): x = i * ch_per_ch_comb y = (i + 1) * ch_per_ch_comb name = f'{CHANNEL}, {i}' ch_comb_tensor = channel.tensor( inp[x:y], out[x:y], env[i], env[i+1], sd, name=name ) ch_comb_ten_names.append(name) ch_comb_tensors.append(ch_comb_tensor) channels_tnet = TensorNetwork(ch_comb_tensors, sd, CHANNEL) MEASUREMENT = 'MEASUREMENT' m_tensor = measure_var([out[-1], anc[-1]], MEASUREMENT, sd) tn = comb * env_inp_tensor * channels_tnet * env_trace * m_tensor init_tensors = [] if initial_teeth is not None: for i, init_tooth in enumerate(initial_teeth): spaces = [inp[i], anc[i]] if i > 0: spaces += [out[i - 1], anc[i - 1]] _ct = ConstTensor( spaces, choi=init_tooth, sdict=sd, output_spaces=[inp[i], anc[i]], name=teeth_names[i], ) init_tensors.append(_ct) if initial_sld is not None: t = ConstTensor( [out[-1], anc[-1]], initial_sld, sd, name=MEASUREMENT ) init_tensors.append(t) init_tn = TensorNetwork(init_tensors, sd) if init_tensors else None if artificial_noise_after is None: noise_spaces = [] elif artificial_noise_after: noise_spaces = [[out[i]] for i in range(n)] else: noise_spaces = [[inp[i]] for i in range(n)] contraction_order: list[str] = [] for i in range(n): contraction_order.append(teeth_names[i]) if i == 0: contraction_order.append(ENV_INP_STATE) if i % ch_per_ch_comb == 0: j = int(i / ch_per_ch_comb) contraction_order.append(ch_comb_ten_names[j]) contraction_order.append(ENV_TRACE) contraction_order.append(MEASUREMENT) qfi, qfiss, new_tn, status = iss_opt( tn, init_tn=init_tn, art_noise_spaces=noise_spaces, contraction_order=contraction_order, **kwargs ) teeth_arrs = [] for i in range(n): t = new_tn.tensors[teeth_names[i]] spaces: list[Hashable] = [inp[i], anc[i]] if i > 0: spaces += [out[i - 1], anc[i - 1]] t_arr = t.choi(spaces) teeth_arrs.append(t_arr) sld_arr = new_tn.tensors[MEASUREMENT].choi([out[n - 1], anc[n - 1]]) return qfi, qfiss[0], teeth_arrs, sld_arr, status
[docs] def iss_tnet_collisional_qfi(channel: ParamChannel, number_of_channels: int, ancilla_dim: int, mps_bond_dim: int, measure_bond_dim: int, unital_teeth: bool = False, initial_teeth: list[np.ndarray] | None = None, artificial_noise_after: bool | None = True, fixed_teeth: list[tuple[int, np.ndarray]] | None = None, env_inp_state: np.ndarray | None = None, **kwargs ) -> tuple[ float, list[float], list[np.ndarray], list[np.ndarray], list[np.ndarray], bool ]: """ Returns the quantum Fisher information in a scenario with channels in a comb whose teeth have separate ancillas using the iterative see-saw (ISS) algorithm :cite:`dulian2025,Chabuda2020,kurdzialek2024`. This is a very similar structure to the one in the adaptive strategy the only difference being that the comb's teeth cannot communicate with each other using their common ancilla space. More precisely, in this approach we consider n copies of a quantum channel Phi: Phi_i: L(env_i (x) in_i) -> L(env_i+1 (x) out_i) for i = 0, ..., n - 1, an input state: rho0 in L(in_0 (x) anc_0 (x) ... (x) anc_n-2), controls called also teeth: T_i: L(out_i-1 (x) anc_i-1) -> L(in_i (x) anc'_i-1) for i = 1, ..., n - 1, and measurement at the end: P: L(anc'_0 (x) ... (x) anc'_n-2 (x) out_n-1) -> R. The input of the first environment space (env_0) is initialized with `env_inp_state` and the last environment spcace (env_n) is traced out. Parameters ---------- channel : ParamChannel Channel to compute quantum Fisher information for. In case this argument is a comb created from single channels the control operation (comb's tooth) will be put between every **single** channel. number_of_channels : int 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`. ancilla_dim : int Dimension of the ancilla space connecting controls (teeth). mps_bond_dim : int Dimension of the bond space of the input state which is a matrix product state. measure_bond_dim : int Dimension of the bond space of the measurement (SLD matrix) which is a matrix product operator. unital_teeth : bool, optional If True than all proper teeth (T_i with i>0) are constrained to be unital, that is they preserve identity matrix T_i(Id) = Id, by default False. initial_teeth : list[np.ndarray] | None, optional Initial values of teeth: [T_1, ...]. For T_i it should be its Choi matrix with spaces in the order: (inp_i, anc'_i-1, out_i-1, anc_i-1), by default None. artificial_noise_after : bool, optional Whether auxiliary noise is added after the channel: - True : auxiliary noise is added after the channel, - False : auxiliary noise is added before the channel, - None : auxiliary noise is not used. By default True. fixed_teeth : list[tuple[int, np.ndarray]] | None, optional Teeths to be fixed during optimization. Element (i, Ti) in the list means that tooth number i will be fixed to Ti. Note that this value will replace the value given in initial_teeth argument, by default None. 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. Returns ------- qfi : float Quantum Fisher information. qfis : list[floats] A list of quantum Fisher informations computed in each iteration. The last value in the list is the final solution. input_state : list[np.array] The optimal matrix product state (MPS). The i-th tensor on the list - R_i is the input of the i-th channel except the last which is the part going to ancilla. Its indices are in the order: bond space connecting to the previous tensor (if it exists), input state of the channel/ancilla, bond space connecting to the next tensor (if it exists). Ts : list[np.ndarray] List of optimal teeth [T_1, T_2, ...] in the form of their Choi matrices. For the i-th element - T_i spaces are in order (inp_i, anc'_i-1, out_i-1, anc_i-1). sld : list[np.array] Matrix product operator (MPO) of the optimal symmetric logarithmic derivative (SLD). For i < n-1 i-th tensor on the list is a sld element on anc'_i then the last tensor on the list is a sld element on space out_n-1. Indices/spaces of the i-th tnesor are in the order: bond space connecting to the previous tensor (if it exists), out_n-1 or anc'_i-1, bond space connecting to the next tensor (if it exists). status : bool True if the algorithm converged, False otherwise. """ if channel.env_inp_dim != channel.env_out_dim: if number_of_channels == 1: channel = channel.trace_env(env_inp_state) # To avoid error for later env_inp_tensor declaration: env_inp_state = None else: raise EnvDimsError(channel.env_inp_dim, channel.env_out_dim) env_dim = channel.env_inp_dim if unital_teeth and channel.input_dims != channel.output_dims: raise UnitalDimsError(channel.input_dims, channel.output_dims) # ch_comb for number "channel combs" i.e. combs of channels to # differentiate from comb of controls. n_ch_comb = number_of_channels ch_per_ch_comb = len(channel.input_spaces) n = n_ch_comb * ch_per_ch_comb sd = SpaceDict() inp = [('IN', i) for i in range(n)] out = [('OUT', i) for i in range(n)] for space, dim in zip(inp, cycle(channel.input_dims)): sd[space] = dim for space, dim in zip(out, cycle(channel.output_dims)): sd[space] = dim anc_pre = sd.arrange_spaces(n - 1, ancilla_dim, ('ANC', 'PRE')) anc_post = sd.arrange_spaces(n - 1, ancilla_dim, ('ANC', 'POST')) env = sd.arrange_spaces(n_ch_comb + 1, env_dim, 'ENV') RHO0 = 'RHO0' mps, mps_names, mps_bonds = mps_var_tnet( [inp[0]] + anc_pre, RHO0, sd, mps_bond_dim ) fixed_teeth_dict = {} if fixed_teeth: for i, arr in fixed_teeth: fixed_teeth_dict[i] = arr TOOTH = 'TOOTH' teeth = [] teeth_names = [] for i in range(n - 1): tooth_name = f'{TOOTH}, {i}' teeth_names.append(tooth_name) tooth_inp = [out[i], anc_pre[i]] tooth_out = [inp[i + 1], anc_post[i]] if i in fixed_teeth_dict: tooth = ConstTensor( tooth_out + tooth_inp, sdict=sd, name=tooth_name, output_spaces=tooth_out, choi=fixed_teeth_dict[i] ) else: tooth = VarTensor( tooth_out + tooth_inp, sdict=sd, name=tooth_name, output_spaces=tooth_out, is_unital=unital_teeth ) teeth.append(tooth) ENV_INP_STATE = 'ENVIRONMENT INPUT STATE' if env_inp_state is None: env_inp_state = np.identity(env_dim) / env_dim env_inp_tensor = ConstTensor( [env[0]], sdict=sd, output_spaces=[env[0]], name=ENV_INP_STATE, choi=env_inp_state ) ENV_TRACE = 'ENVIRONMENT TRACE' env_trace = choi_identity([env[-1]], sdict=sd, name=ENV_TRACE) CHANNEL = 'CHANNEL' ch_names = [] chans = [] for i in range(n_ch_comb): x = i * ch_per_ch_comb y = (i + 1) * ch_per_ch_comb channel_name = f'{CHANNEL}, {i}' chan = channel.tensor( inp[x:y], out[x:y], env[i], env[i+1], sd, name=channel_name ) ch_names.append(channel_name) chans.append(chan) channels = TensorNetwork(chans, sd, CHANNEL) MEASUREMENT = 'MEASUREMENT' meas_mpo, meas_names, meas_bonds = mpo_measure_var_tnet( anc_post + [out[-1]], MEASUREMENT, sd, measure_bond_dim ) tn = TensorNetwork( [mps, *teeth, env_inp_tensor, channels, env_trace, meas_mpo], sd ) init_teeth = [] if initial_teeth: for i, t_arr in enumerate(initial_teeth): _c = ConstTensor( [inp[i + 1], anc_post[i], out[i], anc_pre[i]], sdict=sd, name=teeth_names[i], choi=t_arr, output_spaces=[inp[i + 1], anc_post[i]] ) init_teeth.append(_c) # TODO: Add initial input state and initial sld. init_tn = TensorNetwork(init_teeth, sd) if init_teeth else None if artificial_noise_after is None: noise_spaces = [] elif artificial_noise_after: noise_spaces = [[out[i]] for i in range(n)] else: noise_spaces = [[inp[i]] for i in range(n)] contraction_order = [] for i in range(n): contraction_order.append(mps_names[i]) if i == 0: contraction_order.append(ENV_INP_STATE) if i % ch_per_ch_comb == 0: j = int(i / ch_per_ch_comb) contraction_order.append(ch_names[j]) if i < n - 1: contraction_order.append(teeth_names[i]) contraction_order.append(meas_names[i]) contraction_order.append(ENV_TRACE) qfi, qfiss, new_tn, status = iss_opt( tn, init_tn=init_tn, art_noise_spaces=noise_spaces, contraction_order=contraction_order, **kwargs ) mps_arrs = [] sld_arrs = [] for i in range(n): mps_el = new_tn.tensors[mps_names[i]] sld_el = new_tn.tensors[meas_names[i]] mps_spaces = [] sld_spaces = [] if i == 0: mps_spaces.append(inp[0]) else: mps_spaces += [mps_bonds[i - 1], anc_pre[i - 1]] sld_spaces.append(meas_bonds[i - 1]) if i < n - 1: mps_spaces.append(mps_bonds[i]) sld_spaces += [anc_post[i], meas_bonds[i]] else: sld_spaces.append(out[-1]) mps_arrs.append(mps_el.to_mps(mps_spaces)[-1]) sld_el.reorder(sld_spaces) sld_arrs.append(sld_el.array) teeth_arrs = [] for i in range(n - 1): t = new_tn.tensors[teeth_names[i]] t_arr = t.choi([inp[i + 1], anc_post[i], out[i], anc_pre[i]]) teeth_arrs.append(t_arr) return qfi, qfiss[0], mps_arrs, teeth_arrs, sld_arrs, status