Repository: Qiskit/qiskit

Issue:
QasmQobj includes pulse job information
<!-- ⚠️ 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**:
- **Operating system**:

### What is the current behavior?
`QasmQobj` includes pulse specific fields (`meas_lo_freq', 'schedule_los`, etc):


QasmQobj(config=QasmQobjConfig(initial_layout=[5, 6], meas_level=2, meas_lo_freq=[], meas_lo_range=[], meas_return='avg', memory=False, memory_slot_size=100, memory_slots=2, n_qubits=2, parameter_binds=[], qubit_lo_freq=[], qubit_lo_range=[], schedule_los=[], shots=1024), experiments=[QasmQobjExperiment(config=QasmQobjExperimentConfig(memory_slots=2, n_qubits=2), header=QobjExperimentHeader(clbit_labels=[['c', 0], ['c', 1]], creg_sizes=[['c', 2]], memory_slots=2, n_qubits=2, name='bell', qreg_sizes=[['q', 2]], qubit_labels=[['q', 0], ['q', 1]]), instructions=[QasmQobjInstruction(name='h', qubits=[0]), QasmQobjInstruction(name='cx', qubits=[0, 1]), QasmQobjInstruction(memory=[0], name='measure', qubits=[0]), QasmQobjInstruction(memory=[1], name='measure', qubits=[1])])], header=QobjHeader(backend_name='ibmq_16_melbourne', backend_version='1.0.0'), qobj_id='68127e25-a6b5-4cb6-9c32-3ce16dea9223', schema_version='1.1.0', type='QASM')


### Steps to reproduce the problem



### What is the expected behavior?



### Suggested solutions





Discussion:
This is a result of a single parameter extraction method `compiler.assemble._parse_run_args`. This should be split into three methods. The first for parameters general to pulse qasm and pulse experiments, second for qasm only parameters and third for pulse only parameters. These should then be conditionally called based on input experiment type.
Would it be possible to split `_parse_run_args` into just two methods, one for pulse experiments and another for qasm experiments. I see this check in `assemble` already, 
```
    if all(isinstance(exp, QuantumCircuit) for exp in experiments):
        # If circuits are parameterized, bind parameters and remove from run_config
        bound_experiments, run_config = _expand_parameters(circuits=experiments,
                                                           run_config=run_config)
        return assemble_circuits(circuits=bound_experiments, qobj_id=qobj_id,
                                 qobj_header=qobj_header, run_config=run_config)

    elif all(isinstance(exp, ScheduleComponent) for exp in experiments):
        return assemble_schedules(schedules=experiments, qobj_id=qobj_id,
                                  qobj_header=qobj_header, run_config=run_config)

    else:
        raise QiskitError("bad input to assemble() function; "
                          "must be either circuits or schedules")
```

which causes me to believe `assemble` will only have 2 types of input for the experiments. `_parse_run_args` is called before this check, but maybe splitting the method into two and inputting them within the respective conditional above would solve the issue? 
Pulse requires some of the same arguments as the circuit path. So I think it should be three methods. Otherwise, I agree.
After looking through the parameters for `assemble()` again, am I wrong in categorizing them this way:

Circuits Only:
- `seed_simulator`

Both Circuits and Pulse:
- `experiments`
- `backend`
- `qobj_id`
- `qobj_header`
- `shots`
- `memory`
- `max_credits`
- `memory_slots`
- `memory_slot_size`
- `meas_return`
- `meas_map`
- `parameter_binds`
- `**run_configs`

Pulse Only:
- `qubit_lo_freq`
- `meas_lo_freq`
- `qubit_lo_range`
- `meas_lo_range`
- `schedule_los`
- `meas_level`

Are there any other parameters that I'm missing or that I categorized incorrecectly?
- `meas_map` is pulse only
- `meas_return` is pulse only
- `parameter_binds` is currently circuits only
`seed_simulator` should be for both as we will soon have a pulse simulator.
So I've split `_parse_run_args` into two functions so far, and have placed them into `assemble.py` at the following place:
```
    if all(isinstance(exp, QuantumCircuit) for exp in experiments):
        qobj_id, qobj_header, run_config = _parse_run_circuit_args(backend, qobj_id, qobj_header,
                                                                   shots, memory, max_credits,
                                                                   seed_simulator, memory_slots,
                                                                   memory_slot_size, rep_time,
                                                                   parameter_binds, **run_config)
        # If circuits are parameterized, bind parameters and remove from run_config
        bound_experiments, run_config = _expand_parameters(circuits=experiments,
                                                           run_config=run_config)
        return assemble_circuits(circuits=bound_experiments, qobj_id=qobj_id,
                                 qobj_header=qobj_header, run_config=run_config)

    elif all(isinstance(exp, ScheduleComponent) for exp in experiments):
        qobj_id, qobj_header, run_config = _parse_run_pulse_args(backend, qobj_id, qobj_header,
                                                                 shots, memory, max_credits,
                                                                 seed_simulator, qubit_lo_freq,
                                                                 meas_lo_freq, qubit_lo_range,
                                                                 meas_lo_range, schedule_los,
                                                                 meas_level, meas_return,
                                                                 meas_map, memory_slots,
                                                                 memory_slot_size, rep_time,
                                                                 **run_config)
        return assemble_schedules(schedules=experiments, qobj_id=qobj_id,
                                  qobj_header=qobj_header, run_config=run_config)

    else:
        raise QiskitError("bad input to assemble() function; "
                          "must be either circuits or schedules")
```

`_parse_run_circuit_args` includes the args required only by circuits, plus the args required by both circuits and pulse jobs. `_parse_run_pulse_args` includes the args required only by pulse, plus the args required by both circuits and pulse jobs.

I am just a little confused on what the third `_parse_run_args` function would be. Would it be the one that is currently in place that includes all of the arguments? Are users allowed to submit jobs that have both circuits and pulse experiments in them? Is there another type besides `QuantumCircuit` or `ScheduleComponent` that is accepted as `experiments` in `assemble()`?

Sorry if it seems like I keep asking the same question, I am still just a little confused as to what the third version of `_parse_run_args` would be.
By three I meant there would be a

- `_parse_common_run_args` (common to circuits and pulse)
- `_parse_circuit_args` (common to circuits)
- `_parse_pulse_args` (common to pulse)


Resolving patch:
diff --git a/qiskit/compiler/assemble.py b/qiskit/compiler/assemble.py
--- a/qiskit/compiler/assemble.py
+++ b/qiskit/compiler/assemble.py
@@ -28,9 +28,9 @@
 # TODO: parallelize over the experiments (serialize each separately, then add global header/config)
 def assemble(experiments,
              backend=None,
-             qobj_id=None, qobj_header=None,  # common run options
+             qobj_id=None, qobj_header=None,
              shots=1024, memory=False, max_credits=None, seed_simulator=None,
-             qubit_lo_freq=None, meas_lo_freq=None,  # schedule run options
+             qubit_lo_freq=None, meas_lo_freq=None,
              qubit_lo_range=None, meas_lo_range=None,
              schedule_los=None, meas_level=2, meas_return='avg', meas_map=None,
              memory_slot_size=100, rep_time=None, parameter_binds=None,
@@ -128,18 +128,15 @@ def assemble(experiments,
     Raises:
         QiskitError: if the input cannot be interpreted as either circuits or schedules
     """
-    # Get RunConfig(s) that will be inserted in Qobj to configure the run
     experiments = experiments if isinstance(experiments, list) else [experiments]
-    qobj_id, qobj_header, run_config = _parse_run_args(backend, qobj_id, qobj_header,
-                                                       shots, memory, max_credits, seed_simulator,
-                                                       qubit_lo_freq, meas_lo_freq,
-                                                       qubit_lo_range, meas_lo_range,
-                                                       schedule_los, meas_level, meas_return,
-                                                       meas_map, memory_slot_size, rep_time,
-                                                       parameter_binds, **run_config)
+    qobj_id, qobj_header, run_config_common_dict = _parse_common_args(backend, qobj_id, qobj_header,
+                                                                      shots, memory, max_credits,
+                                                                      seed_simulator, **run_config)
 
     # assemble either circuits or schedules
     if all(isinstance(exp, QuantumCircuit) for exp in experiments):
+        run_config = _parse_circuit_args(parameter_binds, **run_config_common_dict)
+
         # If circuits are parameterized, bind parameters and remove from run_config
         bound_experiments, run_config = _expand_parameters(circuits=experiments,
                                                            run_config=run_config)
@@ -147,6 +144,12 @@ def assemble(experiments,
                                  qobj_header=qobj_header, run_config=run_config)
 
     elif all(isinstance(exp, ScheduleComponent) for exp in experiments):
+        run_config = _parse_pulse_args(backend, qubit_lo_freq, meas_lo_freq,
+                                       qubit_lo_range, meas_lo_range,
+                                       schedule_los, meas_level, meas_return,
+                                       meas_map, memory_slot_size, rep_time,
+                                       **run_config_common_dict)
+
         return assemble_schedules(schedules=experiments, qobj_id=qobj_id,
                                   qobj_header=qobj_header, run_config=run_config)
 
@@ -156,13 +159,9 @@ def assemble(experiments,
 
 
 # TODO: rework to return a list of RunConfigs (one for each experiments), and a global one
-def _parse_run_args(backend, qobj_id, qobj_header,
-                    shots, memory, max_credits, seed_simulator,
-                    qubit_lo_freq, meas_lo_freq,
-                    qubit_lo_range, meas_lo_range,
-                    schedule_los, meas_level, meas_return,
-                    meas_map, memory_slot_size, rep_time,
-                    parameter_binds, **run_config):
+def _parse_common_args(backend, qobj_id, qobj_header, shots,
+                       memory, max_credits, seed_simulator,
+                       **run_config):
     """Resolve the various types of args allowed to the assemble() function through
     duck typing, overriding args, etc. Refer to the assemble() docstring for details on
     what types of inputs are allowed.
@@ -171,6 +170,47 @@ def _parse_run_args(backend, qobj_id, qobj_header,
     them in case a run option is passed through multiple args (explicitly setting an arg
     has more priority than the arg set by backend)
 
+    Returns:
+        RunConfig: a run config, which is a standardized object that configures the qobj
+            and determines the runtime environment.
+    """
+    # grab relevant info from backend if it exists
+    backend_config = None
+    if backend:
+        backend_config = backend.configuration()
+
+    # an identifier for the Qobj
+    qobj_id = qobj_id or str(uuid.uuid4())
+
+    # The header that goes at the top of the Qobj (and later Result)
+    # we process it as dict, then write entries that are not None to a QobjHeader object
+    qobj_header = qobj_header or {}
+    if isinstance(qobj_header, QobjHeader):
+        qobj_header = qobj_header.to_dict()
+    backend_name = getattr(backend_config, 'backend_name', None)
+    backend_version = getattr(backend_config, 'backend_version', None)
+    qobj_header = {**dict(backend_name=backend_name, backend_version=backend_version),
+                   **qobj_header}
+    qobj_header = QobjHeader(**{k: v for k, v in qobj_header.items() if v is not None})
+
+    # create run configuration and populate
+    run_config_dict = dict(shots=shots,
+                           memory=memory,
+                           max_credits=max_credits,
+                           seed_simulator=seed_simulator,
+                           **run_config)
+
+    return qobj_id, qobj_header, run_config_dict
+
+
+def _parse_pulse_args(backend, qubit_lo_freq, meas_lo_freq, qubit_lo_range,
+                      meas_lo_range, schedule_los, meas_level,
+                      meas_return, meas_map,
+                      memory_slot_size, rep_time,
+                      **run_config):
+    """Build a pulse RunConfig replacing unset arguments with defaults derived from the `backend`.
+    See `assemble` for more information on the required arguments.
+
     Returns:
         RunConfig: a run config, which is a standardized object that configures the qobj
             and determines the runtime environment.
@@ -194,13 +234,6 @@ def _parse_run_args(backend, qobj_id, qobj_header,
 
     meas_map = meas_map or getattr(backend_config, 'meas_map', None)
 
-    rep_time = rep_time or getattr(backend_config, 'rep_times', None)
-    if isinstance(rep_time, list):
-        rep_time = rep_time[0]
-
-    parameter_binds = parameter_binds or []
-
-    # add default empty lo config
     schedule_los = schedule_los or []
     if isinstance(schedule_los, (LoConfig, dict)):
         schedule_los = [schedule_los]
@@ -214,26 +247,13 @@ def _parse_run_args(backend, qobj_id, qobj_header,
 
     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', [])
-    # an identifier for the Qobj
-    qobj_id = qobj_id or str(uuid.uuid4())
 
-    # The header that goes at the top of the Qobj (and later Result)
-    # we process it as dict, then write entries that are not None to a QobjHeader object
-    qobj_header = qobj_header or {}
-    if isinstance(qobj_header, QobjHeader):
-        qobj_header = qobj_header.to_dict()
-    backend_name = getattr(backend_config, 'backend_name', None)
-    backend_version = getattr(backend_config, 'backend_version', None)
-    qobj_header = {**dict(backend_name=backend_name, backend_version=backend_version),
-                   **qobj_header}
-    qobj_header = QobjHeader(**{k: v for k, v in qobj_header.items() if v is not None})
+    rep_time = rep_time or getattr(backend_config, 'rep_times', None)
+    if isinstance(rep_time, list):
+        rep_time = rep_time[0]
 
     # create run configuration and populate
-    run_config_dict = dict(shots=shots,
-                           memory=memory,
-                           max_credits=max_credits,
-                           seed_simulator=seed_simulator,
-                           qubit_lo_freq=qubit_lo_freq,
+    run_config_dict = dict(qubit_lo_freq=qubit_lo_freq,
                            meas_lo_freq=meas_lo_freq,
                            qubit_lo_range=qubit_lo_range,
                            meas_lo_range=meas_lo_range,
@@ -243,11 +263,27 @@ def _parse_run_args(backend, qobj_id, qobj_header,
                            meas_map=meas_map,
                            memory_slot_size=memory_slot_size,
                            rep_time=rep_time,
-                           parameter_binds=parameter_binds,
                            **run_config)
     run_config = RunConfig(**{k: v for k, v in run_config_dict.items() if v is not None})
 
-    return qobj_id, qobj_header, run_config
+    return run_config
+
+
+def _parse_circuit_args(parameter_binds, **run_config):
+    """Build a circuit RunConfig replacing unset arguments with defaults derived from the `backend`.
+    See `assemble` for more information on the required arguments.
+
+    Returns:
+        RunConfig: a run config, which is a standardized object that configures the qobj
+            and determines the runtime environment.
+    """
+    parameter_binds = parameter_binds or []
+
+    # create run configuration and populate
+    run_config_dict = dict(parameter_binds=parameter_binds, **run_config)
+    run_config = RunConfig(**{k: v for k, v in run_config_dict.items() if v is not None})
+
+    return run_config
 
 
 def _expand_parameters(circuits, run_config):


Test patch: