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

More improvements to the Controlled infrastructure #879

Merged
merged 5 commits into from
Apr 19, 2024

Conversation

tanujkhattar
Copy link
Collaborator

Fixes #865
Fixes #686

The improvements are -

  • GateWithRegisters.controlled() now returns Controlled bloq
  • cirq_gate_to_bloq now maps a cirq.Controlled gates to Controlled bloq.
  • Controlled bloq is now a GateWithRegisters. This is required because we want to support cirq style simulations for circuits containing Controlled(Rx()) bloqs and BloqAsCirqGate(Controlled()) is not sufficient because we need special logic to derive the unitary matrix for controlled bloqs (eg: when subbloq is a GateWithRegisters but doesn't have only THRU registers).

After this PR, we should never need to invoke the cirq.Controlled gates in either cirq-style or bloq-style API in Qualtran and we should always use Controlled bloq in all cases. All existing uses of cirq.Controlled gates would be updated to Controlled bloq when invoking the bloq-style API using the cirq_gate_to_bloq mapping.

This is the final PR in my series of PRs to improve the controlled infrastructure. cc @mpharrigan

Comment on lines 321 to 326
def controlled(
self,
num_controls: Optional[int] = None,
num_controls: Union[Optional[int], 'CtrlSpec'] = None,
control_values=None,
control_qid_shape: Optional[Tuple[int, ...]] = None,
) -> 'cirq.Gate':
from qualtran.cirq_interop import BloqAsCirqGate

controlled_gate = cirq.ControlledGate(
self,
num_controls=num_controls,
control_values=control_values,
control_qid_shape=control_qid_shape,
)
ctrl_spec = CtrlSpec.from_cirq_cv(controlled_gate.control_values)
return BloqAsCirqGate(Controlled(self, ctrl_spec))
) -> 'GateWithRegisters':
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be useful to provide overloaded type hints?

    # pylint: disable=arguments-renamed
    @overload
    def controlled(
        self,
        num_controls: Optional[int] = None,
        control_values=None,
        control_qid_shape: Optional[Tuple[int, ...]] = None,
    ) -> 'GateWithRegisters':
        ...

    # pylint: disable=signature-differs
    @overload
    def controlled(self, ctrl_spec: 'CtrlSpec') -> 'GateWithRegisters':
        ...

    # pylint: disable=arguments-renamed
    def controlled(self, num_controls=None, control_values=None, control_qid_shape=None):

Right now it's difficult to understand the usage without looking at the function body.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, I think this needs a big docstring that describes the two modes of operation

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a detailed docstring and the overloads. Also added tests to verify the different cases raise errors.

Copy link
Collaborator

@mpharrigan mpharrigan 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. approval but please fix the issues raised

@@ -150,6 +150,13 @@ def decompose_from_registers(
return cirq.inverse(self.subbloq.decompose_from_registers(context=context, **quregs))
return super().decompose_from_registers(context=context, **quregs)

def _circuit_diagram_info_(self, args: 'cirq.CircuitDiagramInfoArgs'):
Copy link
Collaborator

Choose a reason for hiding this comment

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

return type annotation

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

cirq.ControlledGate(self.subbloq, control_values=self.ctrl_spec.to_cirq_cv())
)
if all(reg.side == Side.THRU for reg in self.subbloq.signature):
# subbloq has only THRU registers, so the unitary is well defined.
Copy link
Collaborator

Choose a reason for hiding this comment

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

pycharm tells me this is supposed to be "well-defined"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

reworded

Comment on lines 321 to 326
def controlled(
self,
num_controls: Optional[int] = None,
num_controls: Union[Optional[int], 'CtrlSpec'] = None,
control_values=None,
control_qid_shape: Optional[Tuple[int, ...]] = None,
) -> 'cirq.Gate':
from qualtran.cirq_interop import BloqAsCirqGate

controlled_gate = cirq.ControlledGate(
self,
num_controls=num_controls,
control_values=control_values,
control_qid_shape=control_qid_shape,
)
ctrl_spec = CtrlSpec.from_cirq_cv(controlled_gate.control_values)
return BloqAsCirqGate(Controlled(self, ctrl_spec))
) -> 'GateWithRegisters':
Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, I think this needs a big docstring that describes the two modes of operation


yield self.ctrl_state_prep.on(*phase_qubits)
for i, qbit in enumerate(phase_qubits[::-1]):
yield cirq.pow(unitary_op.controlled_by(qbit), 2**i)
yield Power(self.unitary.controlled(), 2**i).on(qbit, *target_qubits)
# yield cirq.pow(unitary_op.controlled_by(qbit), 2**i)
Copy link
Collaborator

Choose a reason for hiding this comment

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

remove old code

Copy link
Collaborator Author

@tanujkhattar tanujkhattar Apr 18, 2024

Choose a reason for hiding this comment

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

Reverted the file so now there are no changes and the original implementation uses bloqs instead of cirq gates. See added tests in gate_with_registers_test.py for clarity.

num_inverse_applications -= 1
else:
# apply C[0]-U
yield self.U.on_registers(**quregs).controlled_by(signal_qubit, control_values=[0])
yield self.U.controlled(control_values=[0]).on(signal_qubit, *flat_qubits_for_u)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we default to using qualtran style controls CtrlSpec(cvs=0) instead of cirq style control_values=[0]?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Reverted this change because self.U.on_registers(**quregs).controlled_by(signal_qubit) now uses the Controlled bloq instead of cirq gate. The op.controlled_by still expects the cirq-style API since we don't have a custom GWROperation where we can override the controlled_by to expect the bloq-style API as well. But I think it's fine right now to do things this way

@tanujkhattar
Copy link
Collaborator Author

@mpharrigan I'm ready to merge this but feel free to take another look since I've made some minor upgrades. Using gwr.controlled().on_registers() and gwr.on_registers().controlled_by are now equivalent and both result GateOperation(Controlled(gwr)) where the wrapped gate is a the Controlled bloq.

Tl;Dr - the discrepancy in usage from a users perspective that I described in Qualtran sync today is now gone and users don't need to worry about the intricacies when using the Cirq or Bloq style APIs.

@mpharrigan
Copy link
Collaborator

where is the logic for controlled_by?

@tanujkhattar
Copy link
Collaborator Author

where is the logic for controlled_by?

No override is needed because of the way Cirq and Qualtran works. GateOperation.controlled_by delegates to Gate.controlled() by default, which now uses GateWithRegisters.controlled() which returns a Controlled(GateWithRegisters) which is a cirq gate and gets wrapped in the GateOperation(controlled_bloq).

This was true today morning as well, I just realized it when making these changes and thus added tests to gate_with_registers_test.py to make this concrete.

@tanujkhattar
Copy link
Collaborator Author

Merging this now

@tanujkhattar tanujkhattar merged commit 7bf19b3 into quantumlib:main Apr 19, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants