diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index f2690dbf88ef..72d00a523381 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -14,6 +14,7 @@ """ Look-up table for variable parameters in QuantumCircuit. """ +from collections import OrderedDict from collections.abc import MutableMapping from .instruction import Instruction @@ -27,7 +28,7 @@ def __init__(self, *args, **kwargs): the structure of _table is, {var_object: [(instruction_object, parameter_index), ...]} """ - self._table = dict(*args, **kwargs) + self._table = OrderedDict(*args, **kwargs) # replace by dict() when Python 3.5 reaches EOL def __getitem__(self, key): return self._table[key] diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 53e5c58b69f8..20f09170ce5c 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -568,7 +568,7 @@ def _check_cargs(self, cargs): if not all(self.has_register(i.register) for i in cargs): raise CircuitError("register not in this circuit") - def to_instruction(self, parameter_map=None): + def to_instruction(self, parameter_map=None, sort_parameters_by_name=True): """Create an Instruction out of this circuit. Args: @@ -576,15 +576,18 @@ def to_instruction(self, parameter_map=None): parameters in the circuit to parameters to be used in the instruction. If None, existing circuit parameters will also parameterize the instruction. + sort_parameters_by_name (bool): If True, the parameters in the circuit are sorted by + name before being added to the gate. Otherwise, the order of the circuit is used, + i.e. insertion-ordered by default. Returns: Instruction: a composite instruction encapsulating this circuit (can be decomposed back) """ from qiskit.converters.circuit_to_instruction import circuit_to_instruction - return circuit_to_instruction(self, parameter_map) + return circuit_to_instruction(self, parameter_map, sort_parameters_by_name) - def to_gate(self, parameter_map=None): + def to_gate(self, parameter_map=None, sort_parameters_by_name=True): """Create a Gate out of this circuit. Args: @@ -592,13 +595,16 @@ def to_gate(self, parameter_map=None): parameters in the circuit to parameters to be used in the gate. If None, existing circuit parameters will also parameterize the gate. + sort_parameters_by_name (bool): If True, the parameters in the circuit are sorted by + name before being added to the gate. Otherwise, the order of the circuit is used, + i.e. insertion-ordered by default. Returns: Gate: a composite gate encapsulating this circuit (can be decomposed back) """ from qiskit.converters.circuit_to_gate import circuit_to_gate - return circuit_to_gate(self, parameter_map) + return circuit_to_gate(self, parameter_map, sort_parameters_by_name) def decompose(self): """Call a decomposition pass on this circuit, @@ -1238,7 +1244,7 @@ def from_qasm_str(qasm_str): @property def parameters(self): """Convenience function to get the parameters defined in the parameter table.""" - return set(self._parameter_table.keys()) + return list(self._parameter_table.keys()) def bind_parameters(self, value_dict): """Assign parameters to values yielding a new circuit. @@ -1255,7 +1261,7 @@ def bind_parameters(self, value_dict): new_circuit = self.copy() unrolled_value_dict = self._unroll_param_dict(value_dict) - if unrolled_value_dict.keys() > self.parameters: + if unrolled_value_dict.keys() > set(self.parameters): raise CircuitError('Cannot bind parameters ({}) not present in the circuit.'.format( [str(p) for p in value_dict.keys() - self.parameters])) @@ -1267,7 +1273,7 @@ def bind_parameters(self, value_dict): return new_circuit def _unroll_param_dict(self, value_dict): - unrolled_value_dict = {} + unrolled_value_dict = OrderedDict() # replace by dict() when Python 3.5 reaches EOL for (param, value) in value_dict.items(): if isinstance(param, ParameterExpression): unrolled_value_dict[param] = value diff --git a/qiskit/compiler/assemble.py b/qiskit/compiler/assemble.py index cf4866228b5d..819ca380f890 100644 --- a/qiskit/compiler/assemble.py +++ b/qiskit/compiler/assemble.py @@ -326,7 +326,7 @@ def _expand_parameters(circuits, run_config): all_bind_parameters = [bind.keys() for bind in parameter_binds] - all_circuit_parameters = [circuit.parameters for circuit in circuits] + all_circuit_parameters = [set(circuit.parameters) for circuit in circuits] # Collect set of all unique parameters across all circuits and binds unique_parameters = {param diff --git a/qiskit/converters/circuit_to_gate.py b/qiskit/converters/circuit_to_gate.py index a313b17a8149..84edb9694ae9 100644 --- a/qiskit/converters/circuit_to_gate.py +++ b/qiskit/converters/circuit_to_gate.py @@ -14,12 +14,14 @@ """Helper function for converting a circuit to a gate""" +from collections import OrderedDict + from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.exceptions import QiskitError -def circuit_to_gate(circuit, parameter_map=None): +def circuit_to_gate(circuit, parameter_map=None, sort_parameters_by_name=True): """Build a ``Gate`` object from a ``QuantumCircuit``. The gate is anonymous (not tied to a named quantum register), @@ -32,6 +34,9 @@ def circuit_to_gate(circuit, parameter_map=None): parameters in the circuit to parameters to be used in the gate. If None, existing circuit parameters will also parameterize the Gate. + sort_parameters_by_name (bool): If True, the parameters in the circuit are sorted by name + before being added to the gate. Otherwise, the order of the circuit is used, i.e. + insertion-ordered by default. Raises: QiskitError: if circuit is non-unitary or if @@ -51,19 +56,26 @@ def circuit_to_gate(circuit, parameter_map=None): raise QiskitError('One or more instructions in this instruction ' 'cannot be converted to a gate') + parameter_dict = OrderedDict() # replace by dict() when Python 3.5 reaches EOL if parameter_map is None: - parameter_dict = {p: p for p in circuit.parameters} + parameter_dict.update(zip(circuit.parameters, circuit.parameters)) else: - parameter_dict = circuit._unroll_param_dict(parameter_map) - - if parameter_dict.keys() != circuit.parameters: - raise QiskitError(('parameter_map should map all circuit parameters. ' - 'Circuit parameters: {}, parameter_map: {}').format( - circuit.parameters, parameter_dict)) + unrolled_parameter_map = circuit._unroll_param_dict(parameter_map) + if unrolled_parameter_map.keys() != set(circuit.parameters): + raise QiskitError(('parameter_map should map all circuit parameters. ' + 'Circuit parameters: {}, parameter_map: {}').format( + circuit.parameters, parameter_dict)) + for parameter in circuit.parameters: + parameter_dict[parameter] = unrolled_parameter_map[parameter] + + if sort_parameters_by_name: + gate_parameters = sorted(parameter_dict.values(), key=lambda p: p.name) + else: + gate_parameters = list(parameter_dict.values()) gate = Gate(name=circuit.name, num_qubits=sum([qreg.size for qreg in circuit.qregs]), - params=sorted(parameter_dict.values(), key=lambda p: p.name)) + params=gate_parameters) gate.condition = None def find_bit_position(bit): diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 6001c9de659a..787e8ecb1f0d 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -14,13 +14,15 @@ """Helper function for converting a circuit to an instruction.""" +from collections import OrderedDict + from qiskit.exceptions import QiskitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister -def circuit_to_instruction(circuit, parameter_map=None): +def circuit_to_instruction(circuit, parameter_map=None, sort_parameters_by_name=True): """Build an ``Instruction`` object from a ``QuantumCircuit``. The instruction is anonymous (not tied to a named quantum register), @@ -33,6 +35,9 @@ def circuit_to_instruction(circuit, parameter_map=None): parameters in the circuit to parameters to be used in the instruction. If None, existing circuit parameters will also parameterize the instruction. + sort_parameters_by_name (bool): If True, the parameters in the circuit are sorted by name + before being added to the gate. Otherwise, the order of the circuit is used, i.e. + insertion-ordered by default. Raises: QiskitError: if parameter_map is not compatible with circuit @@ -59,20 +64,27 @@ def circuit_to_instruction(circuit, parameter_map=None): circuit_to_instruction(circ) """ + parameter_dict = OrderedDict() # replace by dict() when Python 3.5 reaches EOL if parameter_map is None: - parameter_dict = {p: p for p in circuit.parameters} + parameter_dict.update(zip(circuit.parameters, circuit.parameters)) else: - parameter_dict = circuit._unroll_param_dict(parameter_map) - - if parameter_dict.keys() != circuit.parameters: - raise QiskitError(('parameter_map should map all circuit parameters. ' - 'Circuit parameters: {}, parameter_map: {}').format( - circuit.parameters, parameter_dict)) + unrolled_parameter_map = circuit._unroll_param_dict(parameter_map) + if unrolled_parameter_map.keys() != set(circuit.parameters): + raise QiskitError(('parameter_map should map all circuit parameters. ' + 'Circuit parameters: {}, parameter_map: {}').format( + circuit.parameters, parameter_dict)) + for parameter in circuit.parameters: + parameter_dict[parameter] = unrolled_parameter_map[parameter] + + if sort_parameters_by_name: + gate_parameters = sorted(parameter_dict.values(), key=lambda p: p.name) + else: + gate_parameters = list(parameter_dict.values()) instruction = Instruction(name=circuit.name, num_qubits=sum([qreg.size for qreg in circuit.qregs]), num_clbits=sum([creg.size for creg in circuit.cregs]), - params=sorted(parameter_dict.values(), key=lambda p: p.name)) + params=gate_parameters) instruction.condition = None def find_bit_position(bit): diff --git a/releasenotes/notes/insertion-ordered-parameters-e8a4a44c3912df6f.yaml b/releasenotes/notes/insertion-ordered-parameters-e8a4a44c3912df6f.yaml new file mode 100644 index 000000000000..df10b57141d4 --- /dev/null +++ b/releasenotes/notes/insertion-ordered-parameters-e8a4a44c3912df6f.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Since Python 3.6 dictionaries are insertion ordered, i.e. retrieving the keys of a + dictionary will return an iterable mirroring the order in which the keys are inserted. + In the QuantumCircuit, Parameters are stored in a dictionary so the information of + the insertion order is present. + Currently, we discard this information at two points: + 1) if ``QuantumCircuit.parameters`` is called we return a set of the parameters, + which can change the order + 2) if we create a ``Gate`` or ``Instruction`` out of a circuit, the parameters are + actively sorted by name + The new behaviour ensures that parameters are insertion-sorted by + 1) returning a list, not set, of the parameter dictionary keys + 2) not re-sorting the parameters of the circuit + This feature mirrors the behaviour some users might expect and is necessary for changes + in Aqua introduced by the Ansatz object. \ No newline at end of file diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 9e0b63162c3e..c74c59c91baf 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -95,6 +95,33 @@ def test_fix_variable(self): self.assertEqual(float(bqc.data[0][0].params[0]), 0.6) self.assertEqual(float(bqc.data[1][0].params[1]), 0.6) + @data('circuit', 'gate', 'instruction', 'nested') + def test_sorted_by_insertion(self, target): + """Test that parameters are sorted by insertion.""" + params = [Parameter('2'), Parameter('13'), Parameter('0')] + qc = QuantumCircuit(2) + qc.rx(params[0], 0) + qc.rx(params[1], 1) + qc.rx(params[2], 0) + + if target == 'circuit': + self.assertEqual(qc.parameters, params) + elif target == 'gate': + self.assertEqual(qc.to_gate(sort_parameters_by_name=False).params, params) + elif target == 'instruction': + self.assertEqual(qc.to_gate(sort_parameters_by_name=False).params, params) + elif target == 'nested': + gate = qc.to_gate(sort_parameters_by_name=False) + qc2 = QuantumCircuit(2) + before, after = Parameter('before'), Parameter('after') + qc2.rz(before, 1) + qc2.append(gate, [0, 1]) + qc2.rz(after, 0) + + self.assertEqual(qc2.parameters, [before] + params + [after]) + else: + raise ValueError('Unsupported target {}'.format(target)) + def test_multiple_parameters(self): """Test setting multiple parameters""" theta = Parameter('θ') @@ -103,7 +130,7 @@ def test_multiple_parameters(self): qc = QuantumCircuit(qr) qc.rx(theta, qr) qc.u3(0, theta, x, qr) - self.assertEqual(qc.parameters, {theta, x}) + self.assertEqual(set(qc.parameters), {theta, x}) def test_partial_binding(self): """Test that binding a subset of circuit parameters returns a new parameterized circuit.""" @@ -116,7 +143,7 @@ def test_partial_binding(self): pqc = qc.bind_parameters({theta: 2}) - self.assertEqual(pqc.parameters, {x}) + self.assertEqual(pqc.parameters, [x]) self.assertEqual(float(pqc.data[0][0].params[0]), 2) self.assertEqual(float(pqc.data[1][0].params[1]), 2) @@ -133,14 +160,14 @@ def test_expression_partial_binding(self): pqc = qc.bind_parameters({theta: 2}) - self.assertEqual(pqc.parameters, {phi}) + self.assertEqual(pqc.parameters, [phi]) self.assertTrue(isinstance(pqc.data[0][0].params[0], ParameterExpression)) self.assertEqual(str(pqc.data[0][0].params[0]), 'phi + 2') fbqc = pqc.bind_parameters({phi: 1}) - self.assertEqual(fbqc.parameters, set()) + self.assertEqual(fbqc.parameters, []) self.assertTrue(isinstance(fbqc.data[0][0].params[0], ParameterExpression)) self.assertEqual(float(fbqc.data[0][0].params[0]), 3) @@ -156,14 +183,14 @@ def test_expression_partial_binding_zero(self): pqc = qc.bind_parameters({theta: 0}) - self.assertEqual(pqc.parameters, {phi}) + self.assertEqual(pqc.parameters, [phi]) self.assertTrue(isinstance(pqc.data[0][0].params[0], ParameterExpression)) self.assertEqual(str(pqc.data[0][0].params[0]), '0') fbqc = pqc.bind_parameters({phi: 1}) - self.assertEqual(fbqc.parameters, set()) + self.assertEqual(fbqc.parameters, []) self.assertTrue(isinstance(fbqc.data[0][0].params[0], ParameterExpression)) self.assertEqual(float(fbqc.data[0][0].params[0]), 0) @@ -227,7 +254,7 @@ def test_circuit_composition(self): qc2.measure(qr, cr) qc3 = qc1 + qc2 - self.assertEqual(qc3.parameters, {theta, phi}) + self.assertEqual(set(qc3.parameters), {theta, phi}) def test_composite_instruction(self): """Test preservation of parameters via parameterized instructions.""" @@ -246,7 +273,7 @@ def test_composite_instruction(self): qc2.ry(phi, qr2[0]) qc2.h(qr2) qc2.append(gate, qargs=[qr2[1]]) - self.assertEqual(qc2.parameters, {theta, phi}) + self.assertEqual(set(qc2.parameters), {phi, theta}) def test_parameter_name_conflicts_raises(self): """Verify attempting to add different parameters with matching names raises an error.""" @@ -454,26 +481,22 @@ def test_decompose_propagates_bound_parameters(self, target_type, parameter_type qc2 = QuantumCircuit(1) qc2.append(inst, [0]) + expected_qc2 = QuantumCircuit(1) if parameter_type == 'numbers': bound_qc2 = qc2.bind_parameters({theta: 0.5}) - expected_parameters = set() - expected_qc2 = QuantumCircuit(1) + expected_parameters = [] expected_qc2.rx(0.5, 0) else: phi = Parameter('ph') bound_qc2 = qc2.copy() bound_qc2._substitute_parameters({theta: phi}) - expected_parameters = {phi} - expected_qc2 = QuantumCircuit(1) + expected_parameters = [phi] expected_qc2.rx(phi, 0) decomposed_qc2 = bound_qc2.decompose() with self.subTest(msg='testing parameters of initial circuit'): - self.assertEqual(qc2.parameters, {theta}) - - with self.subTest(msg='testing parameters of bound circuit'): - self.assertEqual(bound_qc2.parameters, expected_parameters) + self.assertEqual(qc2.parameters, [theta]) with self.subTest(msg='testing parameters of deep decomposed bound circuit'): self.assertEqual(decomposed_qc2.parameters, expected_parameters) @@ -506,21 +529,21 @@ def test_decompose_propagates_deeply_bound_parameters(self, target_type, paramet if parameter_type == 'numbers': bound_qc3 = qc3.bind_parameters({theta: 0.5}) - expected_parameters = set() + expected_parameters = [] expected_qc3 = QuantumCircuit(1) expected_qc3.rx(0.5, 0) else: phi = Parameter('ph') bound_qc3 = qc3.copy() bound_qc3._substitute_parameters({theta: phi}) - expected_parameters = {phi} + expected_parameters = [phi] expected_qc3 = QuantumCircuit(1) expected_qc3.rx(phi, 0) deep_decomposed_qc3 = bound_qc3.decompose().decompose() - with self.subTest(msg='testing parameters of initial circuit'): - self.assertEqual(qc3.parameters, {theta}) + self.assertEqual(qc3.parameters, [theta]) + self.assertEqual(bound_qc3.parameters, expected_parameters) with self.subTest(msg='testing parameters of bound circuit'): self.assertEqual(bound_qc3.parameters, expected_parameters) @@ -528,9 +551,6 @@ def test_decompose_propagates_deeply_bound_parameters(self, target_type, paramet with self.subTest(msg='testing parameters of deep decomposed bound circuit'): self.assertEqual(deep_decomposed_qc3.parameters, expected_parameters) - with self.subTest(msg='testing deep decomposed circuit'): - self.assertEqual(deep_decomposed_qc3, expected_qc3) - @data('gate', 'instruction') def test_executing_parameterized_instruction_bound_early(self, target_type): """Verify bind-before-execute preserves bound values.""" @@ -778,6 +798,7 @@ def test_to_instruction_with_expression(self, target_type, order): elif target_type == 'instruction': gate = qc1.to_instruction() + # parameters are name sorted by default, thus [phi, θ] self.assertEqual(gate.params, [phi, theta]) delta = Parameter('delta') @@ -786,7 +807,17 @@ def test_to_instruction_with_expression(self, target_type, order): qc2.ry(delta, qr2[0]) qc2.append(gate, qargs=[qr2[1]]) - self.assertEqual(qc2.parameters, {delta, theta, phi}) + with self.subTest(msg='test contained parameters'): + self.assertEqual(set(qc2.parameters), {delta, theta, phi}) + + with self.subTest(msg='test order of the parameters'): + # Within the circuit the parameters are insertion ordered. + # Since first ry(delta) is applied, this is the first parameter. Then `gate` is + # appended which has the parameters [phi, theta]. Note that the conversion to + # a gate assumes sorting of the parameters by name, which can be turned off + # using `QuantumCircuit.to_gate(sort_parameters_by_name=False)`. Then the insertion + # ordering of the circuit is also used in the gate. + self.assertEqual(qc2.parameters, [delta, phi, theta]) binds = {delta: 1, theta: 2, phi: 3} expected_qc = QuantumCircuit(qr2) @@ -800,7 +831,7 @@ def test_to_instruction_with_expression(self, target_type, order): elif order == 'decompose-bind': decomp_bound_qc = qc2.decompose().bind_parameters(binds) - self.assertEqual(decomp_bound_qc.parameters, set()) + self.assertEqual(decomp_bound_qc.parameters, []) self.assertEqual(decomp_bound_qc, expected_qc) @combine(target_type=['gate', 'instruction'], @@ -825,6 +856,7 @@ def test_to_instruction_expression_parameter_map(self, target_type, order): elif target_type == 'instruction': gate = qc1.to_instruction(parameter_map={theta: theta_p, phi: phi_p}) + # parameters are name sorted by default, thus [phi, theta] self.assertEqual(gate.params, [phi_p, theta_p]) delta = Parameter('delta') @@ -833,7 +865,17 @@ def test_to_instruction_expression_parameter_map(self, target_type, order): qc2.ry(delta, qr2[0]) qc2.append(gate, qargs=[qr2[1]]) - self.assertEqual(qc2.parameters, {delta, theta_p, phi_p}) + with self.subTest(msg='test contained parameters'): + self.assertEqual(set(qc2.parameters), {delta, theta_p, phi_p}) + + with self.subTest(msg='test order of the parameters'): + # Within the circuit the parameters are insertion ordered. + # Since first ry(delta) is applied, this is the first parameter. Then `gate` is + # appended which has the parameters [phi, theta]. Note that the conversion to + # a gate assumes sorting of the parameters by name, which can be turned off + # using `QuantumCircuit.to_gate(sort_parameters_by_name=False)`. Then the insertion + # ordering of the circuit is also used in the gate. + self.assertEqual(qc2.parameters, [delta, phi_p, theta_p]) binds = {delta: 1, theta_p: 2, phi_p: 3} expected_qc = QuantumCircuit(qr2) @@ -847,7 +889,7 @@ def test_to_instruction_expression_parameter_map(self, target_type, order): elif order == 'decompose-bind': decomp_bound_qc = qc2.decompose().bind_parameters(binds) - self.assertEqual(decomp_bound_qc.parameters, set()) + self.assertEqual(decomp_bound_qc.parameters, []) self.assertEqual(decomp_bound_qc, expected_qc) def test_binding_across_broadcast_instruction(self):