Skip to content
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

Issue 6633: Update _parameter_table after circuit assignment #7434

Closed
wants to merge 21 commits into from

Conversation

TakahitoMotoki
Copy link

@TakahitoMotoki TakahitoMotoki commented Dec 22, 2021

Summary

The problem is data property in QuantumCircuit object is inconsistent with ParameterTable when data property is updated by __setitem__ in quantumcircuitdata.py. In this modification, ParameterTable is cleared and reconstruct from the updated data property.

Details and comments

Problem

When data property in QuantumCircuit qc.data is set via __setitem__ in quantumcircuitdata.py, qc.data becomes inconsistent with qc._parameter_table.

Reason

qc.data element is updated in __setitem__ but a new element is added to qc._parameter_table by _update_parameter_table() instead of updating an existing element.

Solution

My idea is

  1. Update an element of qc.data.
  2. Obtain relating Parameter object from old qc.data and instruction.
  3. Clear qc._parameter_table.
  4. Reconstruct qc._parameter_tablefrom the new qc.data`.

Fixes #6633

@TakahitoMotoki TakahitoMotoki requested a review from a team as a code owner December 22, 2021 13:57
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, thanks for looking at this!

At the moment, I'm quite worried that this implementation will have really big performance implications - on every modification to the circuit data, it iterates through the entire circuit's data twice over. The current data structures in ParameterTable are really only designed for additive usage; they can't really handle modification, because there's no way to tell how many different data elements use each entry in the table. I think we would need to introduce some form of reference counting to the elements of ParameterTable to handle this somewhat efficiently.

@3yakuya
Copy link
Contributor

3yakuya commented Jan 1, 2022

Also appears to be blocked on #7457

@TakahitoMotoki
Copy link
Author

TakahitoMotoki commented Jan 5, 2022

@jakelishman
Thank you for your comment. I modified the PR.
I think your comment is reflected on 3 and 4 in the steps below.
When qc.data is updated, find a corresponding element in qc._parameter_table instead of reset and reconstruct it.

  1. Before the update, save original qc.data to circ_data["old"].
  2. Update qc.data and qc._parameter_table.
  3. After the update, save qc.data to circ_data["new"].
  4. If an original element in qc.data have Parameter, delete a corresponding element in qc._parameter_table.
  5. If an updated element in qc.data have Parameter, insert an element in qc._parameter_table.

@javabster
Copy link
Contributor

In addition to @jakelishman's comments, a few more things from me:

  • CI is failing with lint errors, running tox -eblack should fix this up
  • Could you please add a test for this? I think test_circuit_data.py would be a good place to add some
  • Please also add a release note, instructions here

@TakahitoMotoki
Copy link
Author

@javabster
Thank you for your comment!

  • I have already run tox -eblack and it seemed fine (1st screenshot), but it didn't fix the error. I dug into the details though (2nd screenshot), I couldn't get it.
  • I added a test in test_circuit_data.py
  • I added release note.

スクリーンショット 2022-01-17 22 40 17

スクリーンショット 2022-01-17 22 44 52

Copy link
Member

@jakelishman jakelishman left a 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 that this new form has addressed the efficiency concerns from before; it still loops over the entire circuit data twice, even though this implementation of __setitem__ only allows replacements. Since the number and position of all the instructions cannot change, we shouldn't need to loop over everything each time to determine things.

I think a lot of the parameter handling here should be done in ParameterTable, not in this structure. We shouldn't be mutating private attributes of objects that aren't ourselves - it's very fragile to code changes, and it's difficult to maintain, because it's never clear which object is responsible for the mutations. I think some of this will be made easier by #7408; the ordering of references in the table will become less important, so searching it will be much faster.

This whole QuantumCircuitData object is a slight exception, because it mutates private methods of QuantumCircuit, but in an ideal world, this object would be defined inside QuantumCircuit itself, because it's really not separate at all.

Comment on lines 58 to 62
circ_data = {}
param = {"old": 0, "new": 0}
circ_data["old"] = self._circuit._data
self._circuit._data[key] = (instruction, qargs, cargs)
circ_data["new"] = self._circuit._data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to use dictionaries for this. It's much clearer to use two separate variables. This also does not do what you think - you're saving the same reference to the list into both old and new, and so there are no circumstances in which circ_data["old"] will ever be different from circ_data["new"], because they're the exact same object.

Comment on lines 66 to 68
if len(circ_data["old"][key][0].params) > 0:
if isinstance(circ_data["old"][key][0].params[0], Parameter):
param["old"] = circ_data["old"][key][0].params[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only checks the first parameter of an instruction, not all of the parameters.

Comment on lines 76 to 77
for idx, data in enumerate(circ_data["old"]):
if idx <= key:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be better done by only enumerating the items you are interested in, rather than looping through everything and skipping the last set.

Comment on lines 66 to 93
if len(circ_data["old"][key][0].params) > 0:
if isinstance(circ_data["old"][key][0].params[0], Parameter):
param["old"] = circ_data["old"][key][0].params[0]

if len(circ_data["new"][key][0].params) > 0:
if isinstance(circ_data["new"][key][0].params[0], Parameter):
param["new"] = circ_data["new"][key][0].params[0]

param_count = -1
if isinstance(param["old"], Parameter):
for idx, data in enumerate(circ_data["old"]):
if idx <= key:
if len(data[0].params) > 0:
if data[0].params[0] == param["old"]:
param_count = param_count + 1
else:
pass
self._circuit._parameter_table[param["old"]].pop(param_count)

param_count = -1
if isinstance(param["new"], Parameter):
for idx, data in enumerate(circ_data["new"]):
if idx <= key:
if len(data[0].params) > 0:
if data[0].params[0] == param["new"]:
param_count = param_count + 1
else:
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole section is difficult to read, to the point that I'm not confident that I can review it for correctness. I suggested a few simplifications above, but really I think a lot of this should be in quite a different form within ParameterTable, not here. I think that a lot of this information could be read much faster out of ParameterTable, rather than iterating through all the circuit data twice, but I'm not certain.


self._circuit._update_parameter_table(instruction)

if len(circ_data["old"][key][0].params) > 0:
if isinstance(circ_data["old"][key][0].params[0], Parameter):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't handle ParameterExpression, which is the base class - there may be more than one parameter in any given position.

Comment on lines 94 to 96
added_element = self._circuit._parameter_table[param["new"]][-1]
self._circuit._parameter_table[param["new"]].insert(param_count, added_element)
self._circuit._parameter_table[param["new"]].pop()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why update the parameter table earlier if we need to remove what it did and re-arrange it?

More importantly, though, all of this modification of the internal data structures of ParameterTable shouldn't happen here in an unrelated class - I think a more complete mechanism for this needs to have ParameterTable gain some public methods that can be used to remove parameters from the table. However, that comes with the reference counting issues that I mentioned previously; this PR also doesn't account for the parameter table needing to remove a Parameter key if the last reference to a Parameter is removed from the circuit. If the same instruction (referentially) is put in the circuit more than once using this method, the number of elements in ParameterTable will likely be incorrect, and removals might not make sense.

@@ -62,6 +62,7 @@ class BaseTestCase(testtools.TestCase):
assertRaises = unittest.TestCase.assertRaises
assertEqual = unittest.TestCase.assertEqual


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version of black you have is out-of-date - we updated the tox configuration a couple of months ago, including a bump of black. If you've merged in the latest changes from the main branch, then you should be able to run tox -r -e black to regenerate the tox environment, which should install the correct version of black, and fix the discrepancy between your local copy and our CI.

Comment on lines 429 to 443
def test_parameter_table_is_updated(self):
"""Verify that circuit._parameter_table is consistent with circuit.data."""
qr = QuantumRegister(1, "q")
qc = QuantumCircuit(qr)

a = Parameter("a")
qc.h(0)
qc.ry(a, 0)
qc.rz(a, 0)

qc.data[0] = (RXGate(a), [qr[0]], [])
qc.data[1] = (RXGate(a), [qr[0]], [])
qc.data[2] = (HGate(), [qr[0]], [])

qc.copy()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has no assertions - it is only testing that nothing crashes. Tests also need to have positive assertions, that the output is correct. In every case, we should also test that:

  • the circuit is equal to the form we expect it to be
  • parameter assignment works, and binds all parameters

There are also quite a few special cases, that we should be sure to test as well:

  • an instruction is added that adds a parameter to the circuit that wasn't previously there
  • an instruction is changed such that the total number of instructions in the circuit that have parameters is different
  • instructions with more than one parameter (e.g. u) work
  • instructions that take a ParameterExpression work
  • if the last reference to a Parameter is removed from a circuit with this method, it is no longer in the parameter table
  • the table is updated correctly if the same instruction (referentially) is passed in more than once

That list might not be complete, so other ideas are good too.

@TakahitoMotoki
Copy link
Author

TakahitoMotoki commented Jan 31, 2022

@jakelishman
Thank you for your review.
I have read through the comments and found that it needs quite a big modification.

Thus, I want to set direction with this issue.

I think the solution to this problem looks like…

  • Define a new public method to remove an instruction in ParameterTable.py if instructions set in _parameter_table and the input instruction have the same address. (Similar to _check_dup_param_spec in QuantumCircuit.py.)
  • The method is called from setitem in QuantumCircuitData.py. The input should be the instruction and param_index defined in qc._data before the replacement.

I am really grateful for your help.

@coveralls
Copy link

Pull Request Test Coverage Report for Build 1697828587

  • 31 of 31 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.02%) to 83.112%

Totals Coverage Status
Change from base Build 1697484261: 0.02%
Covered Lines: 52022
Relevant Lines: 62593

💛 - Coveralls

@TakahitoMotoki
Copy link
Author

TakahitoMotoki commented Feb 10, 2022

I made quite a big modification. I am really sorry for troubling you.
Thank you for your help.

Problem (Copy from PullReq statement)

When data property in QuantumCircuit qc.data is set via setitem in quantumcircuitdata.py, qc.data becomes inconsistent with qc._parameter_table.

Reason (Copy from PullReq statement)

An element of qc.data is properly updated by setitem but an old instruction remains in qc._parameter_table because there is no code to remove an old instruction from qc._parameter_table in setitem.

Solution

My idea is

  1. Define a public method to remove an element from ParameterTable. This method takes old instruction as an input. If ParameterTable has an element with the same address of the input, then remove it.
  2. Call the public method from setitem in quantumcircuitdata.py.

About test code

I added a new test in test/python/circuit/test_circuit_data.py.
It covers following cases

  • Case1-1: Add an instruction with a parameter that was not previously in ParameterTable.

  • Case1-2: Add an instruction with a parameter that was already in ParameterTable.

  • Case2-1: Replace an instruction with a parameter by other instruction with the same parameter.

  • Case2-2: Replace an instruction with a parameter by other instruction with a different parameter.

  • Case2-3: Replace an instruction with a parameter by other instruction with no parameters.

  • Case3-1: Add an instruction with multiple parameters

  • Case3-2: Replace an instruction with multiple parameters by other instruction

@TakahitoMotoki TakahitoMotoki marked this pull request as ready for review March 2, 2022 13:34
@HuangJunye HuangJunye added the Community PR PRs from contributors that are not 'members' of the Qiskit repo label Jun 21, 2022
@HuangJunye
Copy link
Collaborator

@TakahitoMotoki Sorry for slow response. Are you still working on this PR? There are a few conflicts with the main branch. Can you please resolve them? Please let us know if you need help.

@javabster
Copy link
Contributor

Closing this PR as contributor is no longer responsive.
If contributor returns feel free to let us know if you would like us to re-open this PR 😃

@javabster javabster closed this Apr 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community PR PRs from contributors that are not 'members' of the Qiskit repo
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Parameter table not updated after circuit assignment
6 participants