Repository: Qiskit/qiskit

Issue:
Specifying schedule_los should give more informative error when backend not present
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: master
- **Python version**: 3.6.8
- **Operating system**: Ubuntu 18.04

### What is the current behavior?

When `assemble`ing a schedule and also specifying `schedule_los` with no backend specified (or when `qubit_lo_freq/range`, `meas_lo_freq/range` are missing), an `IndexError` is raised.

### Steps to reproduce the problem

```
import numpy as np
from qiskit.pulse.channels import (DeviceSpecification, Qubit, RegisterSlot, MemorySlot,
                                   DriveChannel, AcquireChannel, ControlChannel, MeasureChannel)
from qiskit.pulse.commands import functional_pulse
from qiskit.pulse import LoConfig
from qiskit.pulse.schedule import Schedule
from qiskit.compiler import assemble


@functional_pulse
def linear(duration, slope, intercept):
    x = np.linspace(0, duration - 1, duration)
    return slope * x + intercept


qubits = [Qubit(0, DriveChannel(0), AcquireChannel(0), MeasureChannel(0),
                control_channels=[ControlChannel(0)]), Qubit(1, DriveChannel(1), MeasureChannel(0), AcquireChannel(1))]
registers = [RegisterSlot(i) for i in range(2)]
mem_slots = [MemorySlot(i) for i in range(2)]
two_qubit_device = DeviceSpecification(qubits, registers, mem_slots)
device = two_qubit_device
lp0 = linear(duration=3, slope=0.2, intercept=0.1)
sched = Schedule()
sched = sched.append(lp0(device.q[0].drive))

assemble(
    sched,
    schedule_los=[LoConfig(channel_los={
        device.q[0].drive: 2*np.pi * 5.4,
        device.q[1].drive: 2*np.pi * 5.5
    })]
)
```

Raises `IndexError: list assignment index out of range`

### What is the expected behavior?

An error is raised.

### Suggested solutions

The error raised should be more informative than the current error.


Discussion:

Resolving patch:
diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py
--- a/qiskit/assembler/assemble_schedules.py
+++ b/qiskit/assembler/assemble_schedules.py
@@ -39,16 +39,27 @@ def assemble_schedules(schedules, qobj_id, qobj_header, run_config):
         instruction_converter = InstructionToQobjConverter
 
     qobj_config = run_config.to_dict()
-    qubit_lo_range = qobj_config.pop('qubit_lo_range')
-    meas_lo_range = qobj_config.pop('meas_lo_range')
+
+    qubit_lo_freq = qobj_config.get('qubit_lo_freq', None)
+    if qubit_lo_freq is None:
+        raise QiskitError('qubit_lo_freq must be supplied.')
+
+    meas_lo_freq = qobj_config.get('meas_lo_freq', None)
+    if meas_lo_freq is None:
+        raise QiskitError('meas_lo_freq must be supplied.')
+
+    qubit_lo_range = qobj_config.pop('qubit_lo_range', None)
+    meas_lo_range = qobj_config.pop('meas_lo_range', None)
     meas_map = qobj_config.pop('meas_map', None)
 
     max_memory_slot = 0
 
     instruction_converter = instruction_converter(PulseQobjInstruction, **qobj_config)
 
-    lo_converter = LoConfigConverter(PulseQobjExperimentConfig, qubit_lo_range=qubit_lo_range,
-                                     meas_lo_range=meas_lo_range, **qobj_config)
+    lo_converter = LoConfigConverter(PulseQobjExperimentConfig,
+                                     qubit_lo_range=qubit_lo_range,
+                                     meas_lo_range=meas_lo_range,
+                                     **qobj_config)
 
     # Pack everything into the Qobj
     qobj_schedules = []
diff --git a/qiskit/compiler/assemble.py b/qiskit/compiler/assemble.py
--- a/qiskit/compiler/assemble.py
+++ b/qiskit/compiler/assemble.py
@@ -75,16 +75,20 @@ def assemble(experiments,
             Random seed to control sampling, for when backend is a simulator
 
         qubit_lo_freq (list):
-            List of default qubit lo frequencies
+            List of default qubit lo frequencies. Will be overridden by
+            `schedule_los` if set.
 
         meas_lo_freq (list):
-            List of default meas lo frequencies
+            List of default meas lo frequencies. Will be overridden by
+            `schedule_los` if set.
 
         qubit_lo_range (list):
-            List of drive lo ranges
+            List of drive lo ranges used to validate that the supplied qubit los
+            are valid.
 
         meas_lo_range (list):
-            List of meas lo ranges
+            List of meas lo ranges used to validate that the supplied measurement los
+            are valid.
 
         schedule_los (None or list[Union[Dict[PulseChannel, float], LoConfig]] or \
                       Union[Dict[PulseChannel, float], LoConfig]):
@@ -242,11 +246,11 @@ def _parse_pulse_args(backend, qubit_lo_freq, meas_lo_freq, qubit_lo_range,
     schedule_los = [lo_config if isinstance(lo_config, LoConfig) else LoConfig(lo_config)
                     for lo_config in schedule_los]
 
-    qubit_lo_freq = qubit_lo_freq or getattr(backend_default, 'qubit_freq_est', [])
-    meas_lo_freq = meas_lo_freq or getattr(backend_default, 'meas_freq_est', [])
+    qubit_lo_freq = qubit_lo_freq or getattr(backend_default, 'qubit_freq_est', None)
+    meas_lo_freq = meas_lo_freq or getattr(backend_default, 'meas_freq_est', None)
 
-    qubit_lo_range = qubit_lo_range or getattr(backend_config, 'qubit_lo_range', [])
-    meas_lo_range = meas_lo_range or getattr(backend_config, 'meas_lo_range', [])
+    qubit_lo_range = qubit_lo_range or getattr(backend_config, 'qubit_lo_range', None)
+    meas_lo_range = meas_lo_range or getattr(backend_config, 'meas_lo_range', None)
 
     rep_time = rep_time or getattr(backend_config, 'rep_times', None)
     if isinstance(rep_time, list):
diff --git a/qiskit/qobj/converters/lo_config.py b/qiskit/qobj/converters/lo_config.py
--- a/qiskit/qobj/converters/lo_config.py
+++ b/qiskit/qobj/converters/lo_config.py
@@ -25,7 +25,7 @@ class LoConfigConverter:
     """
 
     def __init__(self, qobj_model, qubit_lo_freq, meas_lo_freq,
-                 qubit_lo_range, meas_lo_range, **run_config):
+                 qubit_lo_range=None, meas_lo_range=None, **run_config):
         """Create new converter.
 
         Args:
@@ -43,10 +43,13 @@ def __init__(self, qobj_model, qubit_lo_freq, meas_lo_freq,
 
         self.default_lo_config = LoConfig()
 
-        for i, lo_range in enumerate(qubit_lo_range):
-            self.default_lo_config.add_lo_range(DriveChannel(i), lo_range)
-        for i, lo_range in enumerate(meas_lo_range):
-            self.default_lo_config.add_lo_range(MeasureChannel(i), lo_range)
+        if qubit_lo_range:
+            for i, lo_range in enumerate(qubit_lo_range):
+                self.default_lo_config.add_lo_range(DriveChannel(i), lo_range)
+
+        if meas_lo_range:
+            for i, lo_range in enumerate(meas_lo_range):
+                self.default_lo_config.add_lo_range(MeasureChannel(i), lo_range)
 
     def __call__(self, user_lo_config):
         """Return PulseQobjExperimentConfig


Test patch:
diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py
--- a/test/python/compiler/test_assembler.py
+++ b/test/python/compiler/test_assembler.py
@@ -429,10 +429,16 @@ def test_assemble_meas_map(self):
         """Test assembling a single schedule, no lo config."""
         acquire = pulse.Acquire(5)
         schedule = acquire(self.device.acquires, mem_slots=self.device.memoryslots)
-        assemble(schedule, meas_map=[[0], [1]])
+        assemble(schedule,
+                 qubit_lo_freq=self.default_qubit_lo_freq,
+                 meas_lo_freq=self.default_meas_lo_freq,
+                 meas_map=[[0], [1]])
 
         with self.assertRaises(QiskitError):
-            assemble(schedule, meas_map=[[0, 1, 2]])
+            assemble(schedule,
+                     qubit_lo_freq=self.default_qubit_lo_freq,
+                     meas_lo_freq=self.default_meas_lo_freq,
+                     meas_map=[[0, 1, 2]])
 
     def test_assemble_memory_slots(self):
         """Test assembling a schedule and inferring number of memoryslots."""
@@ -442,7 +448,11 @@ def test_assemble_memory_slots(self):
         # single acquisition
         schedule = acquire(self.device.acquires[0], mem_slots=pulse.MemorySlot(n_memoryslots-1))
 
-        qobj = assemble(schedule, meas_map=[[0], [1]])
+        qobj = assemble(schedule,
+                        qubit_lo_freq=self.default_qubit_lo_freq,
+                        meas_lo_freq=self.default_meas_lo_freq,
+                        meas_map=[[0], [1]])
+
         self.assertEqual(qobj.config.memory_slots, n_memoryslots)
 
         # multiple acquisition
@@ -450,7 +460,11 @@ def test_assemble_memory_slots(self):
         schedule = schedule.insert(10, acquire(self.device.acquires[0],
                                                mem_slots=pulse.MemorySlot(n_memoryslots-1)))
 
-        qobj = assemble(schedule, meas_map=[[0], [1]])
+        qobj = assemble(schedule,
+                        qubit_lo_freq=self.default_qubit_lo_freq,
+                        meas_lo_freq=self.default_meas_lo_freq,
+                        meas_map=[[0], [1]])
+
         self.assertEqual(qobj.config.memory_slots, n_memoryslots)
 
     def test_pulse_name_conflicts(self):
@@ -609,10 +623,16 @@ def test_assemble_meas_map(self):
         """Test assembling a single schedule, no lo config."""
         acquire = pulse.Acquire(5)
         schedule = acquire(self.device.q, mem_slots=self.device.mem)
-        assemble(schedule, meas_map=[[0], [1]])
+        assemble(schedule,
+                 qubit_lo_freq=self.default_qubit_lo_freq,
+                 meas_lo_freq=self.default_meas_lo_freq,
+                 meas_map=[[0], [1]])
 
         with self.assertRaises(QiskitError):
-            assemble(schedule, meas_map=[[0, 1, 2]])
+            assemble(schedule,
+                     qubit_lo_freq=self.default_qubit_lo_freq,
+                     meas_lo_freq=self.default_meas_lo_freq,
+                     meas_map=[[0, 1, 2]])
 
     def test_pulse_name_conflicts(self):
         """Test that pulse name conflicts can be resolved."""
@@ -633,5 +653,117 @@ def test_pulse_name_conflicts(self):
         self.assertNotEqual(qobj.experiments[0].instructions[1].name, 'pulse0')
 
 
+class TestPulseAssemblerMissingKwargs(QiskitTestCase):
+    """Verify that errors are raised in case backend is not provided and kwargs are missing."""
+
+    def setUp(self):
+        self.schedule = pulse.Schedule(name='fake_experiment')
+        self.schedule += pulse.FrameChange(0.)(pulse.DriveChannel(0))
+
+        self.backend = FakeOpenPulse2Q()
+        self.config = self.backend.configuration()
+        self.defaults = self.backend.defaults()
+        self.qubit_lo_freq = self.defaults.qubit_freq_est
+        self.meas_lo_freq = self.defaults.meas_freq_est
+        self.qubit_lo_range = self.config.qubit_lo_range
+        self.meas_lo_range = self.config.meas_lo_range
+        self.schedule_los = {pulse.DriveChannel(0): self.qubit_lo_freq[0],
+                             pulse.DriveChannel(1): self.qubit_lo_freq[1],
+                             pulse.MeasureChannel(0): self.meas_lo_freq[0],
+                             pulse.MeasureChannel(1): self.meas_lo_freq[1]}
+        self.meas_map = self.config.meas_map
+        self.memory_slots = self.config.n_qubits
+        self.rep_time = self.config.rep_times[0]
+
+    def test_defaults(self):
+        """Test defaults work."""
+        assemble(self.schedule,
+                 qubit_lo_freq=self.qubit_lo_freq,
+                 meas_lo_freq=self.meas_lo_freq,
+                 qubit_lo_range=self.qubit_lo_range,
+                 meas_lo_range=self.meas_lo_range,
+                 schedule_los=self.schedule_los,
+                 meas_map=self.meas_map,
+                 memory_slots=self.memory_slots,
+                 rep_time=self.rep_time)
+
+    def test_missing_qubit_lo_freq(self):
+        """Test error raised if qubit_lo_freq missing."""
+
+        with self.assertRaises(QiskitError):
+            assemble(self.schedule,
+                     qubit_lo_freq=None,
+                     meas_lo_freq=self.meas_lo_freq,
+                     qubit_lo_range=self.qubit_lo_range,
+                     meas_lo_range=self.meas_lo_range,
+                     meas_map=self.meas_map,
+                     memory_slots=self.memory_slots,
+                     rep_time=self.rep_time)
+
+    def test_missing_meas_lo_freq(self):
+        """Test error raised if meas_lo_freq missing."""
+
+        with self.assertRaises(QiskitError):
+            assemble(self.schedule,
+                     qubit_lo_freq=self.qubit_lo_freq,
+                     meas_lo_freq=None,
+                     qubit_lo_range=self.qubit_lo_range,
+                     meas_lo_range=self.meas_lo_range,
+                     meas_map=self.meas_map,
+                     memory_slots=self.memory_slots,
+                     rep_time=self.rep_time)
+
+    def test_missing_memory_slots(self):
+        """Test error is not raised if memory_slots are missing."""
+        assemble(self.schedule,
+                 qubit_lo_freq=self.qubit_lo_freq,
+                 meas_lo_freq=self.meas_lo_freq,
+                 qubit_lo_range=self.qubit_lo_range,
+                 meas_lo_range=self.meas_lo_range,
+                 schedule_los=self.schedule_los,
+                 meas_map=self.meas_map,
+                 memory_slots=None,
+                 rep_time=self.rep_time)
+
+    def test_missing_rep_time(self):
+        """Test that assembly still works if rep_time is missing.
+
+        The case of no rep_time will exist for a simulator.
+        """
+        assemble(self.schedule,
+                 qubit_lo_freq=self.qubit_lo_freq,
+                 meas_lo_freq=self.meas_lo_freq,
+                 qubit_lo_range=self.qubit_lo_range,
+                 meas_lo_range=self.meas_lo_range,
+                 schedule_los=self.schedule_los,
+                 meas_map=self.meas_map,
+                 memory_slots=self.memory_slots,
+                 rep_time=None)
+
+    def test_missing_meas_map(self):
+        """Test that assembly still works if meas_map is missing."""
+        assemble(self.schedule,
+                 qubit_lo_freq=self.qubit_lo_freq,
+                 meas_lo_freq=self.meas_lo_freq,
+                 qubit_lo_range=self.qubit_lo_range,
+                 meas_lo_range=self.meas_lo_range,
+                 schedule_los=self.schedule_los,
+                 meas_map=None,
+                 memory_slots=self.memory_slots,
+                 rep_time=self.rep_time)
+
+    def test_missing_lo_ranges(self):
+        """Test that assembly still works if lo_ranges are missing."""
+        assemble(self.schedule,
+                 qubit_lo_freq=self.qubit_lo_freq,
+                 meas_lo_freq=self.meas_lo_freq,
+                 qubit_lo_range=None,
+                 meas_lo_range=None,
+                 schedule_los=self.schedule_los,
+                 meas_map=self.meas_map,
+                 memory_slots=self.memory_slots,
+                 rep_time=self.rep_time)
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)