Source code for mrmustard.lab.gates

# Copyright 2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=no-member, import-outside-toplevel

"""
This module defines gates and operations that can be applied to quantum modes to construct a quantum circuit.
"""

from typing import List, Optional, Sequence, Tuple, Union
import numpy as np

from mrmustard import settings
from mrmustard.physics import gaussian, fock
from mrmustard.utils.typing import ComplexMatrix, RealMatrix
from mrmustard import math
from mrmustard.math.parameters import update_orthogonal, update_symplectic, update_unitary
from .abstract import Channel, Unitary, State
from .utils import make_parameter

__all__ = [
    "Dgate",
    "Sgate",
    "Rgate",
    "Pgate",
    "Ggate",
    "BSgate",
    "MZgate",
    "S2gate",
    "CZgate",
    "CXgate",
    "Interferometer",
    "RealInterferometer",
    "Attenuator",
    "Amplifier",
    "AdditiveNoise",
    "PhaseNoise",
]


[docs] class Dgate(Unitary): r"""Displacement gate. If ``len(modes) > 1`` the gate is applied in parallel to all of the modes provided. If a parameter is a single float, the parallel instances of the gate share that parameter. To apply mode-specific values use a list of floats. One can optionally set bounds for each parameter, which the optimizer will respect. Args: x (float or List[float]): the list of displacements along the x axis x_bounds (float, float): bounds for the displacement along the x axis x_trainable (bool): whether x is a trainable variable y (float or List[float]): the list of displacements along the y axis y_bounds (float, float): bounds for the displacement along the y axis y_trainable bool: whether y is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "D" parallelizable = True def __init__( self, x: Union[float, List[float]] = 0.0, y: Union[float, List[float]] = 0.0, x_trainable: bool = False, y_trainable: bool = False, x_bounds: Tuple[Optional[float], Optional[float]] = (None, None), y_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[List[int]] = None, ): m = max(len(math.atleast_1d(x)), len(math.atleast_1d(y))) super().__init__( modes=modes or list(range(m)), name="Dgate", ) self._add_parameter(make_parameter(x_trainable, x, "x", x_bounds)) self._add_parameter(make_parameter(y_trainable, y, "y", y_bounds)) @property def d_vector(self): return gaussian.displacement(self.x.value, self.y.value)
[docs] def U(self, cutoffs: Optional[Sequence[int]] = None, shape: Optional[Sequence[int]] = None): r"""Returns the unitary representation of the Displacement gate using the Laguerre polynomials. If specified, ``shape`` takes precedence over ``cutoffs``. ``shape`` is in the order ``(out, in)``. Note that for a unitary transformation on N modes, ``len(cutoffs)`` is ``N`` and ``len(shape)`` is ``2N``. Arguments: cutoffs: the Hilbert space dimension cutoff for each mode. shape: the shape of the unitary matrix. Returns: Raises: ValueError: if the length of the cutoffs array is different from N and 2N """ N = self.num_modes if cutoffs is None: pass elif len(cutoffs) == N: cutoffs = tuple(cutoffs) * 2 elif len(cutoffs) == 2 * N: cutoffs = tuple(cutoffs) else: raise ValueError( "len(cutoffs) should be either equal to the number of modes or twice the number of modes (for output-input)." ) shape = shape or cutoffs if shape is None: raise ValueError x = self.x.value * math.ones(N, dtype=self.x.value.dtype) y = self.y.value * math.ones(N, dtype=self.y.value.dtype) if N > 1: # calculate displacement unitary for each mode and concatenate with outer product Ud = None for idx, out_in in enumerate(zip(shape[:N], shape[N:])): if Ud is None: Ud = fock.displacement(x[idx], y[idx], shape=out_in) else: U_next = fock.displacement(x[idx], y[idx], shape=out_in) Ud = math.outer(Ud, U_next) return math.transpose( Ud, list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) else: return fock.displacement(x[0], y[0], shape=shape)
[docs] class Sgate(Unitary): r"""Squeezing gate. If ``len(modes) > 1`` the gate is applied in parallel to all of the modes provided. If a parameter is a single float, the parallel instances of the gate share that parameter. To apply mode-specific values use a list of floats. One can optionally set bounds for each parameter, which the optimizer will respect. Args: r (float or List[float]): the list of squeezing magnitudes r_bounds (float, float): bounds for the squeezing magnitudes r_trainable (bool): whether r is a trainable variable phi (float or List[float]): the list of squeezing angles phi_bounds (float, float): bounds for the squeezing angles phi_trainable bool: whether phi is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "S" parallelizable = True def __init__( self, r: Union[float, list[float]] = 0.0, phi: Union[float, list[float]] = 0.0, r_trainable: bool = False, phi_trainable: bool = False, r_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), phi_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[list[int]] = None, ): super().__init__( modes=modes or list(range(len(math.atleast_1d(r)))), # type: ignore name="Sgate", ) self._add_parameter(make_parameter(r_trainable, r, "r", r_bounds)) self._add_parameter(make_parameter(phi_trainable, phi, "phi", phi_bounds))
[docs] def U(self, cutoffs: Optional[Sequence[int]] = None, shape: Optional[Sequence[int]] = None): r"""Returns the unitary representation of the Squeezing gate. If specified, ``shape`` takes precedence over ``cutoffs``. ``shape`` is in the order ``(out, in)``. Note that for a unitary transformation on N modes, ``len(cutoffs)`` is ``N`` and ``len(shape)`` is ``2N``. Args: cutoffs: the Hilbert space dimension cutoff for each mode. shape: the shape of the unitary matrix. Returns: array[complex]: the unitary matrix """ N = self.num_modes if cutoffs is None: pass elif len(cutoffs) == N: cutoffs = tuple(cutoffs) * 2 elif len(cutoffs) == 2 * N: cutoffs = tuple(cutoffs) else: raise ValueError( "len(cutoffs) should be either equal to the number of modes or twice the number of modes (for output-input)." ) shape = shape or cutoffs if shape is None: raise ValueError # this works both or scalar r/phi and vector r/phi: r = self.r.value * math.ones(N, dtype=self.r.value.dtype) phi = self.phi.value * math.ones(N, dtype=self.phi.value.dtype) if N > 1: # calculate squeezing unitary for each mode and concatenate with outer product Us = None for idx, single_shape in enumerate(zip(shape[:N], shape[N:])): if Us is None: Us = fock.squeezer(r[idx], phi[idx], shape=single_shape) else: U_next = fock.squeezer(r[idx], phi[idx], shape=single_shape) Us = math.outer(Us, U_next) return math.transpose( Us, list(range(0, 2 * N, 2)) + list(range(1, 2 * N, 2)), ) else: return fock.squeezer(r[0], phi[0], shape=shape)
@property def X_matrix(self): return gaussian.squeezing_symplectic(self.r.value, self.phi.value)
[docs] class Rgate(Unitary): r"""Rotation gate. If ``len(modes) > 1`` the gate is applied in parallel to all of the modes provided. If a parameter is a single float, the parallel instances of the gate share that parameter. To apply mode-specific values use a list of floats. One can optionally set bounds for each parameter, which the optimizer will respect. Args: modes (List[int]): the list of modes this gate is applied to angle (float or List[float]): the list of rotation angles angle_bounds (float, float): bounds for the rotation angles angle_trainable bool: whether angle is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "R" parallelizable = True def __init__( self, angle: Union[float, list[float]] = 0.0, angle_trainable: bool = False, angle_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[list[int]] = None, ): super().__init__( modes=modes or list(range(len(math.atleast_1d(angle)))), # type: ignore name="Rgate", ) self._add_parameter(make_parameter(angle_trainable, angle, "angle", angle_bounds)) @property def X_matrix(self): return gaussian.rotation_symplectic(self.angle.value)
[docs] def U( self, cutoffs: Optional[Sequence[int]] = None, shape: Optional[Sequence[int]] = None, diag_only=False, ): r"""Returns the unitary representation of the Rotation gate. If specified, ``shape`` takes precedence over ``cutoffs``. ``shape`` is in the order ``(out, in)``. Note that for a unitary transformation on N modes, ``len(cutoffs)`` is ``N`` and ``len(shape)`` is ``2N``. Args: cutoffs: cutoff dimension for each mode. shape: the shape of the unitary matrix diag_only: if True, only return the diagonal of the unitary matrix. Returns: array[complex]: the unitary matrix """ N = self.num_modes if diag_only: raise NotImplementedError("Rgate does not support diag_only=True yet") if cutoffs is None: pass elif len(cutoffs) == N: cutoffs = tuple(cutoffs) * 2 elif len(cutoffs) == 2 * N: cutoffs = tuple(cutoffs) else: raise ValueError( "len(cutoffs) should be either equal to the number of modes or twice the number of modes (for output-input)." ) shape = shape or cutoffs if shape is None: raise ValueError angles = self.angle.value * math.ones(self.num_modes, dtype=self.angle.value.dtype) # calculate rotation unitary for each mode and concatenate with outer product Ur = None for idx, cutoff in enumerate(shape[: self.num_modes]): theta = math.arange(cutoff) * angles[idx] if Ur is None: Ur = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) else: U_next = math.diag(math.make_complex(math.cos(theta), math.sin(theta))) Ur = math.outer(Ur, U_next) # return total unitary with indexes reordered according to MM convention return math.transpose( Ur, list(range(0, 2 * self.num_modes, 2)) + list(range(1, 2 * self.num_modes, 2)), )
[docs] class Pgate(Unitary): r"""Quadratic phase gate. If len(modes) > 1 the gate is applied in parallel to all of the modes provided. If a parameter is a single float, the parallel instances of the gate share that parameter. To apply mode-specific values use a list of floats. One can optionally set bounds for each parameter, which the optimizer will respect. Args: modes (List[int]): the list of modes this gate is applied to shearing (float or List[float]): the list of shearing parameters shearing_bounds (float, float): bounds for the shearing parameters shearing_trainable bool: whether shearing is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "P" parallelizable = True def __init__( self, shearing: Union[Optional[float], Optional[list[float]]] = 0.0, shearing_trainable: bool = False, shearing_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[list[int]] = None, ): super().__init__( modes=modes or list(range(len(math.atleast_1d(shearing)))), name="Pgate", ) self._add_parameter( make_parameter(shearing_trainable, shearing, "shearing", shearing_bounds) ) @property def X_matrix(self): return gaussian.quadratic_phase(self.shearing.value)
[docs] class CXgate(Unitary): r"""Controlled X gate. It applies to a single pair of modes. One can optionally set bounds for each parameter, which the optimizer will respect. Args: s (float): control parameter s_bounds (float, float): bounds for the control angle s_trainable (bool): whether s is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "CX" parallelizable = False def __init__( self, s: Optional[float] = 0.0, s_trainable: bool = False, s_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[List[int]] = None, ): super().__init__( modes=modes or [0, 1], name="CXgate", ) self._add_parameter(make_parameter(s_trainable, s, "s", s_bounds)) @property def X_matrix(self): return gaussian.controlled_X(self.s.value)
[docs] class CZgate(Unitary): r"""Controlled Z gate. It applies to a single pair of modes. One can optionally set bounds for each parameter, which the optimizer will respect. Args: s (float): control parameter s_bounds (float, float): bounds for the control angle s_trainable (bool): whether s is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "CZ" parallelizable = False def __init__( self, s: Optional[float] = 0.0, s_trainable: bool = False, s_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[List[int]] = None, ): super().__init__( modes=modes or [0, 1], name="CZgate", ) self._add_parameter(make_parameter(s_trainable, s, "s", s_bounds)) @property def X_matrix(self): return gaussian.controlled_Z(self.s.value)
[docs] class BSgate(Unitary): r"""Beam splitter gate. It applies to a single pair of modes. One can optionally set bounds for each parameter, which the optimizer will respect. Args: theta (float): the transmissivity angle theta_bounds (float, float): bounds for the transmissivity angle theta_trainable (bool): whether theta is a trainable variable phi (float): the phase angle phi_bounds (float, float): bounds for the phase angle phi_trainable bool: whether phi is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "BS" parallelizable = False def __init__( self, theta: float = 0.0, phi: float = 0.0, theta_trainable: bool = False, phi_trainable: bool = False, theta_bounds: Tuple[Optional[float], Optional[float]] = (None, None), phi_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[list[int]] = None, ): super().__init__( modes=modes or [0, 1], # type: ignore name="BSgate", ) self._add_parameter(make_parameter(theta_trainable, theta, "theta", theta_bounds)) self._add_parameter(make_parameter(phi_trainable, phi, "phi", phi_bounds))
[docs] def U( self, cutoffs: Optional[List[int]] = None, shape: Optional[Sequence[int]] = None, method=None, ): r"""Returns the unitary representation of the beam splitter. If specified, ``shape`` takes precedence over ``cutoffs``. ``shape`` is in the order ``(out, in)``. Note that for a unitary transformation on N modes, ``len(cutoffs)`` is ``N`` and ``len(shape)`` is ``2N``. Args: cutoffs: the list of cutoff dimensions for each mode in the order (out_0, out_1, in_0, in_1). shape: the shape of the unitary matrix method: the method used to compute the unitary matrix. Options are: * 'vanilla': uses the standard method * 'schwinger': slower, but numerically stable default is set in settings.DEFAULT_BS_METHOD (with 'vanilla' by default) Returns: array[complex]: the unitary tensor of the beamsplitter """ if cutoffs is None: pass elif len(cutoffs) == 4: shape = tuple(cutoffs) elif len(cutoffs) == 2: shape = tuple(cutoffs) + tuple(cutoffs) else: raise ValueError(f"Invalid len(cutoffs): {len(cutoffs)} (should be 2 or 4).") shape = shape or cutoffs return fock.beamsplitter( self.theta.value, self.phi.value, shape=shape, method=method or settings.DEFAULT_BS_METHOD, )
@property def X_matrix(self): return gaussian.beam_splitter_symplectic(self.theta.value, self.phi.value) def _validate_modes(self, modes): if len(modes) != 2: raise ValueError( f"Invalid number of modes: {len(modes)} (should be 2). Perhaps you are looking for Interferometer." )
[docs] class MZgate(Unitary): r"""Mach-Zehnder gate. It supports two conventions: 1. if ``internal=True``, both phases act inside the interferometer: ``phi_a`` on the upper arm, ``phi_b`` on the lower arm; 2. if ``internal = False``, both phases act on the upper arm: ``phi_a`` before the first BS, ``phi_b`` after the first BS. One can optionally set bounds for each parameter, which the optimizer will respect. Args: phi_a (float): the phase in the upper arm of the MZ interferometer phi_a_bounds (float, float): bounds for phi_a phi_a_trainable (bool): whether phi_a is a trainable variable phi_b (float): the phase in the lower arm or external of the MZ interferometer phi_b_bounds (float, float): bounds for phi_b phi_b_trainable (bool): whether phi_b is a trainable variable internal (bool): whether phases are both in the internal arms (default is False) modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "MZ" parallelizable = False def __init__( self, phi_a: float = 0.0, phi_b: float = 0.0, phi_a_trainable: bool = False, phi_b_trainable: bool = False, phi_a_bounds: Tuple[Optional[float], Optional[float]] = (None, None), phi_b_bounds: Tuple[Optional[float], Optional[float]] = (None, None), internal: bool = False, modes: Optional[List[int]] = None, ): super().__init__( modes=modes or [0, 1], name="MZgate", ) self._add_parameter(make_parameter(phi_a_trainable, phi_a, "phi_a", phi_a_bounds)) self._add_parameter(make_parameter(phi_b_trainable, phi_b, "phi_b", phi_b_bounds)) self._internal = internal @property def X_matrix(self): return gaussian.mz_symplectic(self.phi_a.value, self.phi_b.value, internal=self._internal) def _validate_modes(self, modes): if len(modes) != 2: raise ValueError( f"Invalid number of modes: {len(modes)} (should be 2). Perhaps you are looking for Interferometer?" )
[docs] class S2gate(Unitary): r"""Two-mode squeezing gate. It applies to a single pair of modes. One can optionally set bounds for each parameter, which the optimizer will respect. Args: r (float): the squeezing magnitude r_bounds (float, float): bounds for the squeezing magnitude r_trainable (bool): whether r is a trainable variable phi (float): the squeezing angle phi_bounds (float, float): bounds for the squeezing angle phi_trainable bool: whether phi is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "S2" parallelizable = False def __init__( self, r: float = 0.0, phi: float = 0.0, r_trainable: bool = False, phi_trainable: bool = False, r_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), phi_bounds: Tuple[Optional[float], Optional[float]] = (None, None), modes: Optional[List[int]] = None, ): super().__init__( modes=modes or [0, 1], name="S2gate", ) self._add_parameter(make_parameter(r_trainable, r, "r", r_bounds)) self._add_parameter(make_parameter(phi_trainable, phi, "phi", phi_bounds)) @property def X_matrix(self): return gaussian.two_mode_squeezing_symplectic(self.r.value, self.phi.value) def _validate_modes(self, modes): if len(modes) != 2: raise ValueError(f"Invalid number of modes: {len(modes)} (should be 2")
[docs] class Interferometer(Unitary): r"""N-mode interferometer. It corresponds to a Ggate with zero mean and a ``2N x 2N`` unitary symplectic matrix. Args: num_modes (int): the num_modes-mode interferometer unitary (2d array): a valid unitary matrix U. For N modes it must have shape `(N,N)` unitary_trainable (bool): whether unitary is a trainable variable modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "I" parallelizable = False def __init__( self, num_modes: int, unitary: Optional[ComplexMatrix] = None, unitary_trainable: bool = False, modes: Optional[list[int]] = None, ): if modes is not None and num_modes != len(modes): raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") if unitary is None: unitary = math.random_unitary(num_modes) super().__init__( modes=modes or list(range(num_modes)), name="Interferometer", ) self._add_parameter( make_parameter(unitary_trainable, unitary, "unitary", (None, None), update_unitary) ) @property def X_matrix(self): return math.block( [ [math.real(self.unitary.value), -math.imag(self.unitary.value)], [math.imag(self.unitary.value), math.real(self.unitary.value)], ] ) def _validate_modes(self, modes): if len(modes) != self.unitary.value.shape[-1]: raise ValueError( f"Invalid number of modes: {len(modes)} (should be {self.unitary.shape[-1]})" ) def __repr__(self): modes = self.modes unitary = repr(math.asnumpy(self.unitary.value)).replace("\n", "") return f"Interferometer(num_modes = {len(modes)}, unitary = {unitary}){modes}"
[docs] class RealInterferometer(Unitary): r"""N-mode interferometer parametrized by an NxN orthogonal matrix (or 2N x 2N block-diagonal orthogonal matrix). This interferometer does not mix q and p. Does not mix q's and p's. Args: orthogonal (2d array, optional): a real unitary (orthogonal) matrix. For N modes it must have shape `(N,N)`. If set to `None` a random real unitary (orthogonal) matrix is used. orthogonal_trainable (bool): whether orthogonal is a trainable variable """ is_gaussian = True short_name = "RI" parallelizable = False def __init__( self, num_modes: int, orthogonal: Optional[RealMatrix] = None, orthogonal_trainable: bool = False, modes: Optional[List[int]] = None, ): if modes is not None and (num_modes != len(modes)): raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") if orthogonal is None: orthogonal = math.random_orthogonal(num_modes) super().__init__( modes=modes or list(range(num_modes)), name="RealInterferometer", ) self._add_parameter( make_parameter( orthogonal_trainable, orthogonal, "orthogonal", (None, None), update_orthogonal ) ) @property def X_matrix(self): return math.block( [ [self.orthogonal.value, -math.zeros_like(self.orthogonal.value)], [math.zeros_like(self.orthogonal.value), self.orthogonal.value], ] ) def _validate_modes(self, modes): if len(modes) != self.orthogonal.value.shape[-1]: raise ValueError( f"Invalid number of modes: {len(modes)} (should be {self.orthogonal.value.shape[-1]})" ) def __repr__(self): modes = self.modes orthogonal = repr(math.asnumpy(self.orthogonal.value)).replace("\n", "") return f"RealInterferometer(num_modes = {len(modes)}, orthogonal = {orthogonal}){modes}"
[docs] class Ggate(Unitary): r"""A generic N-mode Gaussian unitary transformation with zero displacement. If a symplectic matrix is not provided, one will be picked at random with effective squeezing strength ``r`` in ``[0, 1]`` for each mode. Args: num_modes (int): the number of modes this gate is acting on. symplectic (2d array): a valid symplectic matrix in XXPP order. For N modes it must have shape ``(2N,2N)``. symplectic_trainable (bool): whether symplectic is a trainable variable. """ is_gaussian = True short_name = "G" parallelizable = False def __init__( self, num_modes: int, symplectic: Optional[RealMatrix] = None, symplectic_trainable: bool = False, modes: Optional[list[int]] = None, ): if modes is not None and (num_modes != len(modes)): raise ValueError(f"Invalid number of modes: got {len(modes)}, should be {num_modes}") if symplectic is None: symplectic = math.random_symplectic(num_modes) super().__init__( modes=modes or list(range(num_modes)), name="Ggate", ) self._add_parameter( make_parameter( symplectic_trainable, symplectic, "symplectic", (None, None), update_symplectic ) ) @property def X_matrix(self): return self.symplectic.value def _validate_modes(self, modes): if len(modes) != self.symplectic.value.shape[1] // 2: raise ValueError( f"Invalid number of modes: {len(modes)} (should be {self.symplectic.value.shape[1] // 2})" ) def __repr__(self): modes = self.modes symplectic = repr(math.asnumpy(self.symplectic.value)).replace("\n", "") return f"Ggate(num_modes = {len(modes)}, symplectic = {symplectic}){modes}"
# ~~~~~~~~~~~~~ # NON-UNITARY # ~~~~~~~~~~~~~ # pylint: disable=no-member
[docs] class Attenuator(Channel): r"""The noisy attenuator channel. It corresponds to mixing with a thermal environment and applying the pure loss channel. The pure lossy channel is recovered for nbar = 0 (i.e. mixing with vacuum). The CPT channel is given by .. math:: X = sqrt(transmissivity) * I Y = (1-transmissivity) * (2*nbar + 1) * (hbar / 2) * I If ``len(modes) > 1`` the gate is applied in parallel to all of the modes provided. If ``transmissivity`` is a single float, the parallel instances of the gate share that parameter. To apply mode-specific values use a list of floats. One can optionally set bounds for `transmissivity`, which the optimizer will respect. Args: transmissivity (float or List[float]): the list of transmissivities nbar (float): the average number of photons in the thermal state transmissivity_trainable (bool): whether transmissivity is a trainable variable nbar_trainable (bool): whether nbar is a trainable variable transmissivity_bounds (float, float): bounds for the transmissivity nbar_bounds (float, float): bounds for the average number of photons in the thermal state modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "Att" parallelizable = True def __init__( self, transmissivity: Union[Optional[float], Optional[List[float]]] = 1.0, nbar: float = 0.0, transmissivity_trainable: bool = False, nbar_trainable: bool = False, transmissivity_bounds: Tuple[Optional[float], Optional[float]] = (0.0, 1.0), nbar_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), modes: Optional[List[int]] = None, ): super().__init__( modes=modes or list(range(len(math.atleast_1d(transmissivity)))), name="Attenuator", ) self._add_parameter( make_parameter( transmissivity_trainable, transmissivity, "transmissivity", transmissivity_bounds, None, ) ) self._add_parameter(make_parameter(nbar_trainable, nbar, "nbar", nbar_bounds)) @property def X_matrix(self): return gaussian.loss_XYd(self.transmissivity.value, self.nbar.value)[0] @property def Y_matrix(self): return gaussian.loss_XYd(self.transmissivity.value, self.nbar.value)[1]
[docs] class Amplifier(Channel): r"""The noisy amplifier channel. It corresponds to mixing with a thermal environment and applying a two-mode squeezing gate. .. code:: python X = sqrt(gain) * I Y = (gain-1) * (2*nbar + 1) * (hbar / 2) * I If ``len(modes) > 1`` the gate is applied in parallel to all of the modes provided. If ``gain`` is a single float, the parallel instances of the gate share that parameter. To apply mode-specific values use a list of floats. One can optionally set bounds for ``gain``, which the optimizer will respect. Args: gain (float or List[float]): the list of gains (must be > 1) nbar (float): the average number of photons in the thermal state nbar_trainable (bool): whether nbar is a trainable variable gain_trainable (bool): whether gain is a trainable variable gain_bounds (float, float): bounds for the gain nbar_bounds (float, float): bounds for the average number of photons in the thermal state modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "Amp" parallelizable = True def __init__( self, gain: Union[Optional[float], Optional[List[float]]] = 1.0, nbar: float = 0.0, gain_trainable: bool = False, nbar_trainable: bool = False, gain_bounds: Tuple[Optional[float], Optional[float]] = (1.0, None), nbar_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), modes: Optional[list[int]] = None, ): super().__init__( modes=modes or list(range(len(math.atleast_1d(gain)))), name="Amplifier", ) self._add_parameter(make_parameter(gain_trainable, gain, "gain", gain_bounds)) self._add_parameter(make_parameter(nbar_trainable, nbar, "nbar", nbar_bounds)) @property def X_matrix(self): return gaussian.amp_XYd(self.gain.value, self.nbar.value)[0] @property def Y_matrix(self): return gaussian.amp_XYd(self.gain.value, self.nbar.value)[1]
# pylint: disable=no-member
[docs] class AdditiveNoise(Channel): r"""The additive noise channel. Equivalent to an amplifier followed by an attenuator. E.g., .. code-block:: na,nb = np.random.uniform(size=2) tr = np.random.uniform() Amplifier(1/tr, nb) >> Attenuator(tr, na) == AdditiveNoise(2*(1-tr)*(1+na+nb)) # evaluates to True or equivalent to an attenuator followed by an amplifier: .. code-block:: na,nb = np.random.uniform(size=2) amp = 1.0 + np.random.uniform() Attenuator(1/amp, nb) >> Amplifier(amp, na) == AdditiveNoise(2*(amp-1)*(1+na+nb)) Args: noise (float or List[float]): the added noise in units of hbar/2 noise_trainable (bool): whether noise is a trainable variable noise_bounds (float, float): bounds for the noise modes (optional, List[int]): the list of modes this gate is applied to """ is_gaussian = True short_name = "Add" parallelizable = True def __init__( self, noise: Union[float, list[float]] = 0.0, noise_trainable: bool = False, noise_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), modes: Optional[list[int]] = None, ): super().__init__( modes=modes or list(range(len(math.atleast_1d(noise)))), name="AddNoise", ) self._add_parameter(make_parameter(noise_trainable, noise, "noise", noise_bounds)) @property def Y_matrix(self): return gaussian.noise_Y(self.noise.value)
[docs] class PhaseNoise(Channel): r"""The phase noise channel. The phase noise channel is a non-Gaussian transformation that is equivalent to a random phase rotation. Args: phase_stdev (float or List[float]): the standard deviation of the (wrapped) normal distribution for the angle of the rotation modes (optional, list(int)): the single mode this gate is applied to (default [0]) """ def __init__( self, phase_stdev: Union[Optional[float], Optional[List[float]]] = 0.0, phase_stdev_trainable: bool = False, phase_stdev_bounds: Tuple[Optional[float], Optional[float]] = (0.0, None), modes: Optional[List[int]] = None, ): super().__init__( modes=modes or [0], name="AddNoise", ) self._add_parameter( make_parameter(phase_stdev_trainable, phase_stdev, "phase_stdev", phase_stdev_bounds) ) self._modes = modes or [0] self.is_unitary = False self.is_gaussian = False self.short_name = "P~" # need to override primal because of the unconventional way # the channel is defined in Fock representation
[docs] def primal(self, state): idx = state.modes.index(self.modes[0]) if state.is_pure: ket = state.ket() dm = fock.ket_to_dm(ket) else: dm = state.dm() # transpose dm so that the modes of interest are at the end M = state.num_modes indices = list(range(2 * M)) indices.remove(idx) indices.remove(idx + M) indices += [idx, idx + M] dm = math.transpose(dm, indices) coeff = math.cast( math.exp( -0.5 * self.phase_stdev.value**2 * math.arange(-dm.shape[-2] + 1, dm.shape[-1]) ** 2 ), dm.dtype, ) for k in range(-dm.shape[-2] + 1, dm.shape[-1]): diagonal = math.diag_part(dm, k=k) diagonal *= coeff[k + dm.shape[-2] - 1] dm = math.set_diag(dm, diagonal, k=k) # transpose dm back to the original order return State(dm=math.transpose(dm, np.argsort(indices)), modes=state.modes)