fix: Add Qiskit native QPY ParameterExpression serialization#2
Closed
mjrodriguez wants to merge 6 commits intostable/1.2from
Closed
fix: Add Qiskit native QPY ParameterExpression serialization#2mjrodriguez wants to merge 6 commits intostable/1.2from
mjrodriguez wants to merge 6 commits intostable/1.2from
Conversation
* Add Qiskit native QPY ParameterExpression serialization With the release of symengine 0.13.0 we discovered a version dependence on the payload format used for serializing symengine expressions. This was worked around in Qiskit#13251 but this is not a sustainable solution and only works for symengine 0.11.0 and 0.13.0 (there was no 0.12.0). While there was always the option to use sympy to serialize the underlying symbolic expression (there is a `use_symengine` flag on `qpy.dumps` you can set to `False` to do this) the sympy serialzation has several tradeoffs most importantly is much higher runtime overhead. To solve the issue moving forward a qiskit native representation of the parameter expression object is necessary for serialization. This commit bumps the QPY format version to 13 and adds a new serialization format for ParameterExpression objects. This new format is a serialization of the API calls made to ParameterExpression that resulted in the creation of the underlying object. To facilitate this the ParameterExpression class is expanded to store an internal "replay" record of the API calls used to construct the ParameterExpression object. This internal list is what gets serialized by QPY and then on deserialization the "replay" is replayed to reconstruct the expression object. This is a different approach to the previous QPY representations of the ParameterExpression objects which instead represented the internal state stored in the ParameterExpression object with the symbolic expression from symengine (or a sympy copy of the expression). Doing this directly in Qiskit isn't viable though because symengine's internal expression tree is not exposed to Python directly. There isn't any method (private or public) to walk the expression tree to construct a serialization format based off of it. Converting symengine to a sympy expression and then using sympy's API to walk the expression tree is a possibility but that would tie us to sympy which would be problematic for Qiskit#13267 and Qiskit#13131, have significant runtime overhead, and it would be just easier to rely on sympy's native serialization tools. The tradeoff with this approach is that it does increase the memory overhead of the `ParameterExpression` class because for each element in the expression we have to store a record of it. Depending on the depth of the expression tree this also could be a lot larger than symengine's internal representation as we store the raw api calls made to create the ParameterExpression but symengine is likely simplifying it's internal representation as it builds it out. But I personally think this tradeoff is worthwhile as it ties the serialization format to the Qiskit objects instead of relying on a 3rd party library. This also gives us the flexibility of changing the internal symbolic expression library internally in the future if we decide to stop using symengine at any point. Fixes Qiskit#13252 * Remove stray comment * Add format documentation * Add release note * Add test and fix some issues with recursive expressions * Add int type for operands * Add dedicated subs test * Pivot to stack based postfix/rpn deserialization This commit changes how the deserialization works to use a postfix stack based approach. Operands are push on the stack and then popped off based on the operation being run. The result of the operation is then pushed on the stack. This handles nested objects much more cleanly than the recursion based approach because we just keep pushing on the stack instead of recursing, making the accounting much simpler. After the expression payload is finished being processed there will be a single value on the stack and that is returned as the final expression. * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Change DERIV to GRAD * Change side kwarg to r_side * Change all the v4s to v13s * Correctly handle non-commutative operations This commit fixes a bug with handling the operand order of subtraction, division, and exponentiation. These operations are not commutative but the qpy deserialization code was treating them as such. So in cases where the argument order was reversed qpy was trying to flip the operands around for code simplicity and this would result in incorrect behavior. This commit fixes this by adding explicit op codes for the reversed sub, div, and pow and preserving the operand order correctly in these cases. * Fix lint --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
mjrodriguez
commented
Feb 24, 2025
| from qiskit.qpy import formats, exceptions | ||
|
|
||
| QPY_VERSION = 12 | ||
| QPY_VERSION = 13 |
Member
Author
There was a problem hiding this comment.
For reviewers, this will pin version to QPY 13.
Member
|
Great work Martin! I gotta find the time to actually setup the CI for this repo so we can make sure this passes tests and actually builds a wheel file. @mjrodriguez did you by chance run the tests locally as explained here? If not, would you mind doing so? |
chookity-pokk
approved these changes
Feb 28, 2025
Member
chookity-pokk
left a comment
There was a problem hiding this comment.
Tests are passing locally for me. Great work @mjrodriguez.
Member
Author
|
Closing this PR. |
chookity-pokk
pushed a commit
that referenced
this pull request
Apr 29, 2026
Qiskit#15943) * fix(transpiler): TemplateOptimization drops circuit global_phase on substitution circuit_to_dagdependency did not copy global_phase when converting a template QuantumCircuit to DAGDependency, so template phases were lost before matching. TemplateSubstitution.run_dag_opt also constructed a new DAGDependency for the optimised output without inheriting the original circuit's global_phase, resetting it to zero whenever at least one substitution was applied. Fixed by copying global_phase in circuit_to_dagdependency and seeding dag_dep_opt.global_phase from the circuit DAG at construction time. Also added the per-match phase subtraction (Fix 3) for when templates with nonzero global_phase become accepted via the identity check fix in Qiskit#14538. Fixes Qiskit#14537. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(transpiler): TemplateOptimization drops circuit global_phase on substitution circuit_to_dagdependency did not copy global_phase when converting a template QuantumCircuit to DAGDependency, so template phases were lost before matching. TemplateSubstitution.run_dag_opt also constructed a new DAGDependency for the optimised output without inheriting the original circuit's global_phase, resetting it to zero whenever at least one substitution was applied. Additionally, when a template carries a nonzero global_phase phi_T (gate content implements e^{-i*phi_T} * I while the full operator is I), the per-match phase contribution was not being subtracted from the output circuit. Fixes Qiskit#14537. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: black reformat test_template_matching.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: use RST hyperlink for issue reference in release note The :issue: role is not available in this project's Sphinx config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(transpiler): add combined circuit+template global_phase regression test Covers the case where both the circuit and the template carry a nonzero global_phase simultaneously, confirming that fixes #2 (circuit phase preserved across substitution) and #3 (per-match template phase subtracted) compose correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(transpiler): use assertEqual for Operator comparisons in template tests Replace assertTrue(Operator(x) == Operator(y)) with assertEqual(Operator(x), Operator(y)) so that assertion failures show the actual vs expected values rather than just "False is not True". Also shorten multi-line test docstrings to concise one-liners. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address review feedback on template optimization global phase fix - Simplify comment on global phase update in template_substitution.py - Split release note into per-bug files; add fix-circuit-to-dagdependency-global-phase-14537.yaml - Update tests: use issue reproducer (4-gate circuit) in test_template_nonzero_global_phase_applied_to_circuit, merge single/multiple match tests, drop modulo from assertAlmostEqual Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix test assertions and remove duplicate test - Fix test_circuit_and_template_both_have_nonzero_global_phase expected value from np.pi/4 to 7*np.pi/12 (pi/3 + pi/4) - Remove duplicate test_circuit_global_phase_preserved_with_multiple_template_matches - Update test_template_nonzero_global_phase_applied_to_circuit to use Operator equivalence instead of phase/count assertions since partial template match does occur Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update releasenotes/notes/fix-circuit-to-dagdependency-global-phase-14537.yaml Co-authored-by: Julien Gacon <gaconju@gmail.com> * Update releasenotes/notes/fix-template-optimization-global-phase-14537.yaml Co-authored-by: Julien Gacon <gaconju@gmail.com> * Update test/python/transpiler/test_template_matching.py Co-authored-by: Julien Gacon <gaconju@gmail.com> * Update test/python/transpiler/test_template_matching.py Actually I removed all reference to "Regression test" in the test descriptions based on this comment Qiskit#15943 (comment) Co-authored-by: Julien Gacon <gaconju@gmail.com> * Remove 'Regression test' from docstring per review feedback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Julien Gacon <gaconju@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses: https://qctrl.atlassian.net/browse/FIRE-2298
With the release of symengine 0.13.0 we discovered a version dependence on the payload format used for serializing symengine expressions. This was worked around in Qiskit#13251 but this is not a sustainable solution and only works for symengine 0.11.0 and 0.13.0 (there was no 0.12.0). While there was always the option to use sympy to serialize the underlying symbolic expression (there is a
use_symengineflag onqpy.dumpsyou can set toFalseto do this) the sympy serialzation has several tradeoffs most importantly is much higher runtime overhead. To solve the issue moving forward a qiskit native representation of the parameter expression object is necessary for serialization.This commit bumps the QPY format version to 13 and adds a new serialization format for ParameterExpression objects. This new format is a serialization of the API calls made to ParameterExpression that resulted in the creation of the underlying object. To facilitate this the ParameterExpression class is expanded to store an internal "replay" record of the API calls used to construct the ParameterExpression object. This internal list is what gets serialized by QPY and then on deserialization the "replay" is replayed to reconstruct the expression object. This is a different approach to the previous QPY representations of the ParameterExpression objects which instead represented the internal state stored in the ParameterExpression object with the symbolic expression from symengine (or a sympy copy of the expression). Doing this directly in Qiskit isn't viable though because symengine's internal expression tree is not exposed to Python directly. There isn't any method (private or public) to walk the expression tree to construct a serialization format based off of it. Converting symengine to a sympy expression and then using sympy's API to walk the expression tree is a possibility but that would tie us to sympy which would be problematic for Qiskit#13267 and Qiskit#13131, have significant runtime overhead, and it would be just easier to rely on sympy's native serialization tools.
The tradeoff with this approach is that it does increase the memory overhead of the
ParameterExpressionclass because for each element in the expression we have to store a record of it. Depending on the depth of the expression tree this also could be a lot larger than symengine's internal representation as we store the raw api calls made to create the ParameterExpression but symengine is likely simplifying it's internal representation as it builds it out. But I personally think this tradeoff is worthwhile as it ties the serialization format to the Qiskit objects instead of relying on a 3rd party library. This also gives us the flexibility of changing the internal symbolic expression library internally in the future if we decide to stop using symengine at any point.Fixes Qiskit#13252
Remove stray comment
Add format documentation
Add release note
Add test and fix some issues with recursive expressions
Add int type for operands
Add dedicated subs test
Pivot to stack based postfix/rpn deserialization
This commit changes how the deserialization works to use a postfix stack based approach. Operands are push on the stack and then popped off based on the operation being run. The result of the operation is then pushed on the stack. This handles nested objects much more cleanly than the recursion based approach because we just keep pushing on the stack instead of recursing, making the accounting much simpler. After the expression payload is finished being processed there will be a single value on the stack and that is returned as the final expression.
Apply suggestions from code review
Change DERIV to GRAD
Change side kwarg to r_side
Change all the v4s to v13s
Correctly handle non-commutative operations
This commit fixes a bug with handling the operand order of subtraction, division, and exponentiation. These operations are not commutative but the qpy deserialization code was treating them as such. So in cases where the argument order was reversed qpy was trying to flip the operands around for code simplicity and this would result in incorrect behavior. This commit fixes this by adding explicit op codes for the reversed sub, div, and pow and preserving the operand order correctly in these cases.
Summary
Details and comments