def test_get_block(self):
        """Test `get` block."""
        sched = ScheduleBlock()
        sched.append(Play(Waveform(np.ones(5)), DriveChannel(0)), inplace=True)
        inst_map = InstructionScheduleMap()
        inst_map.add("x", 0, sched)

        self.assertEqual(sched, inst_map.get("x", (0, )))
    def test_add_block(self):
        """Test add block, and that errors are raised when expected."""
        sched = ScheduleBlock()
        sched.append(Play(Waveform(np.ones(5)), DriveChannel(0)), inplace=True)
        inst_map = InstructionScheduleMap()

        inst_map.add("u1", 1, sched)
        inst_map.add("u1", 0, sched)

        self.assertIn("u1", inst_map.instructions)
        self.assertEqual(inst_map.qubits_with_instruction("u1"), [0, 1])
        self.assertTrue("u1" in inst_map.qubit_instructions(0))
    def _assign(
        self,
        schedule: ScheduleBlock,
        qubits: Tuple[int, ...],
        assign_params: Dict[Union[str, ParameterKey], ParameterValueType],
        group: Optional[str] = "default",
        cutoff_date: datetime = None,
    ) -> ScheduleBlock:
        """Recursively assign parameters in a schedule.

        The recursive behaviour is needed to handle Call instructions as the name of
        the called instruction defines the scope of the parameter. Each time a Call
        is found _assign recurses on the channel-assigned subroutine of the Call
        instruction and the qubits that are in said subroutine. This requires a
        careful extraction of the qubits from the subroutine and in the appropriate
        order. Next, the parameters are identified and assigned. This is needed to
        handle situations where the same parameterized schedule is called but on
        different channels. For example,

        .. code-block:: python

            ch0 = Parameter("ch0")
            ch1 = Parameter("ch1")

            with pulse.build(name="xp") as xp:
                pulse.play(Gaussian(duration, amp, sigma), DriveChannel(ch0))

            with pulse.build(name="xt_xp") as xt:
                pulse.call(xp)
                pulse.call(xp, value_dict={ch0: ch1})

        Here, we define the xp :class:`ScheduleBlock` for all qubits as a Gaussian. Next, we define
        a schedule where both xp schedules are called simultaneously on different channels. We now
        explain a subtlety related to manually assigning values in the case above. In the schedule
        above, the parameters of the Gaussian pulses are coupled, e.g. the xp pulse on ch0 and ch1
        share the same instance of :class:`ParameterExpression`. Suppose now that both pulses have
        a duration and sigma of 160 and 40 samples, respectively, and that the amplitudes are 0.5
        and 0.3 for qubits 0 and 2, respectively. These values are stored in self._params. When
        retrieving a schedule without specifying assign_params, i.e.

        .. code-block:: python

            cals.get_schedule("xt_xp", (0, 2))

        we will obtain the expected schedule with amplitudes 0.5 and 0.3. When specifying the
        following :code:`assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}` we
        will obtain a schedule where the amplitudes of the xp pulse on qubit 0 is set to
        :code:`Parameter("my_new_amp")`. The amplitude of the xp pulse on qubit 2 is set to
        the value stored by the calibrations, i.e. 0.3.

        .. code-bloc:: python

            cals.get_schedule(
                "xt_xp",
                (0, 2),
                assign_params = {("amp", (0,), "xp"): Parameter("my_new_amp")}
            )

        Args:
            schedule: The schedule with assigned channel indices for which we wish to
                assign values to non-channel parameters.
            qubits: The qubits for which to get the schedule.
            assign_params: The parameters to manually assign. See get_schedules for details.
            group: The calibration group of the parameters.
            cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters
                generated after the cutoff date will be ignored. If the cutoff_date is None then
                all parameters are considered. This allows users to discard more recent values that
                may be erroneous.

        Returns:
            ret_schedule: The schedule with assigned parameters.

        Raises:
            CalibrationError:
                - If a channel has not been assigned.
                - If there is an ambiguous parameter assignment.
                - If there are inconsistencies between a called schedule and the template
                  schedule registered under the name of the called schedule.
        """
        # 1) Restrict the given qubits to those in the given schedule.
        qubit_set = set()
        for chan in schedule.channels:
            if isinstance(chan.index, ParameterExpression):
                raise (CalibrationError(
                    f"All parametric channels must be assigned before searching for "
                    f"non-channel parameters. {chan} is parametric."))
            if isinstance(chan, (DriveChannel, MeasureChannel)):
                qubit_set.add(chan.index)

            if isinstance(chan, ControlChannel):
                for qubit in self._controls_config_r[chan]:
                    qubit_set.add(qubit)

        qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set)

        # 2) Recursively assign the parameters in the called instructions.
        ret_schedule = ScheduleBlock(
            alignment_context=schedule.alignment_context,
            name=schedule.name,
            metadata=schedule.metadata,
        )

        for inst in schedule.blocks:
            if isinstance(inst, Call):
                # Check that there are no inconsistencies with the called subroutines.
                template_subroutine = self.get_template(
                    inst.subroutine.name, qubits_)
                if inst.subroutine != template_subroutine:
                    raise CalibrationError(
                        f"The subroutine {inst.subroutine.name} called by {inst.name} does not "
                        f"match the template schedule stored under {template_subroutine.name}."
                    )

                inst = inst.assigned_subroutine()

            if isinstance(inst, ScheduleBlock):
                inst = self._assign(inst, qubits_, assign_params, group,
                                    cutoff_date)

            ret_schedule.append(inst, inplace=True)

        # 3) Get the parameter keys of the remaining instructions. At this point in
        #    _assign all parameters in Call instructions that are supposed to be
        #     assigned have been assigned.
        keys = set()

        if ret_schedule.name in set(key.schedule
                                    for key in self._parameter_map):
            for param in ret_schedule.parameters:
                keys.add(ParameterKey(param.name, qubits_, ret_schedule.name))

        # 4) Build the parameter binding dictionary.
        binding_dict = {}
        assignment_table = {}
        for key, value in assign_params.items():
            key_orig = key
            if key.qubits == ():
                key = ParameterKey(key.parameter, qubits_, key.schedule)
                if key in assign_params:
                    # if (param, (1,), sched) and (param, (), sched) are both
                    # in assign_params, skip the default value instead of
                    # possibly triggering an error about conflicting
                    # parameters.
                    continue
            elif key.qubits != qubits_:
                continue
            param = self.calibration_parameter(*key)
            if param in ret_schedule.parameters:
                assign_okay = (
                    param not in binding_dict
                    or key.schedule == ret_schedule.name
                    and assignment_table[param].schedule != ret_schedule.name)
                if assign_okay:
                    binding_dict[param] = value
                    assignment_table[param] = key_orig
                elif (key.schedule == ret_schedule.name
                      or assignment_table[param].schedule != ret_schedule.name
                      ) and binding_dict[param] != value:
                    raise CalibrationError(
                        "Ambiguous assignment: assign_params keys "
                        f"{key_orig} and {assignment_table[param]} "
                        "resolve to the same parameter.")

        for key in keys:
            # Get the parameter object. Since we are dealing with a schedule the name of
            # the schedule is always defined. However, the parameter may be a default
            # parameter for all qubits, i.e. qubits may be an empty tuple.
            param = self.calibration_parameter(*key)

            if param not in binding_dict:
                binding_dict[param] = self.get_parameter_value(
                    key.parameter,
                    key.qubits,
                    key.schedule,
                    group=group,
                    cutoff_date=cutoff_date,
                )

        return ret_schedule.assign_parameters(binding_dict, inplace=False)