def test_circuit_init(self): """Test initialization from a circuit.""" circuit, target = self.simple_circuit_no_measure() op = Choi(circuit) target = Choi(target) self.assertEqual(op, target)
def test_multiply_except(self): """Test multiply method raises exceptions.""" chan = Choi(self.choiI) self.assertRaises(QiskitError, chan.multiply, 's') self.assertRaises(QiskitError, chan.multiply, chan)
def test_negate(self): """Test negate method""" chan = Choi(self.choiI) targ = Choi(-1 * self.choiI) self.assertEqual(-chan, targ)
def test_add_except(self): """Test add method raises exceptions.""" chan1 = Choi(self.choiI) chan2 = Choi(np.eye(8)) self.assertRaises(QiskitError, chan1.add, chan2) self.assertRaises(QiskitError, chan1.add, 5)
def test_subtract_except(self): """Test subtract method raises exceptions.""" chan1 = Choi(self.choiI) chan2 = Choi(np.eye(8)) self.assertRaises(QiskitError, chan1.subtract, chan2) self.assertRaises(QiskitError, chan1.subtract, 5)
def test_compose_front(self): """Test front compose method.""" # UnitaryChannel evolution chan1 = Choi(self.choiX) chan2 = Choi(self.choiY) chan = chan1.compose(chan2, front=True) targ = Choi(self.choiZ) self.assertEqual(chan, targ) # 50% depolarizing channel chan1 = Choi(self.depol_choi(0.5)) chan = chan1.compose(chan1, front=True) targ = Choi(self.depol_choi(0.75)) self.assertEqual(chan, targ) # Measure and rotation Zp, Zm = np.diag([1, 0]), np.diag([0, 1]) Xp, Xm = np.array([[1, 1], [1, 1]]) / 2, np.array([[1, -1], [-1, 1] ]) / 2 chan1 = Choi(np.kron(Zp, Xp) + np.kron(Zm, Xm)) chan2 = Choi(self.choiX) # X-gate second does nothing chan = chan2.compose(chan1, front=True) targ = Choi(np.kron(Zp, Xp) + np.kron(Zm, Xm)) self.assertEqual(chan, targ) # X-gate first swaps Z states chan = chan1.compose(chan2, front=True) targ = Choi(np.kron(Zm, Xp) + np.kron(Zp, Xm)) self.assertEqual(chan, targ) # Compose different dimensions chan1 = Choi(np.eye(8) / 4, input_dims=2, output_dims=4) chan2 = Choi(np.eye(8) / 2, input_dims=4, output_dims=2) chan = chan1.compose(chan2, front=True) self.assertEqual(chan.dim, (4, 4)) chan = chan2.compose(chan1, front=True) self.assertEqual(chan.dim, (2, 2))
def test_power_except(self): """Test power method raises exceptions.""" chan = Choi(self.depol_choi(1)) # Non-integer power raises error self.assertRaises(QiskitError, chan.power, 0.5)
def test_is_cptp(self): """Test is_cptp method.""" self.assertTrue(Choi(self.depol_choi(0.25)).is_cptp()) # Non-CPTP should return false self.assertFalse( Choi(1.25 * self.choiI - 0.25 * self.depol_choi(1)).is_cptp())
def test_compose_except(self): """Test compose different dimension exception""" self.assertRaises(QiskitError, Choi(np.eye(4)).compose, Choi(np.eye(8))) self.assertRaises(QiskitError, Choi(np.eye(4)).compose, 2)
def thermal_relaxation_error(t1, t2, time, excited_state_population=0): """ Single-qubit thermal relaxation quantum error channel. Args: t1 (double): the T_1 relaxation time constant. t2 (double): the T_2 relaxation time constant. time (double): the gate time for relaxation error. excited_state_population (double): the population of |1> state at equilibrium (default: 0). Returns: QuantumError: a quantum error object for a noise model. Raises: NoiseError: If noise parameters are invalid. Additional information: For parameters to be valid T_2 <= 2 * T_1. If T_2 <= T_1 the error can be expressed as a mixed reset and unitary error channel. If T_1 < T_2 <= 2 * T_1 the error must be expressed as a general non-unitary Kraus error channel. """ if excited_state_population < 0: raise NoiseError("Invalid excited state population " "({} < 0).".format(excited_state_population)) if excited_state_population > 1: raise NoiseError("Invalid excited state population " "({} > 1).".format(excited_state_population)) if time < 0: raise NoiseError("Invalid gate_time ({} < 0)".format(time)) if t1 <= 0: raise NoiseError("Invalid T_1 relaxation time parameter: T_1 <= 0.") if t2 <= 0: raise NoiseError("Invalid T_2 relaxation time parameter: T_2 <= 0.") if t2 - 2 * t1 > 0: raise NoiseError( "Invalid T_2 relaxation time parameter: T_2 greater than 2 * T_1.") # T1 relaxation rate if t1 == np.inf: rate1 = 0 p_reset = 0 else: rate1 = 1 / t1 p_reset = 1 - np.exp(-time * rate1) # T2 dephasing rate if t2 == np.inf: rate2 = 0 exp_t2 = 1 else: rate2 = 1 / t2 exp_t2 = np.exp(-time * rate2) # Qubit state equilibrium probabilities p0 = 1 - excited_state_population p1 = excited_state_population if t2 > t1: # If T_2 > T_1 we must express this as a Kraus channel # We start with the Choi-matrix representation: chan = Choi( np.array([[1 - p1 * p_reset, 0, 0, exp_t2], [0, p1 * p_reset, 0, 0], [0, 0, p0 * p_reset, 0], [exp_t2, 0, 0, 1 - p0 * p_reset]])) return QuantumError(Kraus(chan)) else: # If T_2 < T_1 we can express this channel as a probabilistic # mixture of reset operations and unitary errors: circuits = [[{ 'name': 'id', 'qubits': [0] }], [{ 'name': 'z', 'qubits': [0] }], [{ 'name': 'reset', 'qubits': [0] }], [{ 'name': 'reset', 'qubits': [0] }, { 'name': 'x', 'qubits': [0] }]] # Probability p_reset0 = p_reset * p0 p_reset1 = p_reset * p1 p_z = (1 - p_reset) * (1 - np.exp(-time * (rate2 - rate1))) / 2 p_identity = 1 - p_z - p_reset0 - p_reset1 probabilities = [p_identity, p_z, p_reset0, p_reset1] return QuantumError(zip(circuits, probabilities))
def test_equal(self): """Test __eq__ method""" mat = self.rand_matrix(4, 4) self.assertEqual(Choi(mat), Choi(mat))
def test_evolve(self): """Test evolve method.""" input_psi = [0, 1] input_rho = [[0, 0], [0, 1]] # Identity channel chan = Choi(self.choiI) target_rho = np.array([[0, 0], [0, 1]]) self.assertAllClose(chan._evolve(input_psi), target_rho) self.assertAllClose(chan._evolve(np.array(input_psi)), target_rho) self.assertAllClose(chan._evolve(input_rho), target_rho) self.assertAllClose(chan._evolve(np.array(input_rho)), target_rho) # Hadamard channel chan = Choi(self.choiH) target_rho = np.array([[1, -1], [-1, 1]]) / 2 self.assertAllClose(chan._evolve(input_psi), target_rho) self.assertAllClose(chan._evolve(np.array(input_psi)), target_rho) self.assertAllClose(chan._evolve(input_rho), target_rho) self.assertAllClose(chan._evolve(np.array(input_rho)), target_rho) # Completely depolarizing channel chan = Choi(self.depol_choi(1)) target_rho = np.eye(2) / 2 self.assertAllClose(chan._evolve(input_psi), target_rho) self.assertAllClose(chan._evolve(np.array(input_psi)), target_rho) self.assertAllClose(chan._evolve(input_rho), target_rho) self.assertAllClose(chan._evolve(np.array(input_rho)), target_rho)
def diamond_norm(choi, **kwargs): r"""Return the diamond norm of the input quantum channel object. This function computes the completely-bounded trace-norm (often referred to as the diamond-norm) of the input quantum channel object using the semidefinite-program from reference [1]. Args: choi(Choi or QuantumChannel): a quantum channel object or Choi-matrix array. kwargs: optional arguments to pass to CVXPY solver. Returns: float: The completely-bounded trace norm :math:`\|\mathcal{E}\|_{\diamond}`. Raises: QiskitError: if CVXPY package cannot be found. Additional Information: The input to this function is typically *not* a CPTP quantum channel, but rather the *difference* between two quantum channels :math:`\|\Delta\mathcal{E}\|_\diamond` where :math:`\Delta\mathcal{E} = \mathcal{E}_1 - \mathcal{E}_2`. Reference: J. Watrous. "Simpler semidefinite programs for completely bounded norms", arXiv:1207.5726 [quant-ph] (2012). .. note:: This function requires the optional CVXPY package to be installed. Any additional kwargs will be passed to the ``cvxpy.solve`` function. See the CVXPY documentation for information on available SDP solvers. """ _cvxpy_check('`diamond_norm`') # Check CVXPY is installed if not isinstance(choi, Choi): choi = Choi(choi) def cvx_bmat(mat_r, mat_i): """Block matrix for embedding complex matrix in reals""" return cvxpy.bmat([[mat_r, -mat_i], [mat_i, mat_r]]) # Dimension of input and output spaces dim_in = choi._input_dim dim_out = choi._output_dim size = dim_in * dim_out # SDP Variables to convert to real valued problem r0_r = cvxpy.Variable((dim_in, dim_in)) r0_i = cvxpy.Variable((dim_in, dim_in)) r0 = cvx_bmat(r0_r, r0_i) r1_r = cvxpy.Variable((dim_in, dim_in)) r1_i = cvxpy.Variable((dim_in, dim_in)) r1 = cvx_bmat(r1_r, r1_i) x_r = cvxpy.Variable((size, size)) x_i = cvxpy.Variable((size, size)) iden = sparse.eye(dim_out) # Watrous uses row-vec convention for his Choi matrix while we use # col-vec. It turns out row-vec convention is requried for CVXPY too # since the cvxpy.kron function must have a constant as its first argument. c_r = cvxpy.bmat([[cvxpy.kron(iden, r0_r), x_r], [x_r.T, cvxpy.kron(iden, r1_r)]]) c_i = cvxpy.bmat([[cvxpy.kron(iden, r0_i), x_i], [-x_i.T, cvxpy.kron(iden, r1_i)]]) c = cvx_bmat(c_r, c_i) # Convert col-vec convention Choi-matrix to row-vec convention and # then take Transpose: Choi_C -> Choi_R.T choi_rt = np.transpose( np.reshape(choi.data, (dim_in, dim_out, dim_in, dim_out)), (3, 2, 1, 0)).reshape(choi.data.shape) choi_rt_r = choi_rt.real choi_rt_i = choi_rt.imag # Constraints cons = [ r0 >> 0, r0_r == r0_r.T, r0_i == -r0_i.T, cvxpy.trace(r0_r) == 1, r1 >> 0, r1_r == r1_r.T, r1_i == -r1_i.T, cvxpy.trace(r1_r) == 1, c >> 0 ] # Objective function obj = cvxpy.Maximize( cvxpy.trace(choi_rt_r @ x_r) + cvxpy.trace(choi_rt_i @ x_i)) prob = cvxpy.Problem(obj, cons) sol = prob.solve(**kwargs) return sol
def test_sub_qargs(self): """Test subtract method with qargs.""" mat = self.rand_matrix(8 ** 2, 8 ** 2) mat0 = self.rand_matrix(4, 4) mat1 = self.rand_matrix(4, 4) op = Choi(mat) op0 = Choi(mat0) op1 = Choi(mat1) op01 = op1.tensor(op0) eye = Choi(self.choiI) with self.subTest(msg="qargs=[0]"): value = op - op0([0]) target = op - eye.tensor(eye).tensor(op0) self.assertEqual(value, target) with self.subTest(msg="qargs=[1]"): value = op - op0([1]) target = op - eye.tensor(op0).tensor(eye) self.assertEqual(value, target) with self.subTest(msg="qargs=[2]"): value = op - op0([2]) target = op - op0.tensor(eye).tensor(eye) self.assertEqual(value, target) with self.subTest(msg="qargs=[0, 1]"): value = op - op01([0, 1]) target = op - eye.tensor(op1).tensor(op0) self.assertEqual(value, target) with self.subTest(msg="qargs=[1, 0]"): value = op - op01([1, 0]) target = op - eye.tensor(op0).tensor(op1) self.assertEqual(value, target) with self.subTest(msg="qargs=[0, 2]"): value = op - op01([0, 2]) target = op - op1.tensor(eye).tensor(op0) self.assertEqual(value, target) with self.subTest(msg="qargs=[2, 0]"): value = op - op01([2, 0]) target = op - op0.tensor(eye).tensor(op1) self.assertEqual(value, target)
def process_fidelity(channel, target=None, require_cp=True, require_tp=True): r"""Return the process fidelity of a noisy quantum channel. The process fidelity :math:`F_{\text{pro}}(\mathcal{E}, \mathcal{F})` between two quantum channels :math:`\mathcal{E}, \mathcal{F}` is given by .. math:: F_{\text{pro}}(\mathcal{E}, \mathcal{F}) = F(\rho_{\mathcal{E}}, \rho_{\mathcal{F}}) where :math:`F` is the :func:`~qiskit.quantum_info.state_fidelity`, :math:`\rho_{\mathcal{E}} = \Lambda_{\mathcal{E}} / d` is the normalized :class:`~qiskit.quantum_info.Choi` matrix for the channel :math:`\mathcal{E}`, and :math:`d` is the input dimension of :math:`\mathcal{E}`. When the target channel is unitary this is equivalent to .. math:: F_{\text{pro}}(\mathcal{E}, U) = \frac{Tr[S_U^\dagger S_{\mathcal{E}}]}{d^2} where :math:`S_{\mathcal{E}}, S_{U}` are the :class:`~qiskit.quantum_info.SuperOp` matrices for the *input* quantum channel :math:`\mathcal{E}` and *target* unitary :math:`U` respectively, and :math:`d` is the input dimension of the channel. Args: channel (Operator or QuantumChannel): input quantum channel. target (Operator or QuantumChannel or None): target quantum channel. If `None` target is the identity operator [Default: None]. require_cp (bool): check if input and target channels are completely-positive and if non-CP log warning containing negative eigenvalues of Choi-matrix [Default: True]. require_tp (bool): check if input and target channels are trace-preserving and if non-TP log warning containing negative eigenvalues of partial Choi-matrix :math:`Tr_{\mbox{out}}[\mathcal{E}] - I` [Default: True]. Returns: float: The process fidelity :math:`F_{\text{pro}}`. Raises: QiskitError: if the channel and target do not have the same dimensions. """ # Format inputs channel = _input_formatter(channel, SuperOp, "process_fidelity", "channel") target = _input_formatter(target, Operator, "process_fidelity", "target") if target: # Validate dimensions if channel.dim != target.dim: raise QiskitError( "Input quantum channel and target unitary must have the same " "dimensions ({} != {}).".format(channel.dim, target.dim)) # Validate complete-positivity and trace-preserving for label, chan in [("Input", channel), ("Target", target)]: if chan is not None and require_cp: cp_cond = _cp_condition(chan) neg = cp_cond < -1 * chan.atol if np.any(neg): logger.warning( "%s channel is not CP. Choi-matrix has negative eigenvalues: %s", label, cp_cond[neg], ) if chan is not None and require_tp: tp_cond = _tp_condition(chan) non_zero = np.logical_not( np.isclose(tp_cond, 0, atol=chan.atol, rtol=chan.rtol)) if np.any(non_zero): logger.warning( "%s channel is not TP. Tr_2[Choi] - I has non-zero eigenvalues: %s", label, tp_cond[non_zero], ) if isinstance(target, Operator): # Compute fidelity with unitary target by applying the inverse # to channel and computing fidelity with the identity channel = channel.compose(target.adjoint()) target = None input_dim, _ = channel.dim if target is None: # Compute process fidelity with identity channel if isinstance(channel, Operator): # |Tr[U]/dim| ** 2 fid = np.abs(np.trace(channel.data) / input_dim)**2 else: # Tr[S] / (dim ** 2) fid = np.trace(SuperOp(channel).data) / (input_dim**2) return float(np.real(fid)) # For comparing two non-unitary channels we compute the state fidelity of # the normalized Choi-matrices. This is equivalent to the previous definition # when the target is a unitary channel. state1 = DensityMatrix(Choi(channel).data / input_dim) state2 = DensityMatrix(Choi(target).data / input_dim) return state_fidelity(state1, state2, validate=False)
def process_fidelity(channel, target=None, require_cp=True, require_tp=False): r"""Return the process fidelity of a noisy quantum channel. The process fidelity :math:`F_{\text{pro}}(\mathcal{E}, \methcal{F})` between two quantum channels :math:`\mathcal{E}, \mathcal{F}` is given by .. math: F_{\text{pro}}(\mathcal{E}, \mathcal{F}) = F(\rho_{\mathcal{E}}, \rho_{\mathcal{F}}) where :math:`F` is the :func:`~qiskit.quantum_info.state_fidelity`, :math:`\rho_{\mathcal{E}} = \Lambda_{\mathcal{E}} / d` is the normalized :class:`~qiskit.quantum_info.Choi` matrix for the channel :math:`\mathcal{E}`, and :math:`d` is the input dimension of :math:`\mathcal{E}`. When the target channel is unitary this is equivalent to .. math:: F_{\text{pro}}(\mathcal{E}, U) = \frac{Tr[S_U^\dagger S_{\mathcal{E}}]}{d^2} where :math:`S_{\mathcal{E}}, S_{U}` are the :class:`~qiskit.quantum_info.SuperOp` matrices for the *input* quantum channel :math:`\mathcal{E}` and *target* unitary :math:`U` respectively, and :math:`d` is the input dimension of the channel. Args: channel (Operator or QuantumChannel): input quantum channel. target (Operator or QuantumChannel or None): target quantum channel. If `None` target is the identity operator [Default: None]. require_cp (bool): require channel to be completely-positive [Default: True]. require_tp (bool): require channel to be trace-preserving [Default: False]. Returns: float: The process fidelity :math:`F_{\text{pro}}`. Raises: QiskitError: if the channel and target do not have the same dimensions. QiskitError: if the channel and target are not completely-positive (with ``require_cp=True``) or not trace-preserving (with ``require_tp=True``). """ # Format inputs channel = _input_formatter(channel, SuperOp, 'process_fidelity', 'channel') target = _input_formatter(target, Operator, 'process_fidelity', 'target') if target: # Validate dimensions if channel.dim != target.dim: raise QiskitError( 'Input quantum channel and target unitary must have the same ' 'dimensions ({} != {}).'.format(channel.dim, target.dim)) # Validate complete-positivity and trace-preserving for label, chan in [('Input', channel), ('Target', target)]: if isinstance(chan, Operator) and (require_cp or require_tp): is_unitary = chan.is_unitary() # Validate as unitary if require_cp and not is_unitary: raise QiskitError( '{} channel is not completely-positive'.format(label)) if require_tp and not is_unitary: raise QiskitError( '{} channel is not trace-preserving'.format(label)) elif chan is not None: # Validate as QuantumChannel if require_cp and not chan.is_cp(): raise QiskitError( '{} channel is not completely-positive'.format(label)) if require_tp and not chan.is_tp(): raise QiskitError( '{} channel is not trace-preserving'.format(label)) if isinstance(target, Operator): # Compute fidelity with unitary target by applying the inverse # to channel and computing fidelity with the identity channel = channel @ target.adjoint() target = None input_dim, _ = channel.dim if target is None: # Compute process fidelity with identity channel if isinstance(channel, Operator): # |Tr[U]/dim| ** 2 fid = np.abs(np.trace(channel.data) / input_dim)**2 else: # Tr[S] / (dim ** 2) fid = np.trace(SuperOp(channel).data) / (input_dim**2) return float(np.real(fid)) # For comparing two non-unitary channels we compute the state fidelity of # the normalized Choi-matrices. This is equivalent to the previous definition # when the target is a unitary channel. state1 = DensityMatrix(Choi(channel).data / input_dim) state2 = DensityMatrix(Choi(target).data / input_dim) return state_fidelity(state1, state2, validate=False)