Skip to content

Implement pyqrack interpreter methods for squin dialect #207

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

Merged
merged 49 commits into from
May 5, 2025

Conversation

david-pl
Copy link
Collaborator

@david-pl david-pl commented Apr 25, 2025

Closes #185

Copy link
Contributor

github-actions bot commented Apr 25, 2025

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
6844 5887 86% 0% 🟢

New Files

File Coverage Status
src/bloqade/pyqrack/squin/_init_.py 100% 🟢
src/bloqade/pyqrack/squin/op.py 100% 🟢
src/bloqade/pyqrack/squin/qubit.py 98% 🟢
src/bloqade/pyqrack/squin/runtime.py 70% 🟢
TOTAL 92% 🟢

Modified Files

File Coverage Status
src/bloqade/pyqrack/_init_.py 100% 🟢
src/bloqade/pyqrack/reg.py 95% 🟢
src/bloqade/squin/analysis/nsites/impls.py 93% 🟢
src/bloqade/squin/op/_init_.py 88% 🟢
src/bloqade/squin/op/stmts.py 99% 🟢
src/bloqade/squin/qubit.py 92% 🟢
src/bloqade/squin/wire.py 100% 🟢
TOTAL 95% 🟢

updated for commit: 0b93fd0 by action🐍

Copy link

codecov bot commented Apr 25, 2025

Codecov Report

Attention: Patch coverage is 79.73568% with 92 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/bloqade/pyqrack/squin/runtime.py 69.69% 90 Missing ⚠️
src/bloqade/pyqrack/squin/qubit.py 98.07% 1 Missing ⚠️
src/bloqade/squin/op/__init__.py 92.30% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@david-pl
Copy link
Collaborator Author

So, the implementation here is somewhat complete. However, I'm still struggling with the wire dialect.

@weinbe58 @johnzl-777 how do you even create a Wire right now? I tried this:

@squin.wired
def main():
    q = squin.qubit.new(1)
    w = squin.wire.unwrap(q[0])
    x = squin.op.x()
    squin.wire.apply(x, w)
    return w

But, of course, squin.qubit.new isn't supported by the wired dialect. Is there something like a NewWire statement that is missing?

@david-pl david-pl force-pushed the david/185-pyqrack-squin branch from e6d79f4 to 0cf3d39 Compare April 29, 2025 11:13
@weinbe58
Copy link
Member

weinbe58 commented May 2, 2025

@david-pl I can handle the measurement stuff later on thanks you!

Copy link
Member

@weinbe58 weinbe58 left a comment

Choose a reason for hiding this comment

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

I have some suggestions mostly for the runtime.

Comment on lines 21 to 26
def broadcast_apply(self, qubits: ilist.IList[PyQrackQubit, Any], **kwargs) -> None:
for qbit in qubits:
if not qbit.is_active():
continue

self.apply(qbit, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

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

What if the operator accepts two qubits? you should batch the qubits based on the operator size.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean something like broadcast(CX, ...)? How would the wrapper for this look, i.e. what's the second argument to broadcast(CX, ...)? Not sure about the syntax here.

Comment on lines 155 to 164
def apply(self, *qubits: PyQrackQubit, adjoint: bool = False) -> None:
self.op.apply(*qubits, adjoint=adjoint)

target = qubits[-1]

# NOTE: just factor * eye(2)
m = self.mat(adjoint)

# TODO: output seems to always be normalized -- no-op?
target.sim_reg.mtrx(m, target.addr)
Copy link
Member

Choose a reason for hiding this comment

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

if this is true we might want to error instead of allowing things to run. If we want to do something like rot(scale * op) we can rewrite this to a standard rotation statement.

cc: @Roger-luo

Comment on lines 14 to 27
@_wraps(stmts.Mult)
def mult(lhs: types.Op, rhs: types.Op) -> types.Op: ...


@_wraps(stmts.Scale)
def scale(op: types.Op, factor: complex) -> types.Op: ...


@_wraps(stmts.Adjoint)
def adjoint(op: types.Op, *, is_unitary: bool = False) -> types.Op: ...
def adjoint(op: types.Op) -> types.Op: ...


@_wraps(stmts.Control)
def control(op: types.Op, *, n_controls: int, is_unitary: bool = False) -> types.Op: ...
def control(op: types.Op, *, n_controls: int) -> types.Op: ...
Copy link
Member

Choose a reason for hiding this comment

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

Remove wrappers later when syntax sugar is implemented.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just out of curiosity: why? What's the hurt in keeping the wrapper?

Copy link
Member

Choose a reason for hiding this comment

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

what syntax sugar are we proposing here for control?

Comment on lines +56 to +61
w: PyQrackWire = frame.get(stmt.wire)
qbit = w.qubit
res: int = qbit.sim_reg.m(qbit.addr)
qbit.sim_reg.force_m(qbit.addr, False)
new_w = PyQrackWire(qbit)
return (new_w, res)
Copy link
Member

Choose a reason for hiding this comment

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

I need to make sure this will work in general. I know using force_m can create some issues. Lets leave it for now but I will revisit how to properly reset a qubit with pyqrack.

@david-pl david-pl requested a review from weinbe58 May 2, 2025 20:48
@david-pl
Copy link
Collaborator Author

david-pl commented May 3, 2025

Since this PR is getting kind of long, I'm gonna summarize some of the discussions again here. For the most part, I think there was a bit of confusion as I originally only considered a single target for controlled gates and broadcast (which was wrong). That has been fixed.

Here's an overview of the semantics we discussed in some comments and how I chose to implement them for now:

  • In a Control you can have arbitrarily many controls and targets.
  • If any of the controls are inactive, no gate is applied to any target.
  • If the number of controls + the size of the applied operator doesn't match the number of qubits, you get a RuntimeError.
  • If any of the targets are inactive, they are skipped. The gate is still applied to the remaining, active targets.
  • The number of targets must match the size (I called the property n_qubits) of the operator that is to be applied to the targets (not just for controls, obviously).
  • You can broadcast operators of arbitrary size. It's applied to the list of qubits in batches that match the size of the operator from left to right.
  • That means, you can also broadcast controls, where e.g. CX is then applied to a list of qubits, where each pair is treated as a control and target, respectively. I'm not a huge fan of this since I think it might still be confusing, but I see the value in it.

Pinging @cduck , @Roger-luo and @weinbe58 here. Do you all agree with this?

Also, ready for another round of review from my side!

@Roger-luo
Copy link
Member

Yes, I agree, I think these are good summarize of the semantics, worth putting them in the docstring of the wrappers later.

just to be pedantic here

The number of targets must match the size (I called the property n_qubits) of the operator that is to be applied to the targets (not just for controls, obviously).

we should call it n_sites because it is not necessarily n_qubits for operators.

Comment on lines 64 to 82
@interp.impl(qubit.MeasureAny)
def measure_any(
self, interp: PyQrackInterpreter, frame: interp.Frame, stmt: qubit.MeasureAny
):
input = frame.get(stmt.input)

if isinstance(input, PyQrackQubit) and input.is_active():
result = self._measure_qubit
elif isinstance(input, ilist.IList):
result = []
for qbit in input:
if not isinstance(qbit, PyQrackQubit):
raise InterpreterError(f"Cannot measure {type(qbit).__name__}")

result.append(self._measure_qubit(qbit))
else:
raise InterpreterError(f"Cannot measure {type(input).__name__}")

return (result,)
Copy link
Member

Choose a reason for hiding this comment

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

This statement is only for syntax sugar not for runtime. can you remove this?

@david-pl david-pl requested a review from weinbe58 May 5, 2025 17:34
Copy link
Member

@weinbe58 weinbe58 left a comment

Choose a reason for hiding this comment

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

LGTM, if we run into any issues we can polish them later.

@weinbe58 weinbe58 merged commit cdf3280 into main May 5, 2025
10 of 11 checks passed
@weinbe58 weinbe58 deleted the david/185-pyqrack-squin branch May 5, 2025 19:06
Roger-luo pushed a commit that referenced this pull request May 5, 2025
Seemed sufficiently different from #207, so I created a new branch for
it.

@Roger-luo two things:
* I'm not sure whether this should be a `Fixpoint`.
* Should this also be part of the `wire` kernel?

---------

Co-authored-by: Phillip Weinberg <[email protected]>
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.

PyQrack support for squin
4 participants