-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Assign values directly to fully bound parameters in quantum circuits #10183
Conversation
pi_check casts integers to floats but not integers inside ParameterExpressions.
One or more of the the following people are requested to review this:
|
52b95a0
to
84cc952
Compare
Some other notes:
|
One or more of the the following people are requested to review this:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach seems good to me, we did have some issues now and then with bound expressions still being ParameterExpression
. Since we don't seem to have any use for keeping the object a ParameterExpression
, casting it to a number seems like an improvement 👍🏻 maybe @kdk or @jakelishman have further thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for all the effort here Will, and for finding what definitely looks like the safest course of action. I'm also on board with this new behaviour of QuantumCircuit.assign_parameters
- I think the intended benefits of the old behaviour never really materialised, but it has led to a couple of nuisances in the past.
Please could you add a couple of specific tests for the new behaviour and a regression test on the calibration issue? For example, testing that a fully bound real number produces an instance of float
in the parameters, a fully bound complex produces an instance of complex
, and that a previously failing roundtrip through QPY for the calibrations now succeeds?
In normal circumstances, I'd say this is too big a behaviour change for a backport, but I think you're right that the fix is sufficiently high priority and you've done as best as is possible to provide the least impactful fix. I'll get a second opinion from Matthew (@mtreinish) since it's against policy, but I think this'll be it.
releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general I'm in favor of this change, but I do worry about the backwards compatibility here, especially for a backport. The old behavior while inconvenient (an obviously blocking for serializing calibrations) but I always thought it was an intentional choice (I'd expect @kdk, @ewinston, or @Cryoris to know for sure) to enable reassignment of Parameter
s later if needed. I'm not sure how common a workflow that is in practice, but won't this cause us to lose the context around the parameter on assignment now making that impossible? Or will it still work because we've still got a reference to the original Parameter
in the parameter table? If it's the later I think I'm ok with it as a backport as long as we also include an upgrade note documenting the type change (it's unlikely to cause an issue but it's still necessary to document it just in case).
The "reassignment of parameters" after a complete binding was (as far as I remember) never actually implemented. Once you've bound something, you can't rebind that same object - the path to that is to keep a reference to the unbound attribute like via |
…7cad75c97183257f.yaml Co-authored-by: Jake Lishman <[email protected]>
…7cad75c97183257f.yaml Co-authored-by: Jake Lishman <[email protected]>
To expand on what Jake said, |
Pull Request Test Coverage Report for Build 5147385330
💛 - Coveralls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add tests and an upgrade note. I put a couple questions in line.
One other question is if we stop here or consider a larger change as a follow up, like allowing ParameterExpression.assign
to return a float rather than a ParameterExpression
.
val = int(new_param) | ||
elif new_param.is_real(): | ||
# Workaround symengine not supporting float(<ComplexDouble>) | ||
val = complex(new_param).real |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this complex(self).real
form be moved into ParameterExpression.__float__
or left here? One reason I had it here was is_real()
was already called, and I wasn't sure which behavior was preferred in __float__
. float(1+1j)
is a TypeError in standard Python.
A couple other things I noticed related to this:
- symengine/sympy treat complex numbers as floating point, so something involving complex integers will end up here and evaluate to a float rather than an int even if the math involves integers that evaluate to having no imaginary part.
- The
ParameterExpression.__float__
is misleading when the value is complex because it catches the sympy/symengine error and raises an error about unbound parameters instead of the complex vs float error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The __float__
method is only meant to be implemented for types for which there is an exact conversion (unlike __int__
, which has "casting" semantics), so it sounds like Symengine's behaviour is allowable by the spec. If we want to do something slightly tweaked, it'd be good to unify it in ParameterExpression.__float__
rather than here. I think in the interest of landing this PR for a patch release today, let's look at doing that in a follow-up, though.
Bullet 1: there's no such thing as a complex integral type in Python either, so I don't think we need to worry about that (also, ParameterExpression
is not entirely designed to represent integers).
Bullet 2: yeah, I believe it was an oversight when complex-number support was added to ParameterExpression
. There's issue #9187 tracking that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, okay. I hadn't seen #9187 which is the exact issue. Regarding ParameterExpression
behaving as a float when there is a zero imaginary part, I think following the behavior of Python's float
and complex
makes sense as an intuitive behavior, even though it goes against what I did here. My motivation for casting complex values with zero imaginary part back to float was that before I did that this test was failing:
Perhaps that test should be removed and the behavior not supported. It is a bit funny. That test was passing despite #9187 because __eq__
is less strict than __float__
about the types.
Maybe a design should be written up for Parameter
. These edge cases like #9187 and #9299 may be arising from different developers making different assumptions.
# Workaround symengine not supporting float(<ComplexDouble>) | ||
val = complex(new_param).real | ||
else: | ||
val = complex(new_param) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checking that we expect complex parameters possibly here? I didn't allow them for the calibration keys below. Perhaps I should? The previous code did not allow it and support for complex parameters in pulse has been deprecated in #9897 so I think it is not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think complex parameters are possible here. At least historically complex parameters were only expected in a pulse schedule. But @Cryoris can confirm (since he originally added support for complex parameters).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's right, so far the only use case was the pulses. If they are deprecated there, I don't think there is any other part that requires supporting binding complex parameter values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think/thought that various parts of the algorithms' gradients calculations and fancy custom gates in there used complex parameters (like PauliEvolutionGate
maybe?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took the complex case out but put it back just now because a couple tests were failing. The tests were trying to assign a complex value and see that an appropriate exception was raised by the validate_parameter
method of the instruction. We can leave it for later whether we want to lift up the exception raising to a higher level of than individual instructions' validate_parameter
.
The other nice thing that I just realized is that this will be a nice speed boost for QPY serialization of bound parameterized circuits. The biggest bottleneck in QPY serialization right now is calling out to sympy to serialize |
I pushed some new commits just now, mostly related to testing. Here is extra description (though the individual commits should be readable):
Besides any feedback on the changes here, I am curious about the idea of fixing |
qc.assign_parameters({x: value}, inplace=True) | ||
bound = qc.data[0].operation.params[0] | ||
self.assertIsInstance(bound, type_) | ||
self.assertAlmostEqual(bound, value, delta=1e-4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly, this passed for me with assertEqual
as well and I could not figure out why. I stepped in with a debugger and could see that bound
and value
had different precision for the float16 case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the closest double-precision float to float16(2.1)
is different to the closest double-precision float to real(2.1)
, so the rounding goes differently. Since this test is just testing types, it'd probably be best to test something with an exact representation in half-precision, such as 2.5, then we can use assertEqual
to assert that there's no dodgy conversions happening.
["float", 2.1, float], | ||
["float16", numpy.float16(2.1), float], | ||
["float32", numpy.float32(2.1), float], | ||
["float64", numpy.float64(2.1), float], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw float16 and float32 tested in test_bind_half_single_precision so I tested them here. We could test other types if any are relevant.
Complex assignment maybe not be supported but allowing it in the parameter assignment step lets validate_parameters get the value and raise an appropriate exception.
val = int(new_param) | ||
elif new_param.is_real(): | ||
# Workaround symengine not supporting float(<ComplexDouble>) | ||
val = complex(new_param).real |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The __float__
method is only meant to be implemented for types for which there is an exact conversion (unlike __int__
, which has "casting" semantics), so it sounds like Symengine's behaviour is allowable by the spec. If we want to do something slightly tweaked, it'd be good to unify it in ParameterExpression.__float__
rather than here. I think in the interest of landing this PR for a patch release today, let's look at doing that in a follow-up, though.
Bullet 1: there's no such thing as a complex integral type in Python either, so I don't think we need to worry about that (also, ParameterExpression
is not entirely designed to represent integers).
Bullet 2: yeah, I believe it was an oversight when complex-number support was added to ParameterExpression
. There's issue #9187 tracking that.
qc.assign_parameters({x: value}, inplace=True) | ||
bound = qc.data[0].operation.params[0] | ||
self.assertIsInstance(bound, type_) | ||
self.assertAlmostEqual(bound, value, delta=1e-4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the closest double-precision float to float16(2.1)
is different to the closest double-precision float to real(2.1)
, so the rounding goes differently. Since this test is just testing types, it'd probably be best to test something with an exact representation in half-precision, such as 2.5, then we can use assertEqual
to assert that there's no dodgy conversions happening.
(I'll just push a commit doing that floating-point change I suggested, then tag this for merge) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for getting this sorted, Will. It's a super nasty confluence of two Terra components that don't get a whole lot of attention, not to mention we'd somehow got to smash it down into something suitable for a patch release.
Tests passed locally on my machine, so hopefully we're all good here.
…10183) * Assign circuit parameters as int/float to instructions * Do not test that fully bound parameters are still ParameterExpressions * Change int to float in qasm output pi_check casts integers to floats but not integers inside ParameterExpressions. * Workaround symengine ComplexDouble not supporting float * black * Update releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml Co-authored-by: Jake Lishman <[email protected]> * Update releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml Co-authored-by: Jake Lishman <[email protected]> * Restore assigned parameter value type check to tests * Add test to check type and value of simple circuit parameter assignment * Add consistency check between assigned instruction data and calibrations dict keys * Add regression test * Add upgrade note * Remove support for complex instruction parameter assignment * Restore complex assignment Complex assignment maybe not be supported but allowing it in the parameter assignment step lets validate_parameters get the value and raise an appropriate exception. * black * More specific assertion methods * Use exact floating-point check --------- Co-authored-by: Jake Lishman <[email protected]> Co-authored-by: John Lapeyre <[email protected]> Co-authored-by: Jake Lishman <[email protected]> (cherry picked from commit 332bd9f)
…10183) (#10192) * Assign circuit parameters as int/float to instructions * Do not test that fully bound parameters are still ParameterExpressions * Change int to float in qasm output pi_check casts integers to floats but not integers inside ParameterExpressions. * Workaround symengine ComplexDouble not supporting float * black * Update releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml Co-authored-by: Jake Lishman <[email protected]> * Update releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml Co-authored-by: Jake Lishman <[email protected]> * Restore assigned parameter value type check to tests * Add test to check type and value of simple circuit parameter assignment * Add consistency check between assigned instruction data and calibrations dict keys * Add regression test * Add upgrade note * Remove support for complex instruction parameter assignment * Restore complex assignment Complex assignment maybe not be supported but allowing it in the parameter assignment step lets validate_parameters get the value and raise an appropriate exception. * black * More specific assertion methods * Use exact floating-point check --------- Co-authored-by: Jake Lishman <[email protected]> Co-authored-by: John Lapeyre <[email protected]> Co-authored-by: Jake Lishman <[email protected]> (cherry picked from commit 332bd9f) Co-authored-by: Will Shanks <[email protected]>
Summary
Changed
QuantumCircuit.assign_parameters
so that assigning a concrete value to a parameter inserts that value directly into the circuit's instructions' parameter rather than inserting aParameterExpression
containing only that value.Details and comments
When
QuantumCircuit.assign_parameters
is called, parameter substitution occurs in the list of parameters for each instruction, in the definition of each instrution, in the global phase of the circuit, and in the mapping of gates to schedules in the circuit's calibrations dictionary. Before this change, numeric assigned values were inserted into the instructions' parameters lists asParameterExpression
instances with no free parameters and into the calibrations dictionary as floats. This discrepancy in handling of parameters in these two cases was a problem because qpy serializesParameterExpression
differently fromfloat
. After a round trip, the numeric content in an instruction's parameters list might no longer match the numeric key in the calibrations dictionary. Code that takes circuits as input and executes them needs the instruction parameters and calibrations keys to match as it needs to look up the calibrations for the circuit's instructions in order to run the circuit.To address this issue, here
QuantumCircuit.assign_parameters
is changed to insert the numeric value directly into the instruction's parameter list, so that it matches the key in the calibrations dictionary. There is some precedent for this as theDelay
instruction already does type casting in itsvalidate_parameter
method whichassign_parameters
calls:https://github.com/Qiskit/qiskit-terra/blob/291824a392f131e3b7d7ffca4c6fafdecf72c007/qiskit/circuit/delay.py#L88-L102
This change is not without impact, but its impact needs to be weighed against the impact of not addressing the issue, which prevents parameterized pulse gates from being used with circuits serialized with qpy (such as with qiskit-ibm-provider). The updated unit tests give an indication of the impact:
test_parameters.py
were modified to remove anisinstance
check forParameterExpression
.int
andfloat
are valid in instruction params lists as well asParameterExpression
so I think these changes reflect overly tight test conditions and cases like this would be rare in user code.test_export.py
was updated to change a1
to a1.0
. This change was the result of the behavior ofpi_check
.pi_check(1)
gives"1.0"
whilea = Parameter("a"); pi_check(a.assign(a, 1))
gives"1"
. Based on this discrepancy, I think likely both float and integer representations need to be allowed in these cases or maybepi_check
needs to be more careful about changing types.test_complex_parameter_bound_to_real
was still failing. It tests that a complexParameterExpression
that evaluates to a real number gets treated as a real number by code that does not allow an imaginary component. Here is the test code:https://github.com/Qiskit/qiskit-terra/blob/291824a392f131e3b7d7ffca4c6fafdecf72c007/test/python/circuit/test_parameters.py#L1382-L1390
Interestingly, even though the equality check passes, the bound parameter can not be accessed directly as a float. This code:
gives
To work around this, this change casts to
complex
and then takes the real part after checking that the value is real rather than casting tofloat
directly.Closes #9764 and #10166