Skip to content

add classical control / feedforward support - #403

Merged
jvdwetering merged 2 commits into
zxcalc:masterfrom
dlyongemallo:345-feedforward_classical_control
Mar 11, 2026
Merged

add classical control / feedforward support#403
jvdwetering merged 2 commits into
zxcalc:masterfrom
dlyongemallo:345-feedforward_classical_control

Conversation

@dlyongemallo

@dlyongemallo dlyongemallo commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #345. Relates to tqec/tqec#708.

  • parses OpenQASM 2/3 if (creg == val) gate args; statements (including braced blocks)
  • converts conditional single-qubit rotations to ZX-diagrams with symbolic boolean phases
  • extracts them back to ConditionalGate objects
  • Note: only Z-type vertices are extracted as conditional gates, X-type vertices are left as-is to avoid ambiguity with measurement outcome spiders.

@KabirDubey

KabirDubey commented Feb 27, 2026

Copy link
Copy Markdown

Thanks David!! I met with the group yesterday and I received some feedback.

Regarding gFlow: we have an internal algorithm for finding Pauli webs thanks to Tianyi Hao. This algorithm is faster and leverages our viewpoint that operations with measurements that are not explicitly specified with feedforward by the user are either T gates or using discards. Therefore, all of our input ZX graphs must have gFlow, and we do not need to check for this when finding Pauli webs. I think this means we can put any bugs I was encountering about finding gFlow using PyZX methods aside.

In case it's helpful, the following is a high-level description of the variety of circuits we want to be able to parse eventually:

  1. single qubit HTHTHT
    The test in your earlier PR explicitly specified T gate injection. This is excessive because the tqec compiler will insert the injection circuitry downstream from PyZX. I'm not sure about standards, but I think it makes sense to assume that a QASM user will only provide the gates needed at an algorithmic level, and leave the logical-circuit-implementation of those gates up to a compiler which can target a particular fault tolerant instruction set and hardware. What we would like is a single qubit circuit that maps a single-qubit non-Clifford rotation to "a line of alternating Hadamard edges and pure Z nodes, with each Z node connected to a T-node".

Apologies the earlier PR got merged before I had the chance to tell relay this to you. I should've noticed it earlier.

  1. the Steane encoding circuit, specified with resets and measurements

The example circuit we have been working with was inputted by hacking the QASM specification to PyZX into something which provided a ZX graph which we knew represented the Steane encoding through calculation by hand. There was a different hack involving padding the file that worked that I think J uses, but I can't remember it right now.

Regardless, it would be nice if a Steane encoding circuit intuitively specified with resets and measurements, like this one would now be parseable into something like:

steane_raw
  1. Some sort of logical repeat until success circuit that is not physical magic state cultivation.

There are some techniques, like fallback rotation synthesis, which rely on conditional operations at an algorithmic level, i.e. repeat-until-success. Representing these would be our most-likely usage of feedforward recognition from the pyZX parser, but we have a long ways to go to support conditional operations or large algorithms in our software architecture, so this is far from immediately needed.

Thanks again!

@jbolns

jbolns commented Feb 27, 2026

Copy link
Copy Markdown

There was a different hack involving padding the file that worked that I think J uses, but I can't remember it right now.

Not originally my trick. The ones that figured it out were @Zhaoyilunnn and Purva (can't tag, GH won't offer preview suggestion)

That said, it's a semi-general trick I've used in several circuits.

For the Steane in @KabirDubey 's comment:

# On graph that results after importing a Steane encoded as a QASM file observing previous parser limitations

# All in boundaries -> X
num_apply_state = g.num_inputs()
g.apply_state('0' * num_apply_state)

# Top 3 out boundaries -> X
g.apply_effect('000///////')

For a GHZ:

# On graph resulting from importing a n-qubit GHZ encoded as QASM observing previous parser limitations
# Apply states
num_apply_state = zx_graph_copy.num_inputs()
zx_graph_copy.apply_state("0" * num_apply_state)

# No post-selection needed (but here to signal where to do it if ever necessary)
zx_graph_copy.apply_effect("////////////////")

And for the two-qubit HTHTHT, to get rid of the now-famous lonely spider.

# With the graph one gets from parsing HTHTHT on top of #402. 
g.apply_state('/0/')
g.remove_vertex(4)
g.remove_vertex(5)
g.add_edge((1,6), edgetype=zx.EdgeType.HADAMARD)
join join_reduce

Ps. Nothing was actually done to the QASM file. We just designed the circuits without the type of operations that would result in a QASM line not supported by previous parser, then added the remainder of things manually once in PyZX.

@dlyongemallo
dlyongemallo force-pushed the 345-feedforward_classical_control branch from f53e8da to 741c371 Compare February 28, 2026 07:10
@dlyongemallo
dlyongemallo marked this pull request as ready for review February 28, 2026 07:10
Copilot AI review requested due to automatic review settings February 28, 2026 07:10
@dlyongemallo

Copy link
Copy Markdown
Contributor Author

@KabirDubey Thanks very much for the feedback. I have added a commit to this PR with tests based on your comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds OpenQASM classical control / feedforward support end-to-end (parse → ZX graph → circuit extraction) and expands regression coverage around resets/measurements and conditional operations, targeting tqec/pyzx interop and fixing #345.

Changes:

  • Parse OpenQASM 2/3 if (creg == val) ... statements, including braced blocks, into a new ConditionalGate.
  • Represent conditional single-qubit rotations in ZX graphs using boolean symbolic (Poly) phases and extract (Z-type) conditionals back from graphs.
  • Extend QASM serialization to declare classical registers needed by measurements and conditionals; add extensive tests for parsing/round-trips.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_qasm.py Adds new regression + round-trip tests for reset/measure circuits and conditional parsing/graph behavior.
pyzx/circuit/qasmparser.py Expands braced if blocks and parses if(reg==val) into ConditionalGate.
pyzx/circuit/graphparser.py Extracts Z-type conditional gates from Poly phases; falls back to symbolic phase gates otherwise.
pyzx/circuit/gates.py Introduces ConditionalGate; adjusts QASM output to attempt emitting symbolic phases; improves measurement symbolic naming.
pyzx/circuit/__init__.py Exports ConditionalGate and updates QASM docstrings + creg/bit declaration generation.
Comments suppressed due to low confidence (3)

pyzx/circuit/gates.py:290

  • Gate.to_qasm() now falls back to emitting symbolic (Poly) phases by interpolating self.phase into the QASM parameter string. The resulting text is not valid OpenQASM for this codebase (e.g., Poly.str uses the Unicode '⋅' operator, and QASMParser.parse_phase_arg cannot parse symbolic expressions at all), so a Circuit.to_qasm() output containing such phases will not round-trip and may confuse users. Consider raising a clear TypeError for symbolic phases (or implementing a dedicated, parseable serialization + matching parser support) instead of emitting an invalid QASM expression.
                try:
                    param = "({}*pi)".format(float(self.phase))
                except (TypeError, ValueError):
                    # Symbolic (Poly) phase — emit as-is.
                    param = "({}*pi)".format(self.phase)

pyzx/circuit/graphparser.py:204

  • graph_to_circuit() falls back to emitting raw ZPhase/XPhase gates with a symbolic Poly phase when it can't interpret the Poly as a ConditionalGate. Downstream code paths (e.g., Circuit.split_phase_gates(), Gate.to_qasm(), and QASMParser.parse_phase_arg) assume numeric phases and will either throw (Poly.denominator) or output non-parseable QASM. Consider either (a) ensuring symbolic-phase gates never reach those exporters/splitting routines, (b) introducing a dedicated gate type for symbolic phases, or (c) updating phase-splitting/export to explicitly handle Poly by leaving it unsplit and raising on QASM export with a clear message.
            if isinstance(phase, Poly):
                # Only extract Z-type vertices as conditional gates.
                # X-type vertices with boolean phases are ambiguous:
                # measurement outcomes (from Measurement.to_graph_symbolic_boolean)
                # produce the same X spider structure as conditional NOT/XPhase
                # gates, so we cannot distinguish them here.  Conditional
                # Z rotations are unambiguous because measurements never
                # create Z spiders with boolean phases.
                cgate = None
                if t == VertexType.Z:
                    cgate = _poly_phase_to_conditional_gate(phase, t, int(q))
                if cgate is not None:
                    c.add_gate(cgate)
                elif phase != 0:
                    gate_name = "ZPhase" if t == VertexType.Z else "XPhase"
                    c.add_gate(gate_name, q, phase=phase)
            elif phase!=0 and not split_phases:

pyzx/circuit/graphparser.py:83

  • _poly_phase_to_conditional_gate() uses exhaustive evaluation over all bit assignments (O(2^n)) and only emits a warning for register sizes > 16, but still proceeds to evaluate. For larger classical registers this can easily become a practical hang in graph_to_circuit(). Consider adding a hard cutoff / configurable limit that returns None (i.e., skip extraction) beyond some size, or an alternative non-exponential recognition strategy for the specific condition-polynomial form produced by ConditionalGate.
    if reg_size > 16:
        warnings.warn(
            "Conditional gate extraction is O(2^n) in register size; "
            "register '{}' has {} bits ({} evaluations).".format(
                reg_name, reg_size, 1 << reg_size))
    cond_value: Optional[int] = None
    inner_phase_value: Optional[Fraction] = None
    for val in range(1 << reg_size):
        var_map: Dict[Var, Union[float, complex, Fraction]] = {}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@dlyongemallo

Copy link
Copy Markdown
Contributor Author

@jvdwetering Is there any way to distinguish measurements from conditional X gates in a ZX graph?

For example, is there an order in which I can process the vertices such that the first X-spider I see with a boolean symbolic phase, say c[0], must be the measurement that introduced it, whereas any subsequent vertex whose phase references c[0] must be a conditional gate consuming it?

Alternatively, would it make sense to tag measurement vertices (similar to the ground vertex flag)?

@jvdwetering

Copy link
Copy Markdown
Collaborator

I don't there is, unless you tag things yourself. Maybe you can use the vertex_data field for that?

@dlyongemallo

Copy link
Copy Markdown
Contributor Author

@jvdwetering This PR is ready to be merged, pending your review/approval. I think we can leave the conditional X gate ambiguity / inability to round-trip for later.

@jvdwetering
jvdwetering merged commit 86f48ef into zxcalc:master Mar 11, 2026
6 of 7 checks passed
@dlyongemallo
dlyongemallo deleted the 345-feedforward_classical_control branch March 12, 2026 10:12
@dlyongemallo

Copy link
Copy Markdown
Contributor Author

I think we can leave the conditional X gate ambiguity / inability to round-trip for later.

Fixed in #430.

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.

Issues in loading an OpenQASM circuit into pyzx and optimize it

5 participants