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

Add QuantumCircuit.find_bit to find index and Registers for a Bit. #6621

Merged
merged 8 commits into from
Sep 28, 2021

Conversation

kdk
Copy link
Member

@kdk kdk commented Jun 22, 2021

Summary

Resolves #6495 by replacing QuantumCircuit{._qubit_set,._clbit_set} with QuantumCircuit{._qubit_indices,._clbits_indices} which are dicts mapping Qubit and Clbit instances to a tuple of their index (in QuantumCircuit{.qubits,.clbits} and any containing Registers on the circuit.

Adds a method QuantumCircuit.find_bit to perform a lookup of a given Bit.

Details and comments

WIP:

@kdk kdk added this to the 0.19 milestone Jun 22, 2021
@kdk kdk self-assigned this Jun 22, 2021
Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

I think we also will need this for dags too, This comes up a lot in transpiler passes too. We can do that in a separate PR though

@kdk kdk force-pushed the quantumcircuit-consolidate-bit_indices branch 4 times, most recently from 3e1afd2 to 3a6ff0f Compare July 13, 2021 20:05
@kdk kdk force-pushed the quantumcircuit-consolidate-bit_indices branch from 3a6ff0f to 8b56d85 Compare July 22, 2021 20:12
@kdk kdk marked this pull request as ready for review July 23, 2021 21:50
@kdk kdk requested review from manoelmarques, woodsp-ibm and a team as code owners July 23, 2021 21:50
@kdk kdk assigned mtreinish and unassigned kdk Jul 23, 2021
@kdk
Copy link
Member Author

kdk commented Jul 23, 2021

Due to the addition of QuantumCircuit{Q,C}regs this ended up larger than expected, so splitting out the DAGCircuit implementation and remaining bit_indices clean up in to subsequent PRs.

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.

The find_bit function certainly seems useful given that Bit no longer stores references to its containing registers. I wasn't around at the time that decision was made - if you want to be able to locate registers from Bit instances in some form (like here), could you point me to any discussion of why to remove it? Was there a discussion about keeping weakrefs in the Bit? Perhaps they could have made this reverse look-up a bit simpler, without introducing nasty reference cycles for the garbage collector to sort out.

As it stands, I have a worry about separation of concerns between QuantumCircuit.add_register and QuantumCircuitQregs in a few list-modifying methods. QuantumCircuitQregs seems to suggest that modifying QuantumCircuit.qregs directly is now acceptable (since it calls back to QuantumCircuit), such as

In [9]: from qiskit import QuantumCircuit, QuantumRegister
   ...: qregs = [QuantumRegister(2) for _ in [None]*2]
   ...: qc = QuantumCircuit(qregs[0])
   ...: qc.qregs.append(qregs[1])
   ...: qc.x(range(4))
   ...: qc.draw()
Out[9]:
       ┌───┐
q12_0: ┤ X ├
       ├───┤
q12_1: ┤ X ├
       ├───┤
q13_0: ┤ X ├
       ├───┤
q13_1: ┤ X ├
       └───┘

However, I think in doing so the responsibilities of QuantumCircuitQregs and QuantumCircuit.add_register have become slightly crossed. Now, since Qregs is meant to appear like a list to the user, I'd expect to be able to do

In [2]: from qiskit import QuantumCircuit, QuantumRegister
   ...: qregs = [QuantumRegister(2) for _ in [None]*4]
   ...: qc = QuantumCircuit(qregs[0], qregs[1])
   ...: qc.qregs[2:] = qregs[2:]
---------------------------------------------------------------------------
CircuitError                              Traceback (most recent call last)
<ipython-input-1-98e2b42d9269> in <module>
      2 qregs = [QuantumRegister(2) for _ in [None]*4]
      3 qc = QuantumCircuit(qregs[0], qregs[1])
----> 4 qc.qregs[2:] = qregs[2:]

~/code/qiskit/terra/qiskit/circuit/quantumcircuitdata.py in __setitem__(self, key, value)
    155
    156     def __setitem__(self, key, value):
--> 157         self._circuit.add_register(value)
    158         self._list[key] = value
    159         del self._list[-1]

~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py in add_register(self, *regs)
   1230
   1231             elif isinstance(register, list):
-> 1232                 self.add_bits(register)
   1233             else:
   1234                 raise CircuitError("expected a register")

~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py in add_bits(self, bits)
   1253                 self._clbit_indices[bit] = BitLocations(len(self._clbits) - 1, [])
   1254             else:
-> 1255                 raise CircuitError(
   1256                     "Expected an instance of Qubit, Clbit, or "
   1257                     "AncillaQubit, but was passed {}".format(bit)

CircuitError: "Expected an instance of Qubit, Clbit, or AncillaQubit, but was passed QuantumRegister(2, 'q2')"

(Qregs.__setitem__ doesn't handle slices), or with negative indices:

In [6]: from qiskit import QuantumCircuit, QuantumRegister
   ...: qregs = [QuantumRegister(2) for _ in [None]*4]
   ...: qc = QuantumCircuit(qregs[0])
   ...: print(qc.qregs[-1] is qregs[0], qc.qregs[-1] is qregs[1])
   ...: qc.qregs[-1] = qregs[1]
   ...: print(qc.qregs[-1] is qregs[0], qc.qregs[-1] is qregs[1])
True False
True False

where the last line should be False True.

I mentioned it further down, but I think we need some way for Qregs to access only the (currently imaginary) QuantumCircuit._add_references_to_bits functionality separately to the QuantumCircuit._add_references_to_registers, since Qregs is now meant to be handling that.


# str
self.assertEqual(
str(circ_regs), "[{}, {}, {}, {}]".format(*map(repr, [reg1, reg2, reg3, reg4]))
Copy link
Member

Choose a reason for hiding this comment

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

Logically should this be map(str, [...]), even if __str__ calls __repr__?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, this was copy paste from an existing test, but agree str makes more sense here.


# repr
self.assertEqual(
repr(circ_regs), "[{}, {}, {}, {}]".format(*map(repr, [reg1, reg2, reg3, reg4]))
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason to avoid repr([reg1, reg2, reg3, reg4])?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so, these should be equivalent. I'll update.

test/python/circuit/test_registerless_circuit.py Outdated Show resolved Hide resolved
qiskit/circuit/register.py Show resolved Hide resolved
Comment on lines 41 to 51
@abstractmethod
def __delitem__(self, i):
pass

@abstractmethod
def __setitem__(self, index, value):
pass

@abstractmethod
def insert(self, index, value):
pass
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason for leaving these abstract? With the docstring saying that this class is intended to implement list-like semantics, these would seem to have obvious default implementations, just like sort or copy.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's true. I left the methods that mutated self._list as abstract because I couldn't see an obvious implementation that worked cleanly (across the few use cases we currently support) with validation, so this is left to the subclass.

Comment on lines 136 to 142
try:
# To consolidate validation to __setitem__, insert None and overwrite.
self._list.insert(index, None)
self[index] = value
except CircuitError as err:
del self._list[index]
raise err
Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, would it be cleaner to factor out the validation into a separate, private instance method, and then just have __setitem__ and insert both call it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thinking about this a bit more, I think yes, but this would require a not inconsiderable refactor to both the circuit and register. We're lumping a few concerns behind the concept of "validation" (is the item being added a valid candidate member for this data structure, and following this addition/modification, is the data structure left in a consistent state) which are currently implemented throughout QuantumCircuit.add_register, QuantumCircuit.add_bits, and Register.__init__. If keeping these list-like interfaces is something we intend to support in the long term (and likely even if not), I agree there are definite opportunities for re-structuring to make this logic cleaner which I think we can tackle as a separate issue.

Comment on lines 144 to 145
def __delitem__(self, index):
del self._list[index]
Copy link
Member

Choose a reason for hiding this comment

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

Could this be made the concrete definition in VerifiedMutableSequence? I can't necessarily see a case where del would need to do any subclass-specific value validation, and the other defined subclass below uses the same method.

Copy link
Member

Choose a reason for hiding this comment

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

Come to think of it, I can think of a case - If Qregs manages the registers, and a register is deleted from the list, it should presumably call back to the circuit to delete references to the contained Bit instances, and raise a noisy exception if any current instructions operate on those bits.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, I think there may be a case that I'm missing here (maybe removing a register that has old-style bits needs to be verboten until old-style bits are removed, or removing a register that's used in a conditional of a subsequent instruction) but if not, this could be made concrete.

Comment on lines 185 to 188
def __setitem__(self, key, value):
self._circuit.add_register(value)
self._list[key] = value
del self._list[-1]
Copy link
Member

@jakelishman jakelishman Jul 26, 2021

Choose a reason for hiding this comment

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

The cyclical call structure here is difficult to follow for me. As I understand it:

  1. self._circuit.add_register(value) ends up calling self.append(value)
  2. now that the bit references are in place within self._circuit, we set the actual element of our list to the correct value
  3. we delete the extra data that the unwanted self.append(value) added

My worries here are:

  1. it feels like we actually just want something like self._circuit._add_internal_bit_references(value), since in the end, this class is responsible for the append of registers.
  2. this won't work if key is negative, or a slice, both of which are supported by list syntax. For example, self[-1] = QuantumRegister(2) would append the quantum register to the end of the list, overwrite the same list location with the same data, and then delete it off the end. A slice would cause a list to get nested in the middle.

I think the solution to this involves changing the call to self._circuit.add_register, so that the method by which we register bits in the circuit does not affect self, and from there we can delegate to self._list.__setitem__ entirely. This would probably involve splitting out the "add to list of registers" logic from the "add references to all bits logic" inside QubitCircuit.add_register, so we can only call the latter.

@kdk
Copy link
Member Author

kdk commented Aug 3, 2021

The find_bit function certainly seems useful given that Bit no longer stores references to its containing registers. I wasn't around at the time that decision was made - if you want to be able to locate registers from Bit instances in some form (like here), could you point me to any discussion of why to remove it? Was there a discussion about keeping weakrefs in the Bit? Perhaps they could have made this reverse look-up a bit simpler, without have nasty reference cycles for the garbage collector to sort out.

Qiskit/RFCs#15 (though dated) outlines the motivation behind removing Register references from Bits, which focuses more on unifying the structure of QuantumCircuits and Instructions, so as to make the interface for building and composing each more consistent. Removing reference cycles was just a happy byproduct :) .

As it stands, I have a worry about separation of concerns between QuantumCircuit.add_register and QuantumCircuitQregs in a few list-modifying methods. QuantumCircuitQregs seems to suggest that modifying QuantumCircuit.qregs directly is now acceptable (since it calls back to QuantumCircuit),

The intention here was actually the opposite, that QuantumCircuitQregs exist only as a compatibility shim so that modifying QuantumCircuit.qregs continues to work (and to be consistent with QuantumCircuit.qubits). QuantumCircuitQregs should only ever defer to QuantumCircuit.add_register (though I agree there are opportunities for factoring this code more clearly). Migrating these interfaces (.qregs, .cregs, .data) to be read-only is another viable option, though as this was previously supported behavior, would still require a period of deprecation.

I mentioned it further down, but I think we need some way for Qregs to access only the (currently imaginary) QuantumCircuit._add_references_to_bits functionality separately to the QuantumCircuit._add_references_to_registers, since Qregs is now meant to be handling that.

This seems like the right direction, and I think a lot of your (valid!) concerns stem from the tangling of validation logic in places like QuantumCircuit.add_register so something like QuantumCircuit._add_references_to_{bits,registers} seem a valid path forward. But, as in #6621 (comment) , unless there are interface changes that would come from this, this might be better addressed as a separate issue.

@kdk
Copy link
Member Author

kdk commented Sep 13, 2021

I split out the ValidatedMutableSequence and QuantumCircuit{Q,C}regs changes into a separate PR ( #7008 ) and reported the issue they were trying to fix separately ( #7009 ), so this PR should be ready for review.

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.

Most comments are typo fixes, and in principle everything looks good to me in this PR. My most major comments were whether we can de-duplicate some population of QuantumCircuit._qubit_indices by pushing some work onto QuantumCircuit.add_register (I think we can, without performance issues), and probably that we should use namedtuple attribute access rather than indexed access (unless it is critically slow on Python 3.7 - again, I think we should be ok).

qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py Outdated Show resolved Hide resolved
qiskit/circuit/quantumcircuit.py Outdated Show resolved Hide resolved
qiskit/circuit/quantumcircuit.py Outdated Show resolved Hide resolved
qiskit/circuit/quantumcircuit.py Outdated Show resolved Hide resolved
test/python/circuit/test_registerless_circuit.py Outdated Show resolved Hide resolved
test/python/circuit/test_registerless_circuit.py Outdated Show resolved Hide resolved
test/python/circuit/test_registerless_circuit.py Outdated Show resolved Hide resolved
test/python/circuit/test_registerless_circuit.py Outdated Show resolved Hide resolved
test/python/circuit/test_registerless_circuit.py Outdated Show resolved Hide resolved
qiskit/circuit/register.py Show resolved Hide resolved
@kdk kdk force-pushed the quantumcircuit-consolidate-bit_indices branch from 3c26aa0 to 1a9154e Compare September 27, 2021 15:41
@kdk kdk force-pushed the quantumcircuit-consolidate-bit_indices branch from 1a9154e to 4e1e5e8 Compare September 27, 2021 16:04
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.

Looks good to me - just needs a reno now.

I pushed up a minor change adding type information and Sphinx crossrefs to the documentation of find_bit, and removed the accidentally included test file (there's a bug in the visualisation tests that's generating an extra file at the moment), just in the interests of getting things merged quickly.

I'll approve properly after reno.

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.

These more minimal changes look good to me now, so now there's nothing blocking #6565!

@jakelishman jakelishman added automerge Changelog: New Feature Include in the "Added" section of the changelog labels Sep 27, 2021
@mergify mergify bot merged commit 4069b02 into Qiskit:main Sep 28, 2021
jakelishman added a commit to 1ucian0/qiskit-terra that referenced this pull request Oct 4, 2021
This resets the state of the QASM 3 exporter draft back to a clean
version, as if it were built cleanly upon the `find_bit` feature
(Qiskit#6621), without any of the extra changes that subsequently got cut out
of that PR.
jond01 added a commit to jond01/quantum-viz.js that referenced this pull request Oct 31, 2021
Replace with `QuantumCircuit.find_bit` when `qiskit-terra` 0.18.4 is released:
Qiskit/qiskit#6621
jond01 added a commit to jond01/quantum-viz.js that referenced this pull request Oct 31, 2021
Replace with `QuantumCircuit.find_bit` when `qiskit-terra` 0.18.4 is released:
Qiskit/qiskit#6621
jond01 added a commit to jond01/quantum-viz.js that referenced this pull request Nov 1, 2021
Replace with `QuantumCircuit.find_bit` when `qiskit-terra` 0.18.4 is released:
Qiskit/qiskit#6621
jond01 added a commit to jond01/quantum-viz.js that referenced this pull request Nov 1, 2021
Replace with `QuantumCircuit.find_bit` when `qiskit-terra` 0.18.4 is released:
Qiskit/qiskit#6621
jond01 added a commit to jond01/quantum-viz.js that referenced this pull request Nov 4, 2021
Replace with `QuantumCircuit.find_bit` when `qiskit-terra` 0.18.4 is released:
Qiskit/qiskit#6621
ElePT pushed a commit to ElePT/qiskit that referenced this pull request Jun 27, 2023
…iskit#6621)

* Add Register.index and Register._bit_indices.

* Add QuantumCircuit.find_bit to find index and Registers for a Bit.

* Add typing information and crossrefs to find_bit

* Revert accidental changes from base

* Add release note.

* Fix formatting in reno

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
ElePT pushed a commit to ElePT/qiskit-algorithms-test that referenced this pull request Jul 17, 2023
…iskit/qiskit#6621)

* Add Register.index and Register._bit_indices.

* Add QuantumCircuit.find_bit to find index and Registers for a Bit.

* Add typing information and crossrefs to find_bit

* Revert accidental changes from base

* Add release note.

* Fix formatting in reno

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog optional-registers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consolidate bit_indices as a circuit property
3 participants