Skip to content

feat: add adaptive Rz synthesis for the Steane code - #406

Draft
sjdilkes wants to merge 1 commit into
mainfrom
feat/adaptive-steane-rz
Draft

sjdilkes wants to merge 1 commit into
mainfrom
feat/adaptive-steane-rz

Conversation

@sjdilkes

Copy link
Copy Markdown
Collaborator

Adds adaptive Rz synthesis to the Steane encoder, enabled with SteaneBuilder().with_adaptive_rz(...). This supports small, arbitrary rotation angles through ordinary Guppy rz gates or direct logical adaptive_rz calls to a Steane code blck.

Each round applies physical rotations and checks the error syndromes, adapting the next coherent angle induced depending on the syndrome measured.

AdaptiveRzConf lets users set:

  • The angle tolerance and maximum number of rounds.
  • The expected physical dephasing and a logical dephasing budget for each rotation.
  • Whether exceeding that budget stops execution or records adaptive_rz_dephasing_limit_hit and continues. The maximum round limit still stops execution in either mode.

The implementation draws on:

The noise model assumes independent Z errors and ideal Clifford gates and syndrome measurements. Setting dephasing does not add simulation noise or similar, but just informs the controller what to expect to try to account for accidental overrotation.

@sjdilkes
sjdilkes requested review from a team and hsemenenko September 16, 2026 14:12

@hsemenenko hsemenenko left a comment

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.

Here are some initial comments. I think we need some discussion about this fits into an architecture, especially if we expect other rz implementations to be added in the future.

I haven't reviewed the tests in detail.

/// S dagger gate.
sdg,
/// Rz gate with angle in radians.
adaptive_rz,

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.

I'm not convinced that the HUGR op should be called adaptive_rz. Could it be a more generic rz op in the HUGR extension, that is then implemented with the adaptive routine? What I don't want is requiring a new rz op for every different implementation.

Comment on lines +65 to +73
@custom_type(
rotation().get_type("rotation").instantiate([]), copyable=True, droppable=True
)
class _Rotation:
"""TKET rotation argument used by the Rz encoder."""

@hugr_op(lambda ty, _inst, _ctx: ExtOp(rotation().get_op("to_halfturns"), ty, []))
@no_type_check
def halfturns(self: "_Rotation") -> float: ...

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.

See #314

emit ``adaptive_rz_dephasing_limit_hit=True`` once per affected call.
The round limit still stops execution with an error.

These settings do not add simulator noise.

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.

This seems like an odd comment and not required.

"""Settings for adaptive Steane rotations.

Attributes:
tolerance: Allowed residual angle in radians.

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.

epsilon seems more appropriate than tolerance

if -tolerance <= remaining and remaining <= tolerance:
return total_dephasing
if rounds == max_rounds:
panic("Adaptive Rz exceeded max_rounds")

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 panic right here? I think the difference between exit and panic is that exit will continue to the next shot, while panic will exit and not continue to future shots.

Comment on lines +675 to +677
# NaN and infinity both make this subtraction NaN.
if phase - phase != 0.0:
panic("Adaptive Rz requires a finite angle")

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.

I don't think this is necessary in Guppy.


@guppy
@no_type_check
def rotate_and_correct_rz(blk: LogicalBlock[7], physical_angle: float) -> bool:

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.

Could this be made private? It's only used here and in tests. Do we expect users to need it?

Comment on lines +660 to +666
blk: LogicalBlock[7],
phase: float,
tolerance: float,
max_rounds: int,
dephasing: float,
max_dephasing: float,
abort_on_dephasing: bool,

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.

Which of these arguments will be variable at runtime, and which will be statically known at compile time? We should consider marking @ comptime for args that will be known at compile time to reduce runtime computation.

if abort_on_dephasing:
panic("Adaptive Rz exceeded max_dephasing")
if not dephasing_limit_hit:
output("adaptive_rz_dephasing_limit_hit", True)

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.

Primitives shouldn't include output.

Suggested change
output("adaptive_rz_dephasing_limit_hit", True)



@dataclass(frozen=True)
class AdaptiveRzConf:

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.

If we expect other Rz implementations to be added in the future, we could consider having a more general RzConf instead.

Comment thread src/guppyft/_math.py
lo = -float(pi) / 2.0
hi = float(pi) / 2.0
ratio = y / x
for _ in range(52):

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.

Instead of always repeating 52 times, this could be made more efficient by iterating until two successive values of mid are close enough (within 1e-15 or so).

However, copying the implementation from go.dev (as we do for tan) would be much more efficient still.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants