Change Parameter equality from python id() to internal uuid(). (#2947)

Parameters were defined such that they would only ever __eq__ self. That way, 
if a user defined a Parameter('theta') and wanted to compose in a subcircuit 
defined elsewhere which contained a different Parameter('theta'), we would 
be able to detect that the parameter names overlapped and raise an error.

However, if a user sent a Parameter to a different python instance though (say
 through multiprocessing.Pool or qiskit.tools.parallel_map), it would be
instantiated as a different python object and so no longer be considered equal
to the original parameter.

This PR changes Parameter to instead generate a random UUID on
instantiation and use that for equality testing. Unlike id(self), self._uuid will be
preserved across pickling and de-pickling, but should otherwise preserve
Parameter's equality behavior.

Fixes #2429
Fixes #2864
This commit is contained in:
Kevin Krsulich 2019-08-14 12:53:08 -04:00 committed by Matthew Treinish
parent 85ec21f6db
commit bb2ea373b2
3 changed files with 114 additions and 2 deletions

View File

@ -145,6 +145,9 @@ The format is based on [Keep a Changelog].
- Correctly serialize complex numbers with a nonzero real part
- Fixed bug in measure sampling for `BasicAer` Qasm simulator if only a
subset of qubits are measured (\#2790)
- Parameter objects can be serialized and communicated between
sub-processes (\#2429)
- Parameterized circuits no longer need to be transpiled individually (\#2864)
## [0.8.2] - 2019-06-14

View File

@ -15,6 +15,8 @@
Parameter Class for variable parameters.
"""
from uuid import uuid4
import sympy
from .parameterexpression import ParameterExpression
@ -22,6 +24,27 @@ from .parameterexpression import ParameterExpression
class Parameter(ParameterExpression):
"""Parameter Class for variable parameters"""
def __new__(cls, _, uuid=None):
# Parameter relies on self._uuid being set prior to other attributes
# (e.g. symbol_map) which may depend on self._uuid for Parameter's hash
# or __eq__ functions.
obj = object.__new__(cls)
if uuid is None:
obj._uuid = uuid4()
else:
obj._uuid = uuid
return obj
def __getnewargs__(self):
# Unpickling won't in general call __init__ but will always call
# __new__. Specify arguments to be passed to __new__ when unpickling.
return (self.name, self._uuid)
def __init__(self, name):
self._name = name
@ -48,3 +71,9 @@ class Parameter(ParameterExpression):
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.name)
def __eq__(self, other):
return isinstance(other, Parameter) and self._uuid == other._uuid
def __hash__(self):
return hash(self._uuid)

View File

@ -14,6 +14,7 @@
"""Test circuits with variable parameters."""
import pickle
from operator import add, sub, mul, truediv
import numpy
@ -21,9 +22,10 @@ import numpy
from qiskit import BasicAer
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.circuit import Gate, Parameter, ParameterVector, ParameterExpression
from qiskit.compiler import transpile
from qiskit.compiler import assemble
from qiskit.compiler import assemble, transpile
from qiskit.execute import execute
from qiskit.test import QiskitTestCase
from qiskit.tools import parallel_map
from qiskit.exceptions import QiskitError
@ -296,6 +298,84 @@ class TestParameters(QiskitTestCase):
for param in vec:
self.assertIn(param, qc_aer.parameters)
def test_parameter_equality_through_serialization(self):
"""Verify parameters maintain their equality after serialization."""
# pylint: disable=invalid-name
x = Parameter('x')
x1 = Parameter('x')
x_p = pickle.loads(pickle.dumps(x))
x1_p = pickle.loads(pickle.dumps(x1))
self.assertEqual(x, x_p)
self.assertEqual(x1, x1_p)
self.assertNotEqual(x, x1_p)
self.assertNotEqual(x1, x_p)
def test_binding_parameterized_circuits_built_in_multiproc(self):
"""Verify subcircuits built in a subprocess can still be bound."""
# ref: https://github.com/Qiskit/qiskit-terra/issues/2429
num_processes = 4
qr = QuantumRegister(3)
cr = ClassicalRegister(3)
circuit = QuantumCircuit(qr, cr)
parameters = [Parameter('x{}'.format(i))
for i in range(num_processes)]
results = parallel_map(_construct_circuit,
[(param) for param in parameters],
task_args=(qr,),
num_processes=num_processes)
for qc in results:
circuit += qc
parameter_values = [{x: 1 for x in parameters}]
qobj = assemble(circuit,
backend=BasicAer.get_backend('qasm_simulator'),
parameter_binds=parameter_values)
self.assertEqual(len(qobj.experiments), 1)
self.assertEqual(len(qobj.experiments[0].instructions), 4)
self.assertTrue(all(len(inst.params) == 1
and isinstance(inst.params[0], ParameterExpression)
and float(inst.params[0]) == 1
for inst in qobj.experiments[0].instructions))
def test_transpiling_multiple_parameterized_circuits(self):
"""Verify several parameterized circuits can be transpiled at once."""
# ref: https://github.com/Qiskit/qiskit-terra/issues/2864
qr = QuantumRegister(1)
qc1 = QuantumCircuit(qr)
qc2 = QuantumCircuit(qr)
theta = Parameter('theta')
qc1.u3(theta, 0, 0, qr[0])
qc2.u3(theta, 3.14, 0, qr[0])
circuits = [qc1, qc2]
job = execute(circuits,
BasicAer.get_backend('unitary_simulator'),
shots=512,
parameter_binds=[{theta: 1}])
self.assertTrue(len(job.result().results), 2)
def _construct_circuit(param, qr):
qc = QuantumCircuit(qr)
qc.ry(param, qr[0])
return qc
class TestParameterExpressions(QiskitTestCase):
"""Test expressions of Parameters."""