Skip to content

Add a phased classical action for SelectedMajoranaFermion#1778

Open
maxglick wants to merge 14 commits intoquantumlib:mainfrom
maxglick:selected-majorana-fermion-1699
Open

Add a phased classical action for SelectedMajoranaFermion#1778
maxglick wants to merge 14 commits intoquantumlib:mainfrom
maxglick:selected-majorana-fermion-1699

Conversation

@maxglick
Copy link
Copy Markdown
Contributor

@maxglick maxglick commented Dec 5, 2025

Add a phased classical action for SelectedMajoranaFermion.
The classical action only exists for some choices of target_gate, and we assume specifically target_gate=cirq.X (and also that there is only 1 control register and 1 selection register).

Fixes #1699

Comment on lines +144 to +145
if len(self.control_registers) > 1 or len(self.selection_registers) > 1:
return NotImplemented
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this restriction necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure - it is hard for me to understand what this gate does in the general case. Is my understanding in #1699 (comment) correct?

Comment on lines +142 to +143
if self.target_gate != cirq.X:
return NotImplemented
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In combination with basis_state_phase, this should be able to support Z as well. Z imparts a phase (and leaves the computational basis states unaffected)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have now added Z.

Comment on lines +152 to +153
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
target = (2 ** (max_selection - selection)) ^ target
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can you add a comment describing how this logic works

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you be even more explicit with how the bit twiddling operations correspond to the promised action of the subroutine

"flip the selection-th bit in target. the selection-th bit is addressed with the unsigned integer 2^(N - i) in our big endian convention"

or something like that

@mpharrigan
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements classical simulation support for the SelectedMajoranaFermion bloq by adding on_classical_vals and basis_state_phase methods, along with a new testing utility assert_consistent_phased_classical_action to verify these actions against bloq decompositions. The review feedback identifies several potential runtime issues, including IndexError risks when control or selection registers are empty, and TypeError or ValueError risks during bitwise operations if the selection index exceeds the maximum allowed value. Suggestions were provided to enforce strict register counts and add bounds checking for selection indices.

Comment on lines +144 to +145
if len(self.control_registers) > 1 or len(self.selection_registers) > 1:
return NotImplemented
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The current check len(self.control_registers) > 1 does not account for the case where there are zero control registers. If control_regs is empty, accessing self.control_registers[0] on line 146 will raise an IndexError. Given the implementation assumes exactly one control and one selection register, it's safer to check for != 1.

Suggested change
if len(self.control_registers) > 1 or len(self.selection_registers) > 1:
return NotImplemented
if len(self.control_registers) != 1 or len(self.selection_registers) != 1:
return NotImplemented

Comment on lines +153 to +155
if control and self.target_gate == cirq.X:
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
target = (2 ** (max_selection - selection)) ^ target
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If selection exceeds max_selection, max_selection - selection becomes negative, causing 2 ** (max_selection - selection) to return a float. Bitwise XOR (^) with a float will then raise a TypeError. We should ensure selection <= max_selection before applying the bit flip. If the selection is out of range, the gate should act as identity.

Suggested change
if control and self.target_gate == cirq.X:
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
target = (2 ** (max_selection - selection)) ^ target
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
if control and self.target_gate == cirq.X and selection <= max_selection:
target = (2 ** (max_selection - selection)) ^ target

Comment on lines +163 to +164
if len(self.control_registers) > 1 or len(self.selection_registers) > 1:
return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Similar to on_classical_vals, this check should ensure exactly one control register is present to avoid an IndexError on line 165 when control_regs is empty.

Suggested change
if len(self.control_registers) > 1 or len(self.selection_registers) > 1:
return None
if len(self.control_registers) != 1 or len(self.selection_registers) != 1:
return None

Comment on lines +170 to +176
if control:
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
if self.target_gate == cirq.X:
num_phases = (target >> (max_selection - selection + 1)).bit_count()
else:
num_phases = (target >> (max_selection - selection)).bit_count()
return 1 if (num_phases % 2) == 0 else -1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If selection exceeds max_selection, the bit shift count max_selection - selection can become negative, leading to a ValueError. Additionally, if selection == max_selection + 1 and target_gate == cirq.X, the shift count is 0, which would incorrectly count all bits in target. The phase should only be calculated when selection <= max_selection.

Suggested change
if control:
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
if self.target_gate == cirq.X:
num_phases = (target >> (max_selection - selection + 1)).bit_count()
else:
num_phases = (target >> (max_selection - selection)).bit_count()
return 1 if (num_phases % 2) == 0 else -1
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
if control and selection <= max_selection:
if self.target_gate == cirq.X:
num_phases = (target >> (max_selection - selection + 1)).bit_count()
else:
num_phases = (target >> (max_selection - selection)).bit_count()
return 1 if (num_phases % 2) == 0 else -1
return 1

if control:
max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1
if self.target_gate == cirq.X:
num_phases = (target >> (max_selection - selection + 1)).bit_count()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

explain how this works

if self.target_gate == cirq.X:
num_phases = (target >> (max_selection - selection + 1)).bit_count()
else:
num_phases = (target >> (max_selection - selection)).bit_count()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

explain how this works and why it's different from the cirq.X case. Maybe be more defensive and put this behind an elif in case we add more in the future e.g. cirq.Y

Copy link
Copy Markdown
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.

I added some requests for more comments about the bit twiddling. Ideally, someone should be able to read this method and immediately square the circle with the dirac notation definition given in the class docstring.

Re: multiple controls and multiple selection registers: further comments below, but we can do them in a follow up.

Re: multiple controls: they're implicitly "anded together"

Re: multiple selection registers: you can see in the definition of the target register that we're packing an n-dimensional array of bits into the one QAny(prod(*shape)) register. Whereas the current implementation is doing target[i] ^= 1 multiple selection registers would do target[i,j,k] ^= 1

This ties into my review comments where it's not immediately obvious that we're doing target[i]^=1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Write classical simulation tests for SelectedMajoranaFermion

2 participants