Repository: Qiskit/qiskit

Issue:
qpy_serialization causes some QuantumCircuit objects to have their parameters re-ordered
### Environment

- **qiskit-terra 0.18 or 0.19**: 
- **Python 3.9**: 
- **MacOS**: 


### What is happening?

QuantumCircuits with parameters associated with ParameterVectors generally have their parameters re-ordered upon serialization/deserialization. This is because qpy_serialization interprets the elements of ParameterVectors as Parameters, rather than ParameterVectorElements, which leads to the parameters being re-ordered according to their ascii name values.

### How can we reproduce the issue?
```
import json

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.providers.ibmq.runtime.utils import RuntimeEncoder, RuntimeDecoder


qc = QuantumCircuit(11)

# Vectors of data and trainable parameters
input_params = ParameterVector("x_par", 11)
user_params = ParameterVector("θ_par", 11)

# Create an initial rotation layer of trainable parameters
for i, param in enumerate(user_params):
    qc.ry(param, qc.qubits[i])

# Create a circuit representation of the data group
for i, param in enumerate(input_params):
    qc.rz(param, qc.qubits[i])

# Serialize/deserialize circuit to demonstrate parameter re-ordering
qc_serial = json.dumps(qc, cls=RuntimeEncoder)
qc_deserial = json.loads(qc_serial, cls=RuntimeDecoder)

# Print the parameter ordering discrepancies
for i, unserialized_param in enumerate(qc.parameters):
    if qc_deserial.parameters[i].name != unserialized_param.name:
        print(f"{qc_deserial.parameters[i].name} != {unserialized_param.name}")
```
### What should happen?

I believe qpy serialization should maintain the underlying parameter ordering of the input.

### Any suggestions?

_No response_


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py
--- a/qiskit/circuit/qpy_serialization.py
+++ b/qiskit/circuit/qpy_serialization.py
@@ -160,6 +160,90 @@
 which is immediately followed by ``pauli_op_size`` bytes which are .npy format [#f2]_
 data which represents the :class:`~qiskit.quantum_info.SparsePauliOp`.
 
+Version 3 of the QPY format also defines a struct format to represent a
+:class:`~qiskit.circuit.ParameterVectorElement` as a distinct subclass from
+a :class:`~qiskit.circuit.Parameter`. This adds a new parameter type char ``'v'``
+to represent a :class:`~qiskit.circuit.ParameterVectorElement` which is now
+supported as a type string value for an INSTRUCTION_PARAM. The payload for these
+parameters are defined below as :ref:`param_vector`.
+
+.. _param_vector:
+
+
+PARAMETER_VECTOR_ELEMENT
+------------------------
+
+A PARAMETER_VECTOR_ELEMENT represents a :class:`~qiskit.circuit.ParameterVectorElement`
+object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER_VECTOR_ELEMENT are
+defined as:
+
+.. code-block:: c
+
+    struct {
+        uint16_t vector_name_size;
+        uint64_t vector_size;
+        char uuid[16];
+        uint64_t index;
+    }
+
+which is immediately followed by ``vector_name_size`` utf8 bytes representing
+the parameter's vector name.
+
+.. _param_expr_v3:
+
+
+PARAMETER_EXPR
+--------------
+
+Additionally, since QPY format version v3 distinguishes between a
+:class:`~qiskit.circuit.Parameter` and :class:`~qiskit.circuit.ParameterVectorElement`
+the payload for a :class:`~qiskit.circuit.ParameterExpression` needs to be updated
+to distinguish between the types. The following is the modified payload format
+which is mostly identical to the format in Version 1 and :ref:`version_2` but just
+modifies the ``map_elements`` struct to include a symbol type field.
+
+A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression`
+object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR
+are defined as:
+
+.. code-block:: c
+
+    struct {
+        uint64_t map_elements;
+        uint64_t expr_size;
+    }
+
+Immediately following the header is ``expr_size`` bytes of utf8 data containing
+the expression string, which is the sympy srepr of the expression for the
+parameter expression. Following that is a symbol map which contains
+``map_elements`` elements with the format
+
+.. code-block:: c
+
+    struct {
+        char symbol_type;
+        char type;
+        uint64_t size;
+    }
+
+The ``symbol_type`` key determines the payload type of the symbol representation
+for the element. If it's ``p`` it represents a :class:`~qiskit.circuit.Parameter`
+and if it's ``v`` it represents a :class:`~qiskit.circuit.ParameterVectorElement`.
+The map element struct is immediately followed by the symbol map key payload, if
+``symbol_type`` is ``p`` then it is followed immediately by a :ref:`param_struct`
+object (both the struct and utf8 name bytes) and if ``symbol_type`` is ``v``
+then the struct is imediately followed by :ref:`param_vector` (both the struct
+and utf8 name bytes). That is followed by ``size`` bytes for the
+data of the symbol. The data format is dependent on the value of ``type``. If
+``type`` is ``p`` then it represents a :class:`~qiskit.circuit.Parameter` and
+size will be 0, the value will just be the same as the key. Similarly if the
+``type`` is ``v`` then it represents a :class:`~qiskit.circuit.ParameterVectorElement`
+and size will be 0 as the value will just be the same as the key. If
+``type`` is ``f`` then it represents a double precision float. If ``type`` is
+``c`` it represents a double precision complex, which is represented by the
+:ref:`complex`. Finally, if type is ``i`` it represents an integer which is an
+``int64_t``.
+
 .. _version_2:
 
 Version 2
@@ -375,11 +459,15 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
 a double, ``'s'`` if it's a string (encoded as utf8), ``'c'`` is a complex and
 the data is represented by the struct format in the :ref:`param_expr` section.
 ``'p'`` defines a :class:`~qiskit.circuit.Parameter` object  which is
-represented by a PARAM struct (see below), ``e`` defines a
+represented by a :ref:`param_struct` struct, ``e`` defines a
 :class:`~qiskit.circuit.ParameterExpression` object (that's not a
-:class:`~qiskit.circuit.Parameter`) which is represented by a PARAM_EXPR struct
-(see below), and ``'n'`` represents an object from numpy (either an ``ndarray``
-or a numpy type) which means the data is .npy format [#f2]_ data.
+:class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`param_expr`
+struct (on QPY format :ref:`version_3` the format is tweak slightly see:
+:ref:`param_expr_v3`), ``'n'`` represents an object from numpy (either an
+``ndarray`` or a numpy type) which means the data is .npy format [#f2]_ data,
+and in QPY :ref:`version_3` ``'v'`` represents a
+:class:`~qiskit.circuit.ParameterVectorElement` which is represented by a
+:ref:`param_vector` struct.
 
 .. _param_struct:
 
@@ -435,7 +523,17 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
 ``type`` is ``p`` then it represents a :class:`~qiskit.circuit.Parameter` and
 size will be 0, the value will just be the same as the key. If
 ``type`` is ``f`` then it represents a double precision float. If ``type`` is
-``c`` it represents a double precision complex, which is represented by:
+``c`` it represents a double precision complex, which is represented by :ref:`complex`.
+Finally, if type is ``i`` it represents an integer which is an ``int64_t``.
+
+.. _complex:
+
+COMPLEX
+-------
+
+When representing a double precision complex value in QPY the following
+struct is used:
+
 
 .. code-block:: c
 
@@ -445,7 +543,6 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
     }
 
 this matches the internal C representation of Python's complex type. [#f3]_
-Finally, if type is ``i`` it represents an integer which is an ``int64_t``.
 
 
 .. [#f1] https://tools.ietf.org/html/rfc1700
@@ -466,6 +563,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
 from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
 from qiskit.circuit.parameter import Parameter
 from qiskit.circuit.parameterexpression import ParameterExpression
+from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement
 from qiskit.circuit.gate import Gate
 from qiskit.circuit.instruction import Instruction
 from qiskit.circuit import library
@@ -583,10 +681,20 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
 PARAM_EXPR_MAP_ELEM = namedtuple("PARAMETER_EXPR_MAP_ELEM", ["type", "size"])
 PARAM_EXPR_MAP_ELEM_PACK = "!cQ"
 PARAM_EXPR_MAP_ELEM_SIZE = struct.calcsize(PARAM_EXPR_MAP_ELEM_PACK)
+# PARAMETER_EXPR_MAP_ELEM_V3
+PARAM_EXPR_MAP_ELEM_V3 = namedtuple("PARAMETER_EXPR_MAP_ELEM", ["symbol_type", "type", "size"])
+PARAM_EXPR_MAP_ELEM_PACK_V3 = "!ccQ"
+PARAM_EXPR_MAP_ELEM_SIZE_V3 = struct.calcsize(PARAM_EXPR_MAP_ELEM_PACK_V3)
 # Complex
 COMPLEX = namedtuple("COMPLEX", ["real", "imag"])
 COMPLEX_PACK = "!dd"
 COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK)
+# PARAMETER_VECTOR_ELEMENT
+PARAMETER_VECTOR_ELEMENT = namedtuple(
+    "PARAMETER_VECTOR_ELEMENT", ["vector_name_size", "vector_size", "uuid", "index"]
+)
+PARAMETER_VECTOR_ELEMENT_PACK = "!HQ16sQ"
+PARAMETER_VECTOR_ELEMENT_SIZE = struct.calcsize(PARAMETER_VECTOR_ELEMENT_PACK)
 # Pauli Evolution Gate
 PAULI_EVOLUTION_DEF = namedtuple(
     "PAULI_EVOLUTION_DEF",
@@ -600,7 +708,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
 SPARSE_PAULI_OP_LIST_ELEM_SIZE = struct.calcsize(SPARSE_PAULI_OP_LIST_ELEM_PACK)
 
 
-def _read_header_v2(file_obj):
+def _read_header_v2(file_obj, version, vectors):
     header_raw = struct.unpack(HEADER_V2_PACK, file_obj.read(HEADER_V2_SIZE))
     header_tuple = HEADER_V2._make(header_raw)
     name = file_obj.read(header_tuple[0]).decode("utf8")
@@ -611,11 +719,17 @@ def _read_header_v2(file_obj):
     elif global_phase_type_str == "i":
         global_phase = struct.unpack("!q", data)[0]
     elif global_phase_type_str == "p":
-        container = io.BytesIO(data)
-        global_phase = _read_parameter(container)
+        with io.BytesIO(data) as container:
+            global_phase = _read_parameter(container)
+    elif global_phase_type_str == "v":
+        with io.BytesIO(data) as container:
+            global_phase = _read_parameter_vec(container, vectors)
     elif global_phase_type_str == "e":
-        container = io.BytesIO(data)
-        global_phase = _read_parameter_expression(container)
+        with io.BytesIO(data) as container:
+            if version < 3:
+                global_phase = _read_parameter_expression(container)
+            else:
+                global_phase = _read_parameter_expression_v3(container, vectors)
     else:
         raise TypeError("Invalid global phase type: %s" % global_phase_type_str)
     header = {
@@ -673,6 +787,62 @@ def _read_parameter(file_obj):
     return param
 
 
+def _read_parameter_vec(file_obj, vectors):
+    param_raw = struct.unpack(
+        PARAMETER_VECTOR_ELEMENT_PACK, file_obj.read(PARAMETER_VECTOR_ELEMENT_SIZE)
+    )
+    vec_name_size = param_raw[0]
+    param_uuid = uuid.UUID(bytes=param_raw[2])
+    param_index = param_raw[3]
+    name = file_obj.read(vec_name_size).decode("utf8")
+    if name not in vectors:
+        vectors[name] = (ParameterVector(name, param_raw[1]), set())
+    vector = vectors[name][0]
+    if vector[param_index]._uuid != param_uuid:
+        vectors[name][1].add(param_index)
+        vector._params[param_index] = ParameterVectorElement.__new__(
+            ParameterVectorElement, vector, param_index, uuid=param_uuid
+        )
+        vector._params[param_index].__init__(vector, param_index)
+    return vector[param_index]
+
+
+def _read_parameter_expression_v3(file_obj, vectors):
+    param_expr_raw = struct.unpack(PARAMETER_EXPR_PACK, file_obj.read(PARAMETER_EXPR_SIZE))
+    map_elements = param_expr_raw[0]
+    from sympy.parsing.sympy_parser import parse_expr
+
+    if HAS_SYMENGINE:
+        expr = symengine.sympify(parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8")))
+    else:
+        expr = parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8"))
+    symbol_map = {}
+    for _ in range(map_elements):
+        elem_raw = file_obj.read(PARAM_EXPR_MAP_ELEM_SIZE_V3)
+        elem = struct.unpack(PARAM_EXPR_MAP_ELEM_PACK_V3, elem_raw)
+        symbol_type = elem[0].decode("utf8")
+        if symbol_type == "p":
+            param = _read_parameter(file_obj)
+        elif symbol_type == "v":
+            param = _read_parameter_vec(file_obj, vectors)
+        elem_type = elem[1].decode("utf8")
+        elem_data = file_obj.read(elem[2])
+        if elem_type == "f":
+            value = struct.unpack("!d", elem_data)
+        elif elem_type == "i":
+            value = struct.unpack("!q", elem_data)
+        elif elem_type == "c":
+            value = complex(*struct.unpack(COMPLEX_PACK, elem_data))
+        elif elem_type in ("p", "v"):
+            value = param._symbol_expr
+        elif elem_type == "e":
+            value = _read_parameter_expression_v3(io.BytesIO(elem_data), vectors)
+        else:
+            raise TypeError("Invalid parameter expression map type: %s" % elem_type)
+        symbol_map[param] = value
+    return ParameterExpression(symbol_map, expr)
+
+
 def _read_parameter_expression(file_obj):
     param_expr_raw = struct.unpack(PARAMETER_EXPR_PACK, file_obj.read(PARAMETER_EXPR_SIZE))
     map_elements = param_expr_raw[0]
@@ -705,7 +875,7 @@ def _read_parameter_expression(file_obj):
     return ParameterExpression(symbol_map, expr)
 
 
-def _read_instruction(file_obj, circuit, registers, custom_instructions):
+def _read_instruction(file_obj, circuit, registers, custom_instructions, version, vectors):
     instruction_raw = file_obj.read(INSTRUCTION_SIZE)
     instruction = struct.unpack(INSTRUCTION_PACK, instruction_raw)
     name_size = instruction[0]
@@ -777,7 +947,13 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions):
             param = _read_parameter(container)
         elif type_str == "e":
             container = io.BytesIO(data)
-            param = _read_parameter_expression(container)
+            if version < 3:
+                param = _read_parameter_expression(container)
+            else:
+                param = _read_parameter_expression_v3(container, vectors)
+        elif type_str == "v":
+            container = io.BytesIO(data)
+            param = _read_parameter_vec(container, vectors)
         else:
             raise TypeError("Invalid parameter type: %s" % type_str)
         params.append(param)
@@ -838,7 +1014,7 @@ def _parse_custom_instruction(custom_instructions, gate_name, params):
     return inst_obj
 
 
-def _read_custom_instructions(file_obj, version):
+def _read_custom_instructions(file_obj, version, vectors):
     custom_instructions = {}
     custom_definition_header_raw = file_obj.read(CUSTOM_DEFINITION_HEADER_SIZE)
     custom_definition_header = struct.unpack(
@@ -864,7 +1040,7 @@ def _read_custom_instructions(file_obj, version):
                 if version < 3 or not name.startswith(r"###PauliEvolutionGate_"):
                     definition_circuit = _read_circuit(definition_buffer, version)
                 elif name.startswith(r"###PauliEvolutionGate_"):
-                    definition_circuit = _read_pauli_evolution_gate(definition_buffer)
+                    definition_circuit = _read_pauli_evolution_gate(definition_buffer, vectors)
             custom_instructions[name] = (type_str, num_qubits, num_clbits, definition_circuit)
     return custom_instructions
 
@@ -875,6 +1051,20 @@ def _write_parameter(file_obj, param):
     file_obj.write(name_bytes)
 
 
+def _write_parameter_vec(file_obj, param):
+    name_bytes = param._vector._name.encode("utf8")
+    file_obj.write(
+        struct.pack(
+            PARAMETER_VECTOR_ELEMENT_PACK,
+            len(name_bytes),
+            param._vector._size,
+            param._uuid.bytes,
+            param._index,
+        )
+    )
+    file_obj.write(name_bytes)
+
+
 def _write_parameter_expression(file_obj, param):
     from sympy import srepr, sympify
 
@@ -885,10 +1075,14 @@ def _write_parameter_expression(file_obj, param):
     file_obj.write(param_expr_header_raw)
     file_obj.write(expr_bytes)
     for parameter, value in param._parameter_symbols.items():
-        parameter_container = io.BytesIO()
-        _write_parameter(parameter_container, parameter)
-        parameter_container.seek(0)
-        parameter_data = parameter_container.read()
+        with io.BytesIO() as parameter_container:
+            if isinstance(parameter, ParameterVectorElement):
+                symbol_type_str = "v"
+                _write_parameter_vec(parameter_container, parameter)
+            else:
+                symbol_type_str = "p"
+                _write_parameter(parameter_container, parameter)
+            parameter_data = parameter_container.getvalue()
         if isinstance(value, float):
             type_str = "f"
             data = struct.pack("!d", value)
@@ -899,7 +1093,7 @@ def _write_parameter_expression(file_obj, param):
             type_str = "i"
             data = struct.pack("!q", value)
         elif value == parameter._symbol_expr:
-            type_str = "p"
+            type_str = symbol_type_str
             data = bytes()
         elif isinstance(value, ParameterExpression):
             type_str = "e"
@@ -910,7 +1104,12 @@ def _write_parameter_expression(file_obj, param):
         else:
             raise TypeError(f"Invalid expression type in symbol map for {param}: {type(value)}")
 
-        elem_header = struct.pack(PARAM_EXPR_MAP_ELEM_PACK, type_str.encode("utf8"), len(data))
+        elem_header = struct.pack(
+            PARAM_EXPR_MAP_ELEM_PACK_V3,
+            symbol_type_str.encode("utf8"),
+            type_str.encode("utf8"),
+            len(data),
+        )
         file_obj.write(elem_header)
         file_obj.write(parameter_data)
         file_obj.write(data)
@@ -993,6 +1192,12 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m
             type_key = "s"
             data = param.encode("utf8")
             size = len(data)
+        elif isinstance(param, ParameterVectorElement):
+            type_key = "v"
+            _write_parameter_vec(container, param)
+            container.seek(0)
+            data = container.read()
+            size = len(data)
         elif isinstance(param, Parameter):
             type_key = "p"
             _write_parameter(container, param)
@@ -1048,6 +1253,12 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate):
         time_type = b"f"
         time_data = struct.pack("!d", time)
         time_size = struct.calcsize("!d")
+    elif isinstance(time, ParameterVectorElement):
+        time_type = b"v"
+        with io.BytesIO() as buf:
+            _write_parameter_vec(buf, time)
+            time_data = buf.getvalue()
+            time_size = len(time_data)
     elif isinstance(time, Parameter):
         time_type = b"p"
         with io.BytesIO() as buf:
@@ -1077,7 +1288,7 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate):
     file_obj.write(synth_data)
 
 
-def _read_pauli_evolution_gate(file_obj):
+def _read_pauli_evolution_gate(file_obj, vectors):
     pauli_evolution_raw = struct.unpack(
         PAULI_EVOLUTION_DEF_PACK, file_obj.read(PAULI_EVOLUTION_DEF_SIZE)
     )
@@ -1105,7 +1316,10 @@ def _read_pauli_evolution_gate(file_obj):
             time = _read_parameter(buf)
     elif time_type == b"e":
         with io.BytesIO(time_data) as buf:
-            time = _read_parameter_expression(buf)
+            time = _read_parameter_expression_v3(buf, vectors)
+    elif time_type == b"v":
+        with io.BytesIO(time_data) as buf:
+            time = _read_parameter_vec(buf, vectors)
     synth_data = json.loads(file_obj.read(pauli_evolution_raw[4]))
     synthesis = getattr(evo_synth, synth_data["class"])(**synth_data["settings"])
     return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=synthesis)
@@ -1223,18 +1437,21 @@ def _write_circuit(file_obj, circuit):
     elif isinstance(circuit.global_phase, int):
         global_phase_type = b"i"
         global_phase_data = struct.pack("!q", circuit.global_phase)
+    elif isinstance(circuit.global_phase, ParameterVectorElement):
+        global_phase_type = b"v"
+        with io.BytesIO() as container:
+            _write_parameter_vec(container, circuit.global_phase)
+            global_phase_data = container.getvalue()
     elif isinstance(circuit.global_phase, Parameter):
-        container = io.BytesIO()
         global_phase_type = b"p"
-        _write_parameter(container, circuit.global_phase)
-        container.seek(0)
-        global_phase_data = container.read()
+        with io.BytesIO() as container:
+            _write_parameter(container, circuit.global_phase)
+            global_phase_data = container.getvalue()
     elif isinstance(circuit.global_phase, ParameterExpression):
         global_phase_type = b"e"
-        container = io.BytesIO()
-        _write_parameter_expression(container, circuit.global_phase)
-        container.seek(0)
-        global_phase_data = container.read()
+        with io.BytesIO() as container:
+            _write_parameter_expression(container, circuit.global_phase)
+            global_phase_data = container.getvalue()
     else:
         raise TypeError("unsupported global phase type %s" % type(circuit.global_phase))
     header_raw = HEADER_V2(
@@ -1357,10 +1574,11 @@ def load(file_obj):
 
 
 def _read_circuit(file_obj, version):
+    vectors = {}
     if version < 2:
         header, name, metadata = _read_header(file_obj)
     else:
-        header, name, metadata = _read_header_v2(file_obj)
+        header, name, metadata = _read_header_v2(file_obj, version, vectors)
     global_phase = header["global_phase"]
     num_qubits = header["num_qubits"]
     num_clbits = header["num_clbits"]
@@ -1459,8 +1677,18 @@ def _read_circuit(file_obj, version):
             global_phase=global_phase,
             metadata=metadata,
         )
-    custom_instructions = _read_custom_instructions(file_obj, version)
+    custom_instructions = _read_custom_instructions(file_obj, version, vectors)
     for _instruction in range(num_instructions):
-        _read_instruction(file_obj, circ, out_registers, custom_instructions)
+        _read_instruction(file_obj, circ, out_registers, custom_instructions, version, vectors)
+    for vec_name, (vector, initialized_params) in vectors.items():
+        if len(initialized_params) != len(vector):
+            warnings.warn(
+                f"The ParameterVector: '{vec_name}' is not fully identical to its "
+                "pre-serialization state. Elements "
+                f"{', '.join([str(x) for x in set(range(len(vector))) - initialized_params])} "
+                "in the ParameterVector will be not equal to the pre-serialized ParameterVector "
+                f"as they weren't used in the circuit: {circ.name}",
+                UserWarning,
+            )
 
     return circ


Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py
--- a/test/python/circuit/test_circuit_load_from_qpy.py
+++ b/test/python/circuit/test_circuit_load_from_qpy.py
@@ -26,6 +26,7 @@
 from qiskit.circuit.library import XGate, QFT, QAOAAnsatz, PauliEvolutionGate
 from qiskit.circuit.instruction import Instruction
 from qiskit.circuit.parameter import Parameter
+from qiskit.circuit.parametervector import ParameterVector
 from qiskit.synthesis import LieTrotter, SuzukiTrotter
 from qiskit.extensions import UnitaryGate
 from qiskit.opflow import I, X, Y, Z
@@ -584,6 +585,24 @@ def test_evolutiongate_param_expr_time(self):
         new_evo = new_circ.data[0][0]
         self.assertIsInstance(new_evo, PauliEvolutionGate)
 
+    def test_evolutiongate_param_vec_time(self):
+        """Test loading a an evolution gate that has a param vector element for time."""
+        synthesis = LieTrotter(reps=2)
+        time = ParameterVector("TimeVec", 1)
+        evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=time[0], synthesis=synthesis)
+        qc = QuantumCircuit(2)
+        qc.append(evo, range(2))
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circ = load(qpy_file)[0]
+
+        self.assertEqual(qc, new_circ)
+        self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data])
+
+        new_evo = new_circ.data[0][0]
+        self.assertIsInstance(new_evo, PauliEvolutionGate)
+
     def test_op_list_evolutiongate(self):
         """Test loading a circuit with evolution gate works."""
         evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=0.2, synthesis=None)
@@ -655,3 +674,65 @@ def test_parameter_global_phase(self):
         qpy_file.seek(0)
         new_circuit = load(qpy_file)[0]
         self.assertEqual(qc, new_circuit)
+
+    def test_parameter_vector(self):
+        """Test a circuit with a parameter vector for gate parameters."""
+        qc = QuantumCircuit(11)
+        input_params = ParameterVector("x_par", 11)
+        user_params = ParameterVector("θ_par", 11)
+        for i, param in enumerate(user_params):
+            qc.ry(param, i)
+        for i, param in enumerate(input_params):
+            qc.rz(param, i)
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circuit = load(qpy_file)[0]
+        expected_params = [x.name for x in qc.parameters]
+        self.assertEqual([x.name for x in new_circuit.parameters], expected_params)
+
+    def test_parameter_vector_element_in_expression(self):
+        """Test a circuit with a parameter vector used in a parameter expression."""
+        qc = QuantumCircuit(7)
+        entanglement = [[i, i + 1] for i in range(7 - 1)]
+        input_params = ParameterVector("x_par", 14)
+        user_params = ParameterVector("\u03B8_par", 1)
+
+        for i in range(qc.num_qubits):
+            qc.ry(user_params[0], qc.qubits[i])
+
+        for source, target in entanglement:
+            qc.cz(qc.qubits[source], qc.qubits[target])
+
+        for i in range(qc.num_qubits):
+            qc.rz(-2 * input_params[2 * i + 1], qc.qubits[i])
+            qc.rx(-2 * input_params[2 * i], qc.qubits[i])
+
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circuit = load(qpy_file)[0]
+        expected_params = [x.name for x in qc.parameters]
+        self.assertEqual([x.name for x in new_circuit.parameters], expected_params)
+
+    def test_parameter_vector_incomplete_warns(self):
+        """Test that qpy's deserialization warns if a ParameterVector isn't fully identical."""
+        vec = ParameterVector("test", 3)
+        qc = QuantumCircuit(1, name="fun")
+        qc.rx(vec[1], 0)
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        with self.assertWarnsRegex(UserWarning, r"^The ParameterVector.*Elements 0, 2.*fun$"):
+            new_circuit = load(qpy_file)[0]
+        self.assertEqual(qc, new_circuit)
+
+    def test_parameter_vector_global_phase(self):
+        """Test that a circuit with a standalone ParameterVectorElement phase works."""
+        vec = ParameterVector("phase", 1)
+        qc = QuantumCircuit(1, global_phase=vec[0])
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circuit = load(qpy_file)[0]
+        self.assertEqual(qc, new_circuit)
diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py
--- a/test/qpy_compat/test_qpy.py
+++ b/test/qpy_compat/test_qpy.py
@@ -23,6 +23,7 @@
 from qiskit.circuit.classicalregister import Clbit
 from qiskit.circuit.quantumregister import Qubit
 from qiskit.circuit.parameter import Parameter
+from qiskit.circuit.parametervector import ParameterVector
 from qiskit.circuit.qpy_serialization import dump, load
 from qiskit.opflow import X, Y, Z, I
 from qiskit.quantum_info.random import random_unitary
@@ -242,6 +243,38 @@ def generate_single_clbit_condition_teleportation():  # pylint: disable=invalid-
     return teleport_qc
 
 
+def generate_parameter_vector():
+    """Generate tests for parameter vector element ordering."""
+    qc = QuantumCircuit(11)
+    input_params = ParameterVector("x_par", 11)
+    user_params = ParameterVector("θ_par", 11)
+    for i, param in enumerate(user_params):
+        qc.ry(param, i)
+    for i, param in enumerate(input_params):
+        qc.rz(param, i)
+    return qc
+
+
+def generate_parameter_vector_expression():  # pylint: disable=invalid-name
+    """Generate tests for parameter vector element ordering."""
+    qc = QuantumCircuit(7)
+    entanglement = [[i, i + 1] for i in range(7 - 1)]
+    input_params = ParameterVector("x_par", 14)
+    user_params = ParameterVector("\u03B8_par", 1)
+
+    for i in range(qc.num_qubits):
+        qc.ry(user_params[0], qc.qubits[i])
+
+    for source, target in entanglement:
+        qc.cz(qc.qubits[source], qc.qubits[target])
+
+    for i in range(qc.num_qubits):
+        qc.rz(-2 * input_params[2 * i + 1], qc.qubits[i])
+        qc.rx(-2 * input_params[2 * i], qc.qubits[i])
+
+    return qc
+
+
 def generate_evolution_gate():
     """Generate a circuit with a pauli evolution gate."""
     # Runtime import since this only exists in terra 0.19.0
@@ -280,7 +313,11 @@ def generate_circuits(version_str=None):
         output_circuits["param_phase.qpy"] = generate_param_phase()
 
     if version_parts >= (0, 19, 1):
+        output_circuits["parameter_vector.qpy"] = [generate_parameter_vector()]
         output_circuits["pauli_evo.qpy"] = [generate_evolution_gate()]
+        output_circuits["parameter_vector_expression.qpy"] = [
+            generate_parameter_vector_expression()
+        ]
 
     return output_circuits
 
@@ -288,6 +325,15 @@ def generate_circuits(version_str=None):
 def assert_equal(reference, qpy, count, bind=None):
     """Compare two circuits."""
     if bind is not None:
+        reference_parameter_names = [x.name for x in reference.parameters]
+        qpy_parameter_names = [x.name for x in qpy.parameters]
+        if reference_parameter_names != qpy_parameter_names:
+            msg = (
+                f"Circuit {count} parameter mismatch:"
+                f" {reference_parameter_names} != {qpy_parameter_names}"
+            )
+            sys.stderr.write(msg)
+            sys.exit(4)
         reference = reference.bind_parameters(bind)
         qpy = qpy.bind_parameters(bind)
     if reference != qpy:
@@ -298,7 +344,7 @@ def assert_equal(reference, qpy, count, bind=None):
         sys.stderr.write(msg)
         sys.exit(1)
     # Don't compare name on bound circuits
-    if not bind and reference.name != qpy.name:
+    if bind is None and reference.name != qpy.name:
         msg = f"Circuit {count} name mismatch {reference.name} != {qpy.name}"
         sys.stderr.write(msg)
         sys.exit(2)
@@ -330,6 +376,10 @@ def load_qpy(qpy_files):
                     bind = [1, 2]
                 else:
                     bind = [1]
+            elif path == "parameter_vector.qpy":
+                bind = np.linspace(1.0, 2.0, 22)
+            elif path == "parameter_vector_expression.qpy":
+                bind = np.linspace(1.0, 2.0, 15)
 
             assert_equal(circuit, qpy_circuits[i], i, bind=bind)