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

Observables #213

Merged
merged 37 commits into from
May 28, 2024
Merged

Observables #213

merged 37 commits into from
May 28, 2024

Conversation

Gistbatch
Copy link
Contributor

closes: #141

I started to develop this, and I have some open questions:

  • I've implemented a test for the RawRectangleTemplate. @nelimee, can you clarify if this is the intended behavior?
  • What should the scope of this Issue/PR be? Only build this for the available templates, including moves and so on.
  • How do we define midlines for uneven-sized templates, e.g., where the number of qubit rows is even?

Todos:

  • Add all templates
  • Write tests, including scaling
  • Figure out edge cases: Moving, uneven ...
  • Documentation

@afowler
Copy link

afowler commented Apr 22, 2024 via email

@nelimee
Copy link
Contributor

nelimee commented Apr 22, 2024

I've implemented a test for the RawRectangleTemplate. @nelimee, can you clarify if this is the intended behavior?

The output you fixed is dependent on the actual Plaquette implementation. What if the origin (i.e., (0, 0) point) was on the top-left of the Plaquette? Then both the output coordinates would change.
I cannot remember if we completely ruled out that case though.

In any case, I think this method needs the Plaquette instances to be able to function correctly and generically.

What should the scope of this Issue/PR be? Only build this for the available templates, including moves and so on.

For this PR, only build this for the available Templates. Moving will come after.

How do we define midlines for uneven-sized templates, e.g., where the number of qubit rows is even?

As Austin said, I would rule out that case. To do that, I would change the default_observable_qubits return type to ty.Sequence[tuple[cirq.GridQubit, int]] | None, returning None being a signal that the midline could not be computed.

Also, I think the implementations of default_observable_qubits might be factorisable. But let's have something working for the moment.

The rationale is to get the plaquette indices from the template and then compute the positions according to the plaquettes.
This construction is similar to the circuit generation
@Gistbatch
Copy link
Contributor Author

@nelimee, I've updated this; let me know what you think.

If the midline is supposed to depend on the plaquettes, we need this information, so I built observable_qubits_from_template similar to the generate_circuit function. The template now only returns the row and column indices of the templates that have the observable qubits, by convention, above/left of the midline.

@nelimee
Copy link
Contributor

nelimee commented Apr 26, 2024

If the midline is supposed to depend on the plaquettes

I do not get how you can generate correct results without any idea of the plaquette used. For the moment, all the plaquettes we use have the support

1   2
  5 
3   4

with:

  1. cirq.GridQubit(-1, -1)
  2. cirq.GridQubit( 1, -1)
  3. cirq.GridQubit(-1, 1)
  4. cirq.GridQubit( 1, 1)
  5. cirq.GridQubit( 0, 0)

but (as far as I remember) we never explicitly said that was the only possible default. This means that a plaquette with the same support but with

  1. cirq.GridQubit(0, 0)
  2. cirq.GridQubit(2, 0)
  3. cirq.GridQubit(0, 2)
  4. cirq.GridQubit(2, 2)
  5. cirq.GridQubit(1, 1)

is possible.
If that is the case, then we need to have access to that information, else the midline qubits will be completely wrong. I am missing something here?

I am making a few comments on the code before commenting again on the overall approach.

Copy link
Contributor

@nelimee nelimee left a comment

Choose a reason for hiding this comment

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

I did not check thoroughly the code validity for the moment, but your tests help on that.
For the Template.get_midline_plaquettes method, I completely agree with the fact that we need a convention such as the one you used (plaquettes left of the vertical midline or above the horizontal midline). The two branches (depending on the value of the horizontal boolean) are very similar, maybe they can be "merged" in one more generic version.


def observable_qubits_from_template(
template: Template,
plaquettes: list[Plaquette] | dict[int, Plaquette],
Copy link
Contributor

Choose a reason for hiding this comment

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

This will be less of an issue with #203 merged, but maybe

Suggested change
plaquettes: list[Plaquette] | dict[int, Plaquette],
plaquettes: Sequence[Plaquette] | Mapping[int, Plaquette],

to have the covariance of the typing annotations?

def observable_qubits_from_template(
template: Template,
plaquettes: list[Plaquette] | dict[int, Plaquette],
horizontal: bool = True,
Copy link
Contributor

Choose a reason for hiding this comment

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

Even though I am not always doing it, in general, I prefer to use enumerations with explicit names for boolean parameters. I find that easier to read when calling the function/method. Doing so would require to introduce something along the lines of

class Orientation(Enum):
    HORIZONTAL = auto()
    VERTICAL = auto()

and then the function can be used as

observable_qubits_from_template(template, plaquettes, Orientation.HORIZONTAL)

this is really minor though.

Comment on lines 367 to 371
template (Template): The template to get the default observable qubits from.
plaquettes (list[Plaquette] | dict[int, Plaquette]): The plaquettes to use to get the
acurate positions of the observable qubits.
horizontal (bool, optional): Whether to get the observable qubits from
the horizontal or vertical midline. Defaults to True -> Horizontal.
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure the type annotations in the docstring are needed. To check in #211.
If not needed because Sphinx is able to read directly from the annotations in code, then we should remove them in the docstring as we will likely eventually forget to change the docstring and that will lead to outdated comments.

Comment on lines 399 to 402
try:
midline_indices = template.get_midline_plaquettes(horizontal)
except TQECException:
return None
Copy link
Contributor

Choose a reason for hiding this comment

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

I cannot remember what is the norm in Python, but I try to avoid exceptions for control-flow handling. Seeing it implemented, I think I do not agree with myself anymore. I wrote that

As Austin said, I would rule out that case. To do that, I would change the default_observable_qubits return type to ty.Sequence[tuple[cirq.GridQubit, int]] | None, returning None being a signal that the midline could not be computed.

but I now think that the TQECException should not be caught here but should rather go up in the stack. We can then simply document that the function/method raise TQECException when the Template dimensions are not compatible. Sorry for misleading you in the first place, this is my mistake.

Comment on lines 405 to 406
for index in midline_indices:
row_index, column_index = index
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well be written in one line:

Suggested change
for index in midline_indices:
row_index, column_index = index
for (row_index, column_index) in midline_indices:

Comment on lines 425 to 427
max_index = max(
plaquette.qubits.data_qubits, key=lambda q: q.position.y
).position.y
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
max_index = max(
plaquette.qubits.data_qubits, key=lambda q: q.position.y
).position.y
max_index = max(q.position.y for q in plaquette.qubits.data_qubits)

should be equivalent here. I did not tested the above code though.

TQECException: If the plaquettes are provided as a dictionary and 0 is in the keys.

Returns:
Sequence[tuple[cirq.GridQubit, int]] | None: The observable qubits and their offsets.
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure the type annotations in the docstring are needed. To check in #211.

and to the left of the midline for the vertical case.

Args:
horizontal (bool, optional): Horizontal or vertical qubits. Defaults to True.
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure the type annotations in the docstring are needed. To check in #211.

horizontal (bool, optional): Horizontal or vertical qubits. Defaults to True.

Returns:
list[tuple[int,int]]: The sequence of qubits and offsets.
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure the type annotations in the docstring are needed. To check in #211.

Comment on lines 214 to 215
indices = self._indices[midline]
return [(midline, column ) for column, _ in enumerate(indices)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
indices = self._indices[midline]
return [(midline, column ) for column, _ in enumerate(indices)]
return [(midline, column) for column in range(len(self._indices[midline]))]

Copy link
Contributor

Choose a reason for hiding this comment

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

Or even

Suggested change
indices = self._indices[midline]
return [(midline, column ) for column, _ in enumerate(indices)]
return [(midline, column) for column in range(self.shape.x)]

@afowler
Copy link

afowler commented Apr 26, 2024 via email

@Gistbatch
Copy link
Contributor Author

Using their coordinates, I take the qubits furthest to the right (bottom). This works only for rectangular shapes but has the advantage that this works independently of the orientation.
As an alternative, we could implement this as the default behavior in the plaquette, which can be overwritten.
I'll clean up the code once we've decided on the mechanism and implement the rest of the templates.

@nelimee
Copy link
Contributor

nelimee commented Apr 29, 2024

then the midline calculation should depend on the shape of the plaquette, rather than the precise labeling of the qubits in the plaquette.

I agree, the issue here is that, in the current state of the code, the positions of the plaquette qubits can change all the positions. Basically, the positioning algorithm starts by positioning the origin of the top-left-most plaquette on the qubit (0, 0), all the other qubits position being ultimately determined relative to that position. That mean that in the 2 cases I presented above, all the qubits of the template are shifted by (1, 1). And we need exact coordinates for the midline.

This limitation should be quite easy to remove though, using the top-left corner of the plaquette instead of the origin in the first step would solve it on the current use-case.
Finally, this is very much an implementation detail for the moment, but it has implications on what the user see and experience, so we might want to change the current default anyway.

Using their coordinates, I take the qubits furthest to the right (bottom). This works only for rectangular shapes but has the advantage that this works independently of the orientation.
As an alternative, we could implement this as the default behavior in the plaquette, which can be overwritten.

Both approaches seems fine for me. The second one is likely more generic, but we are only interested in rectangular plaquette for the moment so this is not really a problem to implement a simpler, less generic, version.

@Gistbatch
Copy link
Contributor Author

@nelimee, I'm confused about why the test keeps failing. Do you have any idea why this is the case? I can run it locally without this problem, and I'm not sure how to fix this as it seems it works completely fine with circuit_test which has a similar import structure.

@nelimee
Copy link
Contributor

nelimee commented May 7, 2024

@nelimee, I'm confused about why the test keeps failing. Do you have any idea why this is the case? I can run it locally without this problem, and I'm not sure how to fix this as it seems it works completely fine with circuit_test which has a similar import structure.

This might be due to #198 being merged in main. I am not sure what https://github.com/actions/checkout is checking out, but it might be worth updating this branch with the latest changes from main and see if that resolves the issue.

Copy link
Contributor

@nelimee nelimee left a comment

Choose a reason for hiding this comment

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

One suggestion to fix at least one test.

The other errors are expected as not all Template subclasses implement the get_midline_plaquettes method, which makes them abstract and non-instantiable

@@ -0,0 +1,96 @@
from typing import Any, Mapping, Sequence
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
from typing import Any, Mapping, Sequence
from __future__ import annotations
from typing import Any, Mapping, Sequence

fixes some test issues.

@Gistbatch
Copy link
Contributor Author

I've now implemented the midline definition for the existing template instances.
For the ComposedTemplate and its children, I'm wondering what the default behavior should be.
It seems impossible to know which qubits are on the midline without prior knowledge.
For example, the QubitSquareTemplate has a midline, but none of the associated template midlines coincide.
We could hard code this, but figuring out which templates to look at for qubits seems arbitrary and complex.
@afowler, @nelimee any suggestions?

@nelimee
Copy link
Contributor

nelimee commented May 7, 2024

It seems at least hard, maybe impossible (there are likely instantiations of the CompositeTemplate that do not represent a valid code, and so do not have observables), to me too.
I will review the code as it is right now, let's wait for @afowler answer.

Copy link
Contributor

@nelimee nelimee left a comment

Choose a reason for hiding this comment

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

In the future it might be better to define a method in Plaquette to get the "extremum" qubits in a provided direction. This would be even more general than the _get_edge_qubits and should be quite simple to implement using geometry. Also it would pave the way for non-square plaquettes (for which the edge qubits might not only be horizontal or vertical, say for example for an octagonal plaquette).

You also should document somewhere the convention you used w.r.t which plaquette is returned by the get_midline_plaquettes method (if horizontal, the plaquette on the left or on the right? same question for vertical) and so on which edge the qubits are taken from.

You should document somewhere that the qubits returned by the get_midline_plaquettes method are in the (row, column) format. Even better, maybe create or re-use a structure with named fields for that?


def observable_qubits_from_template(
template: Template,
plaquettes: Union[Sequence[Plaquette], Mapping[int, Plaquette]],
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be

Suggested change
plaquettes: Union[Sequence[Plaquette], Mapping[int, Plaquette]],
plaquettes: Sequence[Plaquette] | Mapping[int, Plaquette],

if we also include

from __future__ import annotations

This version (with |) is used across the code base, it might be better to stay consistent.

Comment on lines 20 to 24
template (Template): The template to get the default observable qubits from.
plaquettes (list[Plaquette] | dict[int, Plaquette]): The plaquettes to use to get the
acurate positions of the observable qubits.
horizontal (bool, optional): Whether to get the observable qubits from
the horizontal or vertical midline. Defaults to True -> Horizontal.
Copy link
Contributor

Choose a reason for hiding this comment

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

As for the previous comments, I am pretty sure types do not have to appear in the docstrings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't updated docs and tests yet, I'll do this once functionality is fixed

(qubit.to_grid_qubit() + offset, 0)
for qubit in _get_edge_qubits(plaquette, horizontal)
]
return list(set(observable_qubits))
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe sorting the qubits before returning them would help human readability in the final generated Stim file?

Suggested change
return list(set(observable_qubits))
return sorted(set(observable_qubits))

else qubit.position.x
)

max_index = max_index = max(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
max_index = max_index = max(
max_index = max(

Comment on lines 29 to 36
result = [
(cirq.GridQubit(3, -1), 0),
(cirq.GridQubit(3, 1), 0),
(cirq.GridQubit(3, 3), 0),
(cirq.GridQubit(3, 5), 0),
(cirq.GridQubit(3, 7), 0),
]
assert sorted(obs, key=lambda t: t[0].col == result)
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure to understand that part. If I understand it correctly, you are sorting obs with a comparator that I do not understand.
Maybe

Suggested change
result = [
(cirq.GridQubit(3, -1), 0),
(cirq.GridQubit(3, 1), 0),
(cirq.GridQubit(3, 3), 0),
(cirq.GridQubit(3, 5), 0),
(cirq.GridQubit(3, 7), 0),
]
assert sorted(obs, key=lambda t: t[0].col == result)
result = [
(cirq.GridQubit(3, -1), 0),
(cirq.GridQubit(3, 1), 0),
(cirq.GridQubit(3, 3), 0),
(cirq.GridQubit(3, 5), 0),
(cirq.GridQubit(3, 7), 0),
]
assert sorted(obs, key=lambda t: t[0].col) == result

is what you wanted?

Comment on lines 175 to 188

def get_midline_plaquettes(
self, horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL
) -> list[tuple[int, int]]:
midline_shape, iteration_shape = self.shape.x, self.shape.y
if midline_shape % 2 == 1:
raise TQECException(
"Midline is not defined for odd "
+ f"{'height' if horizontal == TemplateOrientation.HORIZONTAL else 'width'}."
)
midline = midline_shape // 2 - 1
if horizontal == TemplateOrientation.VERTICAL:
return [(midline, column) for column in range(iteration_shape)]
return [(row, midline) for row in range(iteration_shape)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this one already implemented by the AlternatingRectangleTemplate.get_midline_plaquettes? Why aren't you swapping midline_shape and iteration_shape depending on the orientation here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

since this is a square midline_shape == iteration_shape, and this class inherits from Template so we can't inherit the function.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, I missed that, thanks for the answer :)

Comment on lines 131 to 135
if (
horizontal == TemplateOrientation.VERTICAL
and template.shape.x == midline_shape
):
template.get_midline_plaquettes(horizontal)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (
horizontal == TemplateOrientation.VERTICAL
and template.shape.x == midline_shape
):
template.get_midline_plaquettes(horizontal)
elif (
horizontal == TemplateOrientation.VERTICAL
and template.shape.x == midline_shape
):
return template.get_midline_plaquettes(horizontal)

@Gistbatch
Copy link
Contributor Author

I wrote tests for all the templates now.
I don't know what the intended behavior for the stacked template is.
Also, I left the CornerTemplate and StackedTemplate unimplemented.
I could hardcode the corner template similar to the qubit template if needed.
There isn't a good way of knowing what the midline is supposed to be for the stacked template, so I just left it.
The only option is to give all possible midlines to the user, and they have to piece them together themselves.

@Gistbatch Gistbatch marked this pull request as ready for review May 23, 2024 15:04
@Gistbatch
Copy link
Contributor Author

@nelimee I'll merge this once you approve

Copy link
Contributor

@nelimee nelimee left a comment

Choose a reason for hiding this comment

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

Some very minor comments, mostly about renaming horizontal to orientation.
There is also some duplication of code, but this can be addressed later.

A few other comments for the future of that feature:

  • I can see the _get_edge_qubits function being implemented in the PlaquetteQubits class. With a little bit of geometry, we could even implement something like PlaquetteQubits.extremum_qubits_in_direction(self, x: float, y: float). This can be thought about in a follow-up issue.
  • The more I think about the short-term and end goal of the community, the more I think I made a mistake by introducing StackedTemplate and ShiftedTemplate. These are 1. not really useful and 2. making a few PR more difficult than what they should be (this PR being a good example, another exploration work I have locally being another).

In the end, except the minor comments, I think this is a good first step to include automatic observable computation.

def observable_qubits_from_template(
template: Template,
plaquettes: Sequence[Plaquette] | Mapping[int, Plaquette],
horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL,
orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL,

Copy link
Contributor

Choose a reason for hiding this comment

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

WARNING: should also be changed in the function body if changed here.

Comment on lines 23 to 24
horizontal: Whether to get the observable qubits from
the horizontal or vertical midline. Defaults to True -> Horizontal.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
horizontal: Whether to get the observable qubits from
the horizontal or vertical midline. Defaults to True -> Horizontal.
orientation: Whether to get the observable qubits from
the horizontal or vertical midline. Defaults to horizontal.


def _get_edge_qubits(
plaquette: Plaquette,
horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL,
orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL,

Copy link
Contributor

Choose a reason for hiding this comment

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

WARNING: should also be changed in the function body if changed here.

@@ -90,6 +91,23 @@ def to_dict(self) -> dict[str, ty.Any]:
def expected_plaquettes_number(self) -> int:
return 2

def get_midline_plaquettes(
self, horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self, horizontal: TemplateOrientation = TemplateOrientation.HORIZONTAL
self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL

WARNING: should also be changed in the function body if changed here.

@Gistbatch Gistbatch merged commit 1a63daa into main May 28, 2024
6 checks passed
@Gistbatch Gistbatch deleted the observables branch May 28, 2024 09:34
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.

Specifying an Observable requires too much knowledge about implementation details
3 participants