def lindblad_superop_sum_element(l_op: np.ndarray) -> np.ndarray: """ For a Lindbladian superoperator that is formed by summation over index \\alpha, this function constructs the Lindbladian superoperator for a particular value of this index from the corresponding Lindblad operator, A. This is given by: .. math:: A^* \\otimes A - 0.5 ((A^{\\dagger} A)^* \\otimes I + I \\otimes A^{\\dagger} A) Parameters ---------- l_op : np.ndarray The Lindblad operator A for the given Lindblad model. Returns ------- np.ndarray The N^2 x N^2 superoperator (where N x N is the dimension of the Lindblad operator) for the specific index at which the Lindblad operator has been constructed. """ assert l_op.shape[0] == l_op.shape[1], 'Lindblad operator must be square.' l_op_dag = l_op.T.conjugate() iden = np.eye(l_op.shape[0]) return (np.kron(l_op.conjugate(), l_op) - 0.5 * (np.kron(np.matmul(l_op_dag, l_op).conjugate(), iden) + np.kron(iden, np.matmul(l_op_dag, l_op))))
def get_single_qubit_unitary(num_qubits: int, gate_unitary: np.ndarray, target_qubit: int) -> np.ndarray: """Return single qubit unitary operator of size 2**n x 2**n for given gate and target qubits.""" if target_qubit >= num_qubits: raise IndexError("Target qubit is outside of qubits array.") if np.shape(gate_unitary) != (2, 2): raise TypeError("Gate must be 2x2 array.") if not np.allclose( np.dot(gate_unitary, gate_unitary.conjugate().transpose()), np.identity(2)): raise ValueError("Gate must be unitary.") # initialize the matrix gate = np.array([1]) for i in range(num_qubits): if i == target_qubit: # target qubit, apply unitary gate = np.kron(gate, gate_unitary) else: # not target, apply identity gate = np.kron(gate, np.identity(2)) return gate
def basis_change(matrix: np.ndarray, states: np.ndarray, liouville: bool = False) -> np.ndarray: """ Transforms a matrix expressed in Liouville space into the basis expressed by the states matrix. Parameters ---------- matrix : np.ndarray The matrix to be transformed. If in Liouville space, must set 'liouville=True' and 'matrix' must have dimensions N^2 x N^2. Otherwise, pass in N x N dimensions. states : np.ndarray The Hilbert space of states in the basis into which 'matrix' will be transformed, of dimensions N x N. 'states' must be orthogonal (i.e. that states^{dagger} states = I), and each column must correspond to an eigenstate. liouville : bool Whether or not the input matrix 'matrix' is given in Liouville space. If True, 'matrix' must be given in dimensions N^2 x N^2 and 'states' as N x N. If False (default), both 'matrix' and 'states' must be N x N. Returns ------- np.ndarray The input matrix transformed. Has dimensions N^2 x N^2 if 'matrix' passed in Liouville space, or N x N otherwise. """ # Check 'matrix' and 'states' are square assert (matrix.shape[0] == matrix.shape[1] and states.shape[0] == states.shape[1]), ('Input matrices must be square.') assert isinstance(liouville, bool), 'Must pass liouville as a bool.' # Perform transformation if liouville: assert matrix.shape[0] == states.shape[0]**2, ( 'If providing an input matrix in Liouville space it must have' ' dimensions N^2 x N^2, where the eigenstates matrix has dimensions' ' N x N.') states = np.kron(states, states.conjugate()) # Check for orthogonality # assert np.allclose(np.matmul(states, states.conjugate().T), # np.eye(matrix.shape[0])) return np.matmul(states, np.matmul(matrix, states.conjugate().T))
def make_density_matrix_numpy(wf: ndarray): if wf.ndim == 1: return outer(wf, wf.conjugate()) if wf.ndim == 2: wf_dim, num_wf = wf.shape ret = empty(shape=(num_wf, wf_dim, wf_dim), dtype=wf.dtype) for wf_idx in range(num_wf): a_wf = wf[:, wf_idx] ret[wf_idx, :, :] = outer(a_wf, a_wf.conjugate()) return ret raise NotImplementedError(wf.shape)
def is_unitary(matrix: np.ndarray) -> bool: """ Check if the passed in matrix of size (n times n) is unitary. Arguments: matrix: the matrix which is checked Return: unitary: True if the matrix is unitary """ mcon = matrix.conjugate().T return np.allclose(np.dot(mcon, matrix), np.identity(len(matrix)))
def get_measurement_outcome(state: np.ndarray) -> str: """Return random outcome of measurement in computational basis.""" if len(np.shape(state)) != 1: raise TypeError("State vector must be shape (n,).") if np.modf(np.log2(np.shape(state)[0]))[0] != 0: raise TypeError("Length of state vector must be power of 2.") if not np.allclose(np.inner(state.conjugate(), state), 1.): raise ValueError("State vector must be normalized.") # output probability distribution prob = np.real(np.multiply(state.conjugate(), state)) # dimension of the Hilbert space dim = np.shape(state)[0] # number of qubits n = int(np.modf(np.log2(dim))[1]) # tuple of all possible outcomes outcomes = tuple(range(dim)) # random package used to sample the outcome of the measurement result = random.choices(outcomes, prob)[0] return bin(result)[2:].zfill(n)
def adjoint(t: np.ndarray) -> np.ndarray: """Returns the adjoint of the tensor as if it were representing a circuit:: t = tensorfy(circ) tadj = tensorfy(circ.adjoint()) compare_tensors(adjoint(t),tadj) # This is True """ q = len(t.shape) // 2 transp = [] for i in range(q): transp.append(q + i) for i in range(q): transp.append(i) return np.transpose(t.conjugate(), transp)
def is_hermitian(matrix: np.ndarray) -> bool: """ Checks if a matrix is Hermitian. >>> import numpy as np >>> A = np.array([ ... [2, 2+1j, 4], ... [2-1j, 3, 1j], ... [4, -1j, 1]]) >>> is_hermitian(A) True >>> A = np.array([ ... [2, 2+1j, 4+1j], ... [2-1j, 3, 1j], ... [4, -1j, 1]]) >>> is_hermitian(A) False """ return np.array_equal(matrix, matrix.conjugate().T)
def inner_product(a: np.ndarray, b: np.ndarray) -> np.ndarray: """ Calculates the inner product between vectors a and b, which equals <a|b>. :param a: Vector of n-dimensional (n,), or some vectors of n-dimensional (m, n). :param b: Vector of n-dimensional (n,), or some vectors of n-dimensional (m, n). :return: Inner product between |a> and |b>. """ braket_a_b = a.conjugate() * b if braket_a_b.ndim == 1: braket_a_b = sum(braket_a_b) else: braket_a_b = np.sum(braket_a_b, axis=1) return braket_a_b
def rayleigh_quotient(A: np.ndarray, v: np.ndarray) -> Any: """ Returns the Rayleigh quotient of a Hermitian matrix A and vector v. >>> import numpy as np >>> A = np.array([ ... [1, 2, 4], ... [2, 3, -1], ... [4, -1, 1] ... ]) >>> v = np.array([ ... [1], ... [2], ... [3] ... ]) >>> rayleigh_quotient(A, v) array([[3.]]) """ v_star = v.conjugate().T v_star_dot = v_star.dot(A) assert isinstance(v_star_dot, np.ndarray) return (v_star_dot.dot(v)) / (v_star.dot(v))
def calcProjectionMatrix(A: np.ndarray) -> np.ndarray: """ Calculates the projection matrix that projects a vector (or a matrix) into the signal space spanned by the columns of `A`. Parameters ---------- A : np.ndarray A matrix whose columns form a basis for the desired subspace. Returns ------- np.ndarray The projection matrix that can be used to project a vector or a matrix into the subspace spanned by the columns of `A` See also -------- calcOrthogonalProjectionMatrix Examples -------- >>> A = np.array([[1 + 1j, 2 - 2j], [3 - 2j, 0], \ [-1 - 1j, 2 - 3j]]) >>> # Matrix that projects into the subspace spanned by the columns >>> # of A >>> Q = calcProjectionMatrix(A) >>> np.allclose(Q.round(4), np.array( \ [[ 0.5239+0.j, 0.0366+0.3296j, 0.3662+0.0732j], \ [ 0.0366-0.3296j, 0.7690+0.j, -0.0789+0.2479j], \ [ 0.3662-0.0732j, -0.0789-0.2479j, 0.7070-0.j]])) True """ # MATLAB version: A/(A'*A)*A'; A_H = A.conjugate().transpose() return (A.dot(np.linalg.inv(A_H.dot(A)))).dot(A_H)
def is_hermitian(A: np.ndarray) -> bool: return A == A.conjugate()
def state_to_density_matrix(state: np.ndarray): return np.matmul(state, state.conjugate().transpose())
def dot(a: np.ndarray, b: np.ndarray, out: np.ndarray = None) -> np.ndarray: """calculate dot product with conjugated second operand""" return calc(a, b.conjugate(), out=out) # type: ignore
def inner_product(u: np.ndarray, v: np.ndarray) -> np.ndarray: return np.dot(u, v.conjugate())
def fast_ambiguity( num_delay_bins: int, num_doppler_bins: int, reference_signal: np.ndarray, surveillance_signal: np.ndarray, ) -> np.ndarray: """Fast implementation of cross ambiguity function (CAF), using Fourier Transform of Lag Product approach. Parameters ---------- num_delay_bins : int Number of delay bins by which to shift the surveillance signal in time-domain and calculate the CAF. Must be `>= 1`. num_doppler_bins : int Number of frequency bins by which to shift the surveillance signal in frequency-domain and calculate the CAF. Must be `>= 1`. reference_signal : np.ndarray Samples from the reference channel, which contains the direct path signal. surveillance_signal : np.ndarray Samples from the surveillance channel, which contains target echos. Returns ------- np.ndarray Cross ambiguity of surveillance and reference signal as 2D array with size `num_delay_bins` x `num_doppler_bins`. Examples -------- Calculate the CAF of a real-valued rectangular pulse shifted by 50 samples. This will produce a triangular CAF with its peak at the 50th element. >>> reference = np.pad(np.ones(10), (0, 1000)) >>> surveillance = np.roll(reference, 50) >>> amb = fast_ambiguity( ... num_delay_bins=100, ... num_doppler_bins=1, ... reference_signal=reference, ... surveillance_signal=surveillance) >>> np.argmax(amb) 50 Comprehensive example in which an amplitude modulated square wave is shifted in time and frequency. >>> import matplotlib.pyplot as plt >>> duration = 1.0 >>> duty_cycle = 0.5 >>> delay = 0.3 >>> doppler = -0.2 >>> sample_rate = 4000 >>> carrier_freq = 100 >>> num_samples = int(duration * sample_rate) >>> t = np.arange(num_samples) / sample_rate >>> waveform_prototype = np.concatenate( ... [ ... np.ones(int(num_samples * duty_cycle)), ... np.zeros(int(num_samples * (1 - duty_cycle))), ... ] ... ) >>> waveform_prototype *= 1 + 0.5 * np.sin(2 * np.pi * carrier_freq * t) >>> waveform = scipy.signal.hilbert(waveform_prototype) >>> ref_waveform = waveform >>> surv_waveform = np.roll( ... ref_waveform, int(delay * waveform.shape[0]) ... ) * np.exp(-2j * doppler * np.pi * sample_rate * t) >>> _, axs = plt.subplots(2, 1, figsize=(10, 7)) >>> _ = axs[0].plot(np.real(ref_waveform)) >>> _ = axs[0].set_title("Reference Channel") >>> _ = axs[0].set_ylabel("Real"); >>> axs[0].grid(True) >>> _ = axs[1].plot(np.real(surv_waveform)); >>> _ = axs[1].set_title("Surveillance Channel"); >>> _ = axs[1].set_ylabel("Real"); >>> axs[1].grid(True) >>> amb = fast_ambiguity( ... num_samples, ... sample_rate, ... ref_waveform, ... surv_waveform ... ) >>> peak = np.unravel_index(np.argmax(amb), amb.shape) >>> _, ax = plt.subplots(figsize=(10, 5)) >>> _ = ax.set_title("Cross Ambiguity (log-scale)") >>> _ = ax.annotate( ... f"Peak ({peak[0]},{peak[1] - sample_rate // 2:.0f})", ... peak, ... xytext=(10, 10), ... xycoords="data", ... textcoords="offset pixels", ... arrowprops={"arrowstyle": "wedge"}, ... ) >>> _ = ax.set_xlabel("Delay [Samples]") >>> _ = ax.set_ylabel("Doppler [Hz]") >>> _ = ax.set_yticks(np.linspace(0, sample_rate, 8, endpoint=False)) >>> _ = ax.set_yticklabels( ... map(lambda y: f"{y - sample_rate // 2:.0f}", ax.get_yticks()) ... ) >>> _ = ax.imshow(10 * np.log10(np.abs(amb.T))) >>> assert np.allclose( ... peak, ... np.array( ... [ ... num_samples * delay, ... sample_rate * doppler + sample_rate // 2, ... ] ... ), ... atol=200, ... ) """ num_samples_per_cpi = reference_signal.shape[0] num_taps = num_samples_per_cpi // num_doppler_bins fir = scipy.signal.dlti(np.ones(num_taps), 1) amb = np.empty((num_delay_bins, num_doppler_bins), dtype=np.complex64) surv_cmplx_conj = surveillance_signal.conjugate() surv_cmplx_conj = np.pad(surv_cmplx_conj, pad_width=(0, num_delay_bins)) lag_product = np.empty((num_samples_per_cpi, ), dtype=amb.dtype) decimation = np.empty((num_doppler_bins, ), dtype=amb.dtype) for delay_bin, lag in enumerate(-np.arange(num_delay_bins)): np.multiply( reference_signal, np.roll(surv_cmplx_conj, lag)[:num_samples_per_cpi], out=lag_product, ) decimation[:] = scipy.signal.decimate(lag_product, num_taps, ftype=fir)[:num_doppler_bins] amb[delay_bin, :] = decimation amb = scipy.fft.fftshift(scipy.fft.fft(amb, axis=1), axes=1) return amb
def gpu_ambiguity( num_delay_bins: int, num_doppler_bins: int, reference_signal: np.ndarray, surveillance_signal: np.ndarray, num_samples_per_cpi: int, batch_size: int = 1, ) -> np.ndarray: """Fast implementation of cross ambiguity function (CAF) on GPU, using Fourier Transform of Lag Product approach. Requires CuPy to be installed. Parameters ---------- num_delay_bins : int Number of delay bins by which to shift the surveillance signal in time-domain and calculate the CAF. Must be `>= 1`. num_doppler_bins : int Number of frequency bins by which to shift the surveillance signal in frequency-domain and calculate the CAF. Must be `>= 1`. reference_signal : np.ndarray Samples from the reference channel, which contains the direct path signal. surveillance_signal : np.ndarray Samples from the surveillance channel, which contains target echos. num_samples_per_cpi : int Number of samples per Coherent Processing Intervall (CPI), i.e. how many samples shall be correlated. batch_size : Optional[int] Number of consecutive CPIs to be processed. Inputs `reference_signal` and `surveillance_signal` must contain at least `num_samples_per_cpi * batch_size` samples. Returns ------- np.ndarray Cross ambiguity of surveillance and reference signal as 2D array with size `num_delay_bins` x `num_doppler_bins` x `batch_size`. Examples -------- Calculate the CAF of a real-valued rectangular pulse shifted by 50 samples. This will produce a triangular CAF with its peak at the 50th element. >>> reference = np.pad(np.ones(10), (0, 1000)) >>> surveillance = np.roll(reference, 50) >>> amb = gpu_ambiguity( ... num_delay_bins=100, ... num_doppler_bins=1, ... reference_signal=reference, ... surveillance_signal=surveillance, ... num_samples_per_cpi=reference.shape[0]) >>> np.argmax(amb) 50 """ reference_signal = cp.asarray( reference_signal[:num_samples_per_cpi * batch_size].reshape( num_samples_per_cpi, batch_size)) surveillance_signal = cp.asarray( surveillance_signal[:num_samples_per_cpi * batch_size].reshape( num_samples_per_cpi, batch_size)) num_taps = num_samples_per_cpi // num_doppler_bins amb = cp.empty( (num_delay_bins, num_doppler_bins, batch_size), dtype=cp.complex64, ) surv_cmplx_conj = surveillance_signal.conjugate() surv_cmplx_conj = cp.pad(surv_cmplx_conj, pad_width=((0, num_delay_bins), (0, 0))) lag_product = cp.empty((num_samples_per_cpi, batch_size), dtype=amb.dtype) decimation = cp.empty_like(lag_product) for delay_bin, lag in enumerate(-np.arange(num_delay_bins)): cp.multiply( reference_signal, cp.roll(surv_cmplx_conj, lag, axis=0)[:num_samples_per_cpi], out=lag_product, ) decimation[:] = cusignal.decimate(lag_product, num_taps, axis=1) amb[delay_bin, :, :] = decimation[:num_doppler_bins, :] amb = cp.fft.fftshift(cp.fft.fft(amb, axis=1), axes=1) return amb.get()
def dagger(vector: np.ndarray) -> np.ndarray: """Compute the hermitian conjugate of a vector or matrix""" return vector.conjugate().transpose()
def dot(a: np.ndarray, b: np.ndarray, out: np.ndarray = None) -> np.ndarray: return calc(a, b.conjugate(), out=out) # type: ignore