1import numbers
2from collections.abc import Iterable
3from copy import deepcopy
4import numpy as np
5
6from qutip.qobjevo import QobjEvo
7from qutip.qip.operations import expand_operator
8from qutip.qobj import Qobj
9from qutip.operators import sigmaz, destroy, num
10from qutip.qip.pulse import Pulse
11
12
13__all__ = ["Noise", "DecoherenceNoise", "RelaxationNoise",
14           "ControlAmpNoise", "RandomNoise", "process_noise"]
15
16
17def process_noise(pulses, noise_list, dims, t1=None, t2=None,
18                  device_noise=False):
19    """
20    Apply noise to the input list of pulses. It does not modify the input
21    pulse, but return a new one containing the noise.
22
23    Parameters
24    ----------
25    pulses: list of :class:`.Pulse`
26        The input pulses, on which the noise object will be applied.
27    noise_list: list of :class:`.Noise`
28        A list of noise objects.
29    dims: int or list
30        Dimension of the system.
31        If int, we assume it is the number of qubits in the system.
32        If list, it is the dimension of the component systems.
33    t1: list or float, optional
34        Characterize the decoherence of amplitude damping for
35        each qubit. A list of size `N` or a float for all qubits.
36    t2: list of float, optional
37        Characterize the decoherence of dephasing for
38        each qubit. A list of size `N` or a float for all qubits.
39    device_noise: bool
40        If pulse independent noise such as relaxation are included.
41        Default is False.
42
43    Returns
44    -------
45    noisy_pulses: list of :class:`qutip.qip.Pulse`
46        The noisy pulses, including the system noise.
47    """
48    noisy_pulses = deepcopy(pulses)
49    systematic_noise = Pulse(None, None, label="systematic_noise")
50
51    if (t1 is not None) or (t2 is not None):
52        noise_list.append(RelaxationNoise(t1, t2))
53
54    for noise in noise_list:
55        if isinstance(noise, (DecoherenceNoise, RelaxationNoise)) \
56                and not device_noise:
57            pass
58        else:
59            noisy_pulses, systematic_noise = noise._apply_noise(
60                dims=dims, pulses=noisy_pulses,
61                systematic_noise=systematic_noise)
62
63    if device_noise:
64        return noisy_pulses + [systematic_noise]
65    else:
66        return noisy_pulses
67
68
69class Noise(object):
70    """
71    The base class representing noise in a processor.
72    The noise object can be added to :class:`.Processor` and
73    contributes to evolution.
74    """
75    def __init__(self):
76        pass
77
78    def get_noisy_dynamics(self, dims, pulses, systematic_noise):
79        """
80        Return a pulses list added with noise and
81        the pulse independent noise in a dummy Pulse object.
82
83        Parameters
84        ----------
85        dims: list, optional
86            The dimension of the components system, the default value is
87            [2,2...,2] for qubits system.
88
89        pulses: list of :class:`.Pulse`
90            The input pulses, on which the noise object is to be applied.
91
92        systematic_noise: :class:`.Pulse`
93            The dummy pulse with no ideal control element.
94
95        Returns
96        -------
97        noisy_pulses: list of :class:`.Pulse`
98            Noisy pulses.
99
100        systematic_noise: :class:`.Pulse`
101            The dummy pulse representing pulse independent noise.
102        """
103        raise NotImplementedError(
104            "Subclass error needs a method"
105            "`get_noisy_dynamics` to process the noise.")
106
107    def _apply_noise(self, pulses=None, systematic_noise=None, dims=None):
108        """
109        For backward compatibility, in case the method has no return value
110        or only return the pulse.
111        """
112        result = self.get_noisy_dynamics(
113            pulses=pulses, systematic_noise=systematic_noise, dims=dims)
114        if result is None:  # in-place change
115            pass
116        elif isinstance(result, tuple) and len(result) == 2:
117            pulses, systematic_noise = result
118        # only pulse
119        elif isinstance(result, list) and len(result) == len(pulses):
120            pulses = result
121        else:
122            raise TypeError(
123                "Returned value of get_noisy_dynamics not understood.")
124        return pulses, systematic_noise
125
126
127class DecoherenceNoise(Noise):
128    """
129    The decoherence noise in a processor. It generates lindblad noise
130    according to the given collapse operator `c_ops`.
131
132    Parameters
133    ----------
134    c_ops: :class:`qutip.Qobj` or list
135        The Hamiltonian representing the dynamics of the noise.
136    targets: int or list, optional
137        The indices of qubits that are acted on. Default is on all
138        qubits
139    coeff: list, optional
140        A list of the coefficients for the control Hamiltonians.
141    tlist: array_like, optional
142        A NumPy array specifies the time of each coefficient.
143    all_qubits: bool, optional
144        If `c_ops` contains only single qubits collapse operator,
145        ``all_qubits=True`` will allow it to be applied to all qubits.
146
147    Attributes
148    ----------
149    c_ops: :class:`qutip.Qobj` or list
150        The Hamiltonian representing the dynamics of the noise.
151    targets: int or list
152        The indices of qubits that are acted on.
153    coeff: list
154        A list of the coefficients for the control Hamiltonians.
155    tlist: array_like
156        A NumPy array specifies the time of each coefficient.
157    all_qubits: bool
158        If `c_ops` contains only single qubits collapse operator,
159        ``all_qubits=True`` will allow it to be applied to all qubits.
160    """
161    def __init__(self, c_ops, targets=None, coeff=None, tlist=None,
162                 all_qubits=False):
163        if isinstance(c_ops, Qobj):
164            self.c_ops = [c_ops]
165        else:
166            self.c_ops = c_ops
167        self.coeff = coeff
168        self.tlist = tlist
169        self.targets = targets
170        if all_qubits:
171            if not all([c_op.dims == [[2], [2]] for c_op in self.c_ops]):
172                raise ValueError(
173                    "The operator is not a single qubit operator, "
174                    "thus cannot be applied to all qubits")
175        self.all_qubits = all_qubits
176
177    def get_noisy_dynamics(
178            self, dims=None, pulses=None, systematic_noise=None):
179        if systematic_noise is None:
180            systematic_noise = Pulse(None, None, label="system")
181        N = len(dims)
182        # time-independent
183        if (self.coeff is None) and (self.tlist is None):
184            self.coeff = True
185
186        for c_op in self.c_ops:
187            if self.all_qubits:
188                for targets in range(N):
189                    systematic_noise.add_lindblad_noise(
190                        c_op, targets, self.tlist, self.coeff)
191            else:
192                systematic_noise.add_lindblad_noise(
193                    c_op, self.targets, self.tlist, self.coeff)
194        return pulses, systematic_noise
195
196
197class RelaxationNoise(Noise):
198    """
199    The decoherence on each qubit characterized by two time scales t1 and t2.
200
201    Parameters
202    ----------
203    t1: float or list, optional
204        Characterize the decoherence of amplitude damping for
205        each qubit.
206    t2: float or list, optional
207        Characterize the decoherence of dephasing for
208        each qubit.
209    targets: int or list, optional
210        The indices of qubits that are acted on. Default is on all
211        qubits
212
213    Attributes
214    ----------
215    t1: float or list
216        Characterize the decoherence of amplitude damping for
217        each qubit.
218    t2: float or list
219        Characterize the decoherence of dephasing for
220        each qubit.
221    targets: int or list
222        The indices of qubits that are acted on.
223    """
224    def __init__(self, t1=None, t2=None, targets=None):
225        self.t1 = t1
226        self.t2 = t2
227        self.targets = targets
228
229    def _T_to_list(self, T, N):
230        """
231        Check if the relaxation time is valid
232
233        Parameters
234        ----------
235        T: list of float
236            The relaxation time
237        N: int
238            The number of component systems.
239
240        Returns
241        -------
242        T: list
243            The relaxation time in Python list form
244        """
245        if (isinstance(T, numbers.Real) and T > 0) or T is None:
246            return [T] * N
247        elif isinstance(T, Iterable) and len(T) == N:
248            return T
249        else:
250            raise ValueError(
251                "Invalid relaxation time T={},"
252                "either the length is not equal to the number of qubits, "
253                "or T is not a positive number.".format(T))
254
255    def get_noisy_dynamics(
256            self, dims=None, pulses=None, systematic_noise=None):
257        if systematic_noise is None:
258            systematic_noise = Pulse(None, None, label="system")
259        N = len(dims)
260
261        self.t1 = self._T_to_list(self.t1, N)
262        self.t2 = self._T_to_list(self.t2, N)
263        if len(self.t1) != N or len(self.t2) != N:
264            raise ValueError(
265                "Length of t1 or t2 does not match N, "
266                "len(t1)={}, len(t2)={}".format(
267                    len(self.t1), len(self.t2)))
268
269        if self.targets is None:
270            targets = range(N)
271        else:
272            targets = self.targets
273        for qu_ind in targets:
274            t1 = self.t1[qu_ind]
275            t2 = self.t2[qu_ind]
276            if t1 is not None:
277                op = 1/np.sqrt(t1) * destroy(dims[qu_ind])
278                systematic_noise.add_lindblad_noise(op, qu_ind, coeff=True)
279            if t2 is not None:
280                # Keep the total dephasing ~ exp(-t/t2)
281                if t1 is not None:
282                    if 2*t1 < t2:
283                        raise ValueError(
284                            "t1={}, t2={} does not fulfill "
285                            "2*t1>t2".format(t1, t2))
286                    T2_eff = 1./(1./t2-1./2./t1)
287                else:
288                    T2_eff = t2
289                op = 1/np.sqrt(2*T2_eff) * 2 * num(dims[qu_ind])
290                systematic_noise.add_lindblad_noise(op, qu_ind, coeff=True)
291        return pulses, systematic_noise
292
293
294class ControlAmpNoise(Noise):
295    """
296    The noise in the amplitude of the control pulse.
297
298    Parameters
299    ----------
300    coeff: list
301        A list of the coefficients for the control Hamiltonians.
302        For available choices, see :class:`qutip.QobjEvo`.
303    tlist: array_like, optional
304        A NumPy array specifies the time of each coefficient.
305    indices: list of int, optional
306        The indices of target pulse in the list of pulses.
307    Attributes
308    ----------
309    coeff: list
310        A list of the coefficients for the control Hamiltonians.
311        For available choices, see :class:`qutip.QobjEvo`.
312    tlist: array_like
313        A NumPy array specifies the time of each coefficient.
314    indices: list of int
315        The indices of target pulse in the list of pulses.
316
317    """
318    def __init__(self, coeff, tlist=None, indices=None):
319        self.coeff = coeff
320        self.tlist = tlist
321        self.indices = indices
322
323    def get_noisy_dynamics(
324            self, dims=None, pulses=None, systematic_noise=None):
325        if pulses is None:
326            pulses = []
327        if self.indices is None:
328            indices = range(len(pulses))
329        else:
330            indices = self.indices
331        for i in indices:
332            pulse = pulses[i]
333            if isinstance(self.coeff, (int, float)):
334                coeff = pulse.coeff * self.coeff
335            else:
336                coeff = self.coeff
337            if self.tlist is None:
338                tlist = pulse.tlist
339            else:
340                tlist = self.tlist
341            pulses[i].add_coherent_noise(
342                pulse.qobj, pulse.targets, tlist, coeff)
343        return pulses, systematic_noise
344
345
346class RandomNoise(ControlAmpNoise):
347    """
348    Random noise in the amplitude of the control pulse. The arguments for
349    the random generator need to be given as key word arguments.
350
351    Parameters
352    ----------
353    dt: float, optional
354        The time interval between two random amplitude. The coefficients
355        of the noise are the same within this time range.
356    rand_gen: numpy.random, optional
357        A random generator in numpy.random, it has to take a ``size``
358        parameter as the size of random numbers in the output array.
359    indices: list of int, optional
360        The indices of target pulse in the list of pulses.
361    **kwargs:
362        Key word arguments for the random number generator.
363
364    Attributes
365    ----------
366    dt: float, optional
367        The time interval between two random amplitude. The coefficients
368        of the noise are the same within this time range.
369    rand_gen: numpy.random, optional
370        A random generator in numpy.random, it has to take a ``size``
371        parameter.
372    indices: list of int
373        The indices of target pulse in the list of pulses.
374    **kwargs:
375        Key word arguments for the random number generator.
376
377    Examples
378    --------
379    >>> gaussnoise = RandomNoise( \
380            dt=0.1, rand_gen=np.random.normal, loc=mean, scale=std) \
381            # doctest: +SKIP
382    """
383    def __init__(
384            self, dt, rand_gen, indices=None, **kwargs):
385        super(RandomNoise, self).__init__(coeff=None, tlist=None)
386        self.rand_gen = rand_gen
387        self.kwargs = kwargs
388        if "size" in kwargs:
389            raise ValueError("size is preditermined inside the noise object.")
390        self.dt = dt
391        self.indices = indices
392
393    def get_noisy_dynamics(
394            self, dims=None, pulses=None, systematic_noise=None):
395        if pulses is None:
396            pulses = []
397        if self.indices is None:
398            indices = range(len(pulses))
399        else:
400            indices = self.indices
401        t_max = -np.inf
402        t_min = np.inf
403        for pulse in pulses:
404            t_max = max(max(pulse.tlist), t_max)
405            t_min = min(min(pulse.tlist), t_min)
406        # create new tlist and random coeff
407        num_rand = int(np.floor((t_max - t_min) / self.dt)) + 1
408        tlist = (np.arange(0, self.dt*num_rand, self.dt)[:num_rand] + t_min)
409        # [:num_rand] for round of error like 0.2*6=1.2000000000002
410
411        for i in indices:
412            pulse = pulses[i]
413            coeff = self.rand_gen(**self.kwargs, size=num_rand)
414            pulses[i].add_coherent_noise(
415                pulse.qobj, pulse.targets, tlist, coeff)
416        return pulses, systematic_noise
417