1# This file is part of QuTiP: Quantum Toolbox in Python.
2#
3#    Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson.
4#    All rights reserved.
5#
6#    Redistribution and use in source and binary forms, with or without
7#    modification, are permitted provided that the following conditions are
8#    met:
9#
10#    1. Redistributions of source code must retain the above copyright notice,
11#       this list of conditions and the following disclaimer.
12#
13#    2. Redistributions in binary form must reproduce the above copyright
14#       notice, this list of conditions and the following disclaimer in the
15#       documentation and/or other materials provided with the distribution.
16#
17#    3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names
18#       of its contributors may be used to endorse or promote products derived
19#       from this software without specific prior written permission.
20#
21#    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22#    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23#    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
24#    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25#    HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26#    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27#    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28#    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29#    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30#    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31#    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32###############################################################################
33import numpy as np
34from .instruction import Instruction
35from .scheduler import Scheduler
36from ..circuit import QubitCircuit, Gate
37
38
39__all__ = ['GateCompiler']
40
41
42class GateCompiler(object):
43    """
44    Base class. It compiles a :class:`.QubitCircuit` into
45    the pulse sequence for the processor. The core member function
46    `compile` calls compiling method from the sub-class and concatenate
47    the compiled pulses.
48
49    Parameters
50    ----------
51    N: int
52        The number of the component systems.
53
54    params: dict, optional
55        A Python dictionary contains the name and the value of the parameters,
56        such as laser frequency, detuning etc.
57        It will be saved in the class attributes and can be used to calculate
58        the control pulses.
59
60    pulse_dict: dict, optional
61        A map between the pulse label and its index in the pulse list.
62        If given, the compiled pulse can be identified with
63        ``(pulse_label, coeff)``, instead of ``(pulse_index, coeff)``.
64        The number of key-value pairs should match the number of pulses
65        in the processor.
66        If it is empty, an integer ``pulse_index`` needs to be used
67        in the compiling routine saved under the attributes ``gate_compiler``.
68
69    Attributes
70    ----------
71    gate_compiler: dict
72        The Python dictionary in the form of {gate_name: compiler_function}.
73        It saves the compiling routine for each gate. See sub-classes
74        for implementation.
75        Note that for continuous pulse, the first coeff should always be 0.
76
77    args: dict
78        Arguments for individual compiling routines.
79        It adds more flexibility in customizing compiler.
80    """
81    def __init__(self, N, params=None, pulse_dict=None):
82        self.gate_compiler = {}
83        self.N = N
84        self.params = params if params is not None else {}
85        self.pulse_dict = pulse_dict if pulse_dict is not None else {}
86        self.gate_compiler = {"GLOBALPHASE": self.globalphase_compiler}
87        self.args = {"params": self.params}
88        self.global_phase = 0.
89
90    def globalphase_compiler(self, gate, args):
91        """
92        Compiler for the GLOBALPHASE gate
93        """
94        pass
95
96    def compile(self, circuit, schedule_mode=None, args=None):
97        """
98        Compile the the native gates into control pulse sequence.
99        It calls each compiling method and concatenates
100        the compiled pulses.
101
102        Parameters
103        ----------
104        circuit: :class:`.QubitCircuit` or list of
105            :class:`.Gate`
106            A list of elementary gates that can be implemented in the
107            corresponding hardware.
108            The gate names have to be in `gate_compiler`.
109
110        schedule_mode: str, optional
111            ``"ASAP"`` for "as soon as possible" or
112            ``"ALAP"`` for "as late as possible" or
113            ``False`` or ``None`` for no schedule.
114            Default is None.
115
116        args: dict, optional
117            A dictionary of arguments used in a specific gate compiler
118            function.
119
120        Returns
121        -------
122        tlist: array_like
123            A NumPy array specifies the time of each coefficient
124
125        coeffs: array_like
126            A 2d NumPy array of the shape ``(len(ctrls), len(tlist))``. Each
127            row corresponds to the control pulse sequence for
128            one Hamiltonian.
129        """
130        if isinstance(circuit, QubitCircuit):
131            gates = circuit.gates
132        else:
133            gates = circuit
134        if args is not None:
135            self.args.update(args)
136        instruction_list = []
137
138        # compile gates
139        for gate in gates:
140            if gate.name not in self.gate_compiler:
141                raise ValueError("Unsupported gate %s" % gate.name)
142            instruction = self.gate_compiler[gate.name](gate, self.args)
143            if instruction is None:
144                continue  # neglecting global phase gate
145            instruction_list += instruction
146        if not instruction_list:
147            return None, None
148        if self.pulse_dict:
149            num_controls = len(self.pulse_dict)
150        else:  # if pulse_dict is not given, compute the number of pulses
151            num_controls = 0
152            for instruction in instruction_list:
153                for pulse_index, _ in instruction.pulse_info:
154                    num_controls = max(num_controls, pulse_index)
155            num_controls += 1
156
157        # schedule
158        # scheduled_start_time:
159        #   An ordered list of the start_time for each pulse,
160        #   corresponding to gates in the instruction_list.
161        # instruction_list reordered according to the scheduled result
162        instruction_list, scheduled_start_time = \
163            self._schedule(instruction_list, schedule_mode)
164
165        # An instruction can be composed from several different pulse elements.
166        # We separate them an assign them to each pulse index.
167        pulse_instructions = [[] for tmp in range(num_controls)]
168        for instruction, start_time in \
169                zip(instruction_list, scheduled_start_time):
170            for pulse_name, coeff in instruction.pulse_info:
171                if self.pulse_dict:
172                    try:
173                        pulse_ind = self.pulse_dict[pulse_name]
174                    except KeyError:
175                        raise ValueError(
176                            f"Pulse name {pulse_name} not found"
177                            " in pulse_dict.")
178                else:
179                    pulse_ind = pulse_name
180                pulse_instructions[pulse_ind].append(
181                    (start_time, instruction.tlist, coeff))
182
183        # concatenate pulses
184        compiled_tlist, compiled_coeffs = \
185            self._concatenate_pulses(
186                pulse_instructions, scheduled_start_time, num_controls)
187        return compiled_tlist, compiled_coeffs
188
189    def _schedule(self, instruction_list, schedule_mode):
190        """
191        Schedule the instructions if required and
192        reorder instruction_list accordingly
193        """
194        if schedule_mode:
195            scheduler = Scheduler(schedule_mode)
196            scheduled_start_time = scheduler.schedule(instruction_list)
197            time_ordered_pos = np.argsort(scheduled_start_time)
198            instruction_list = [instruction_list[i] for i in time_ordered_pos]
199            scheduled_start_time.sort()
200        else:  # no scheduling
201            scheduled_start_time = [0.]
202            for instruction in instruction_list[:-1]:
203                scheduled_start_time.append(
204                    instruction.duration + scheduled_start_time[-1])
205        return instruction_list, scheduled_start_time
206
207    def _concatenate_pulses(
208            self, pulse_instructions, scheduled_start_time, num_controls):
209        """
210        Concatenate compiled pulses coefficients and tlist for each pulse.
211        If there is idling time, add zeros properly to prevent wrong spline.
212        """
213        # Concatenate tlist and coeffs for each control pulses
214        compiled_tlist = [[] for tmp in range(num_controls)]
215        compiled_coeffs = [[] for tmp in range(num_controls)]
216        for pulse_ind in range(num_controls):
217            last_pulse_time = 0.
218            for start_time, tlist, coeff in pulse_instructions[pulse_ind]:
219                # compute the gate time, step size and coeffs
220                # according to different pulse mode
221                gate_tlist, coeffs, step_size, pulse_mode = \
222                    self._process_gate_pulse(start_time, tlist, coeff)
223
224                if abs(last_pulse_time) < step_size * 1.0e-6:  # if first pulse
225                    compiled_tlist[pulse_ind].append([0.])
226                    if pulse_mode == "continuous":
227                        compiled_coeffs[pulse_ind].append([0.])
228                    # for discrete pulse len(coeffs) = len(tlist) - 1
229
230                # If there is idling time between the last pulse and
231                # the current one, we need to add zeros in between.
232                if np.abs(start_time - last_pulse_time) > step_size * 1.0e-6:
233                    idling_tlist = self._process_idling_tlist(
234                        pulse_mode, start_time, last_pulse_time, step_size)
235                    compiled_tlist[pulse_ind].append(idling_tlist)
236                    compiled_coeffs[pulse_ind].append(np.zeros(len(idling_tlist)))
237
238                # Add the gate time and coeffs to the list.
239                execution_time = gate_tlist + start_time
240                last_pulse_time = execution_time[-1]
241                compiled_tlist[pulse_ind].append(execution_time)
242                compiled_coeffs[pulse_ind].append(coeffs)
243
244        for i in range(num_controls):
245            if not compiled_coeffs[i]:
246                compiled_tlist[i] = None
247                compiled_coeffs[i] = None
248            else:
249                compiled_tlist[i] = np.concatenate(compiled_tlist[i])
250                compiled_coeffs[i] = np.concatenate(compiled_coeffs[i])
251        return compiled_tlist, compiled_coeffs
252
253    def _process_gate_pulse(
254            self, start_time, tlist, coeff):
255        # compute the gate time, step size and coeffs
256        # according to different pulse mode
257        if np.isscalar(tlist):
258            pulse_mode = "discrete"
259            # a single constant rectanglar pulse, where
260            # tlist and coeff are just float numbers
261            step_size = tlist
262            coeff = np.array([coeff])
263            gate_tlist = np.array([tlist])
264        elif len(tlist) - 1 == len(coeff):
265            # discrete pulse
266            pulse_mode = "discrete"
267            step_size = tlist[1] - tlist[0]
268            coeff = np.asarray(coeff)
269            gate_tlist = np.asarray(tlist)[1:]  #  first t always 0 by def
270        elif len(tlist) == len(coeff):
271            # continuos pulse
272            pulse_mode = "continuous"
273            step_size = tlist[1] - tlist[0]
274            coeff = np.asarray(coeff)[1:]
275            gate_tlist = np.asarray(tlist)[1:]
276        else:
277            raise ValueError(
278                "The shape of the compiled pulse is not correct.")
279        return gate_tlist, coeff, step_size, pulse_mode
280
281    def _process_idling_tlist(
282            self, pulse_mode, start_time, last_pulse_time, step_size):
283        idling_tlist = []
284        if pulse_mode == "continuous":
285            # We add sufficient number of zeros at the begining
286            # and the end of the idling to prevent wrong cubic spline.
287            if start_time - last_pulse_time > 3 * step_size:
288                idling_tlist1 = np.linspace(
289                    last_pulse_time + step_size/5,
290                    last_pulse_time + step_size,
291                    5
292                )
293                idling_tlist2 = np.linspace(
294                    start_time - step_size,
295                    start_time,
296                    5
297                )
298                idling_tlist.extend([idling_tlist1, idling_tlist2])
299            else:
300                idling_tlist.append(
301                    np.arange(
302                        last_pulse_time + step_size,
303                        start_time, step_size
304                    )
305                )
306        elif pulse_mode == "discrete":
307            # idling until the start time
308            idling_tlist.append([start_time])
309        return np.concatenate(idling_tlist)
310