Repository: Qiskit/qiskit

Issue:
Add a new QuantumCircuit constructor QuantumCircuit.from_instructions
### What should we add?

Creating a QuantumCircuit and appending operations into it is a very common pattern. It would be very convenient to be able to create a QuantumCircuit by directly passing an iterator of operations, something like:
```python
@staticmethod
def from_ops(ops: Iterator[tuple[Instruction, Sequence[Qubit], Sequence[Clbit]]]) -> QuantumCircuit:
    circuit = QuantumCircuit()
    for op in ops:
        circuit.append(*ops)
    return circuit
```
Currently this is not possible as written but it would be if my proposal https://github.com/Qiskit/qiskit-terra/issues/8707 were implemented.


Discussion:
I agree that this is a useful constructor method that we could implement. We can do this without #8707 (which I'd generally oppose on the grounds of error-checking) - the control-flow builder interfaces already do something very similar when building up their internal blocks.
(Just to add: the iterable should probably be of `CircuitInstruction`, not the legacy 3-tuple, since this is a new method with no backwards-compatibility concerns, but otherwise I'm fine with it.)
>We can do this without https://github.com/Qiskit/qiskit-terra/issues/8707

@jakelishman so how should this be done? would you just do `circuit.add_bits(set(qubits) - set(circuit.qubits))` before every append?
Yeah, exactly.  It'd probably be good to do a shade more book-keeping just for efficiency and reproducibility reasons (ensure that bits get added in the order they appear in the operand sequences, make sure condition bits and cregs are respected, and track the sets of what we've already appended internally to avoid recalculation), but those are just minor implementation details.

Similarly, you probably want your constructor to accept `name`, `global_phase` and `metadata` fields like the main constructor.  If you'd like to write a PR to do it, I imagine we can get it merged into 0.22 if you've got time this or next week?
I'm not sure I'll have time, so don't let this block 0.22. Why does `Circuit.qubits` need to be ordered? For this specific purpose it would be better for it to be unordered.
It's not absolutely critical, it was just on my mind because we occasionally have issues testing the circuits produced with the control-flow builder interfaces because they have a non-deterministic ordering (admittedly limited to the arguments of the blocks) - hence the recent #8627.  I think you're probably right that it's not an issue here.

That's fine if you've not got time, no worries.  We can mark it as a "help wanted" - I think it's a good issue for someone in the community to take on.
For a community member wanting to have a go at this, the rough interface would look like
```python
class QuantumCircuit
    @classmethod
    def from_instructions(cls, instructions: Iterable[CircuitInstruction], *, name=None, metadata=None, global_phase=None):
        out = cls(name=name, metadata=metadata, global_phase=global_phase)
        # ...
```
`CircuitInstruction` always contains only `Qubit` and `Clbit` instances, so we'd need to track the bits used in a set, and append them to the circuit before appending the instruction if they're not already there.  We'd also need to take care to ensure that any bits or registers that appear in the `condition` field of an `Instruction` instance are added as well.  For control-flow instructions, the method would need to inspect the `blocks` field (which contains `QuantumCircuit` instances, and ensure any registers in there are also in the top-level circuit.
@jakelishman I meant to ask about the `Circuit.qubits` attribute. It already exists, and it is a `list` and not a `set`. Why is that? Can  we make it a `set` instead?
It's important for it to be a sequence, because it defines the order that bits are applied to it when a circuit is used as an `Instruction` definition, for example - we associate `CircuitInstruction.qubits` with the `QuantumCircuit.qubits` by equal indices in the sequence.  We do actually have the set view already, it's just a private attribute - it's `QuantumCircuit._qubit_indices`, which is a dict. (I forgot about that when I was saying we'd need to track the set separately.)
@jakelishman @kevinsung I used the proposed interface for the from_instructions function. for now it has a fixed 2 qubit size.

```
    @classmethod
    def from_instructions(cls, instructions: Iterable[CircuitInstruction], *, name=None, metadata=None, global_phase=0):
        out = cls(name=name, metadata=metadata, global_phase=global_phase)
        qc = QuantumCircuit(2,2)
        for instruction in instructions:
            qc._append(instruction)
        return qc
```

I tested this function with CircuitInstructions in a list, but these instructions are a bit long for a user. So I should make a more user friendly interface and use set.qubit function?
```
def instructions_bell_circuit():
    instructions_list = []
    instruct_1 = CircuitInstruction(operation=Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(2, 'q'), 0),), clbits=())
    instruct_2 = CircuitInstruction(operation=Instruction(name='cx', num_qubits=2, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(2, 'q'), 0), Qubit(QuantumRegister(2, 'q'), 1)), clbits=())
    instructions_list.append(instruct_1)
    instructions_list.append(instruct_2)
    return instructions_list
    
circuit = circuit.from_instructions(instructions_bell_circuit())
```


I'll take over this issue, please assign me.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -14,6 +14,7 @@
 
 """Quantum circuit object."""
 
+from __future__ import annotations
 import copy
 import itertools
 import functools
@@ -285,6 +286,45 @@ def __init__(
             raise TypeError("Only a dictionary or None is accepted for circuit metadata")
         self._metadata = metadata
 
+    @staticmethod
+    def from_instructions(
+        instructions: Iterable[
+            CircuitInstruction
+            | tuple[qiskit.circuit.Instruction]
+            | tuple[qiskit.circuit.Instruction, Iterable[Qubit]]
+            | tuple[qiskit.circuit.Instruction, Iterable[Qubit], Iterable[Clbit]]
+        ],
+        *,
+        name: Optional[str] = None,
+        global_phase: ParameterValueType = 0,
+        metadata: Optional[dict] = None,
+    ) -> "QuantumCircuit":
+        """Construct a circuit from an iterable of CircuitInstructions.
+
+        Args:
+            instructions: The instructions to add to the circuit.
+            name: The name of the circuit.
+            global_phase: The global phase of the circuit in radians.
+            metadata: Arbitrary key value metadata to associate with the circuit.
+
+        Returns:
+            The quantum circuit.
+        """
+        circuit = QuantumCircuit(name=name, global_phase=global_phase, metadata=metadata)
+        added_qubits = set()
+        added_clbits = set()
+        for instruction in instructions:
+            if not isinstance(instruction, CircuitInstruction):
+                instruction = CircuitInstruction(*instruction)
+            qubits = [qubit for qubit in instruction.qubits if qubit not in added_qubits]
+            clbits = [clbit for clbit in instruction.clbits if clbit not in added_clbits]
+            circuit.add_bits(qubits)
+            circuit.add_bits(clbits)
+            added_qubits.update(qubits)
+            added_clbits.update(clbits)
+            circuit._append(instruction)
+        return circuit
+
     @property
     def data(self) -> QuantumCircuitData:
         """Return the circuit data (instructions and context).


Test patch:
diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py
--- a/test/python/circuit/test_circuit_operations.py
+++ b/test/python/circuit/test_circuit_operations.py
@@ -13,21 +13,23 @@
 
 """Test Qiskit's QuantumCircuit class."""
 
-from ddt import ddt, data
 import numpy as np
-from qiskit import BasicAer
-from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
-from qiskit import execute
-from qiskit.circuit import Gate, Instruction, Parameter, Measure
+from ddt import data, ddt
+
+from qiskit import BasicAer, ClassicalRegister, QuantumCircuit, QuantumRegister, execute
+from qiskit.circuit import Gate, Instruction, Measure, Parameter
 from qiskit.circuit.bit import Bit
 from qiskit.circuit.classicalregister import Clbit
 from qiskit.circuit.exceptions import CircuitError
+from qiskit.circuit.controlflow import IfElseOp
+from qiskit.circuit.library import CXGate, HGate
+from qiskit.circuit.library.standard_gates import SGate
 from qiskit.circuit.quantumcircuit import BitLocations
+from qiskit.circuit.quantumcircuitdata import CircuitInstruction
 from qiskit.circuit.quantumregister import AncillaQubit, AncillaRegister, Qubit
-from qiskit.test import QiskitTestCase
-from qiskit.circuit.library.standard_gates import SGate
+from qiskit.pulse import DriveChannel, Gaussian, Play, Schedule
 from qiskit.quantum_info import Operator
-from qiskit.pulse import Schedule, Play, Gaussian, DriveChannel
+from qiskit.test import QiskitTestCase
 
 
 @ddt
@@ -1262,6 +1264,71 @@ def test_deprecated_reset_function(self):
 
         self.assertEqual(test, expected)
 
+    def test_from_instructions(self):
+        """Test from_instructions method."""
+
+        qreg = QuantumRegister(4)
+        creg = ClassicalRegister(3)
+
+        a, b, c, d = qreg
+        x, y, z = creg
+
+        circuit_1 = QuantumCircuit(2)
+        circuit_1.x(0)
+        circuit_2 = QuantumCircuit(2)
+        circuit_2.y(0)
+
+        def instructions():
+            yield CircuitInstruction(HGate(), [a], [])
+            yield CircuitInstruction(CXGate(), [a, b], [])
+            yield CircuitInstruction(Measure(), [a], [x])
+            yield CircuitInstruction(Measure(), [b], [y])
+            yield CircuitInstruction(IfElseOp((z, 1), circuit_1, circuit_2), [c, d], [z])
+
+        def instruction_tuples():
+            yield HGate(), [a], []
+            yield CXGate(), [a, b], []
+            yield CircuitInstruction(Measure(), [a], [x])
+            yield Measure(), [b], [y]
+            yield IfElseOp((z, 1), circuit_1, circuit_2), [c, d], [z]
+
+        def instruction_tuples_partial():
+            yield HGate(), [a]
+            yield CXGate(), [a, b], []
+            yield CircuitInstruction(Measure(), [a], [x])
+            yield Measure(), [b], [y]
+            yield IfElseOp((z, 1), circuit_1, circuit_2), [c, d], [z]
+
+        circuit = QuantumCircuit.from_instructions(instructions())
+        circuit_tuples = QuantumCircuit.from_instructions(instruction_tuples())
+        circuit_tuples_partial = QuantumCircuit.from_instructions(instruction_tuples_partial())
+
+        expected = QuantumCircuit([a, b, c, d], [x, y, z])
+        for instruction in instructions():
+            expected.append(*instruction)
+
+        self.assertEqual(circuit, expected)
+        self.assertEqual(circuit_tuples, expected)
+        self.assertEqual(circuit_tuples_partial, expected)
+
+    def test_from_instructions_metadata(self):
+        """Test from_instructions method passes metadata."""
+        qreg = QuantumRegister(2)
+        a, b = qreg
+
+        def instructions():
+            yield CircuitInstruction(HGate(), [a], [])
+            yield CircuitInstruction(CXGate(), [a, b], [])
+
+        circuit = QuantumCircuit.from_instructions(instructions(), name="test", global_phase=0.1)
+
+        expected = QuantumCircuit([a, b], global_phase=0.1)
+        for instruction in instructions():
+            expected.append(*instruction)
+
+        self.assertEqual(circuit, expected)
+        self.assertEqual(circuit.name, "test")
+
 
 class TestCircuitPrivateOperations(QiskitTestCase):
     """Direct tests of some of the private methods of QuantumCircuit.  These do not represent