OutreachXanadu / PennyLaneFix #7807
Draft PR #9532 open Astral Cai review actioned (a53cd62) Astral merged main + escalated Awaiting Marcus Edwards (code-owner) review

Xanadu · PennyLane

PennyLaneAI/pennylane · issue #7807 · branch fix/7807-draw-wires-consistency

Same circuit, two devices that differ only in whether wires=N is passed, two different drawings. The fix sits in a 10-line branch of _add_measurement that forgot to call _add_grouping_symbols on the broadcast-over-all-wires path. Mirroring the pattern already used by _add_global_op closes the asymmetry without touching the single-wire happy path.

2 → 1
Render paths the drawer takes for an all-wires measurement
10 lines
Core change in pennylane/drawer/_add_obj.py
0
Behaviour change for single-wire devices (early-return preserved)
2
Snapshot tests updated, remaining drift documented in README
01 · The problem

Asymmetric drawing depending on a constructor argument

The reporter compares two devices that differ only in whether default.qubit receives an explicit wires=N parameter. With wires=3, the State measurement renders with the multi-wire grouping brackets (╭ ├ ╰). Without wires, the same measurement renders as a flush label with a leading space. The circuit itself is identical; only the drawer output differs.

What the reporter sees

# dev = qml.device("default.qubit", wires=3) 0: ─────────────────────────╭●───────────────╭●─┤ ╭State 1: ───────────╭●────────────│──╭●────────────│──┤ ├State 2: ──RZ(2.50)─╰X──RZ(-0.50)─╰X─╰X──RZ(-1.00)─╰X─┤ ╰State # dev = qml.device("default.qubit") ← no wires argument 0: ─────────────────────────╭●───────────────╭●─┤ State 1: ───────────╭●────────────│──╭●────────────│──┤ State 2: ──RZ(2.50)─╰X──RZ(-0.50)─╰X─╰X──RZ(-1.00)─╰X─┤ State

Either form is internally consistent; the bug is the asymmetry between them.

Where the asymmetry comes from

In pennylane/drawer/_add_obj.py, the _add_measurement dispatcher splits the rendering into two branches on line 386:

  • If len(m.wires) > 0 (explicit-wires path): _add_grouping_symbols(m.wires, layer_str, config) runs at line 375 and emits the box characters across the listed wires.
  • If len(m.wires) == 0 (broadcast path): the same call returns the input unchanged (_add_grouping_symbols early-exits on empty op_wires), then the label is appended to every wire's string without grouping.

When the device is constructed with wires=N, the measurement's m.wires is populated with [0, …, N-1] upstream and the first branch runs. When the device has no fixed wire count, m.wires stays empty and the second branch runs.

flowchart TD
  Start[_add_measurement called] --> Probe{len of m.wires}
  Probe -- "> 0 (explicit wires)" --> P1[_add_grouping_symbols m.wires]
  P1 --> P2[append label to listed wires]
  P2 --> OutA["✓ rendered with ╭ ├ ╰ brackets"]
  Probe -- "== 0 (broadcast over device)" --> Q1[_add_grouping_symbols early-returns]
  Q1 --> Q2[append label to every wire]
  Q2 --> OutB["✗ rendered without brackets, issue #7807"]
  style OutA fill:#0a1e10,stroke:#10b981,color:#86efac
  style OutB fill:#1e0a0a,stroke:#ef4444,color:#fca5a5
      
02 · The fix

Mirror the pattern _add_global_op already uses

PennyLane's drawer already knows how to render an operation that spans every wire, it does so for GlobalPhase and Identity via _add_global_op, which calls _add_grouping_symbols(list(config.wire_map.keys()), …) when the operation's own wires are empty. The fix is to apply the same trick inside the broadcast branch of _add_measurement.

The change, in the only place it matters

# pennylane/drawer/_add_obj.py, _add_measurement, lines 386-393 if len(m.wires) == 0: # state or probability across all wires n_wires = len(config.wire_map) layer_str = _add_grouping_symbols( # ← NEW list(config.wire_map.keys()), layer_str, config, ) for i, s in enumerate(layer_str[:n_wires]): layer_str[i] = s + meas_label

Single-wire devices stay untouched: _add_grouping_symbols returns the input unchanged when there is only one wire (if len(op_wires) <= 1: return layer_str). From two wires up, both render paths now produce the same brackets.

Safety invariants

Single-wire
Behaviour unchanged. _add_grouping_symbols early-exits.
Multi-wire (explicit)
Behaviour unchanged. The function it already called is still called.
Multi-wire (broadcast)
Behaviour aligned with the explicit case, grouping brackets now appear.
Mid-circuit measurements
Untouched. Those go through _add_cwire_measurement.
API surface
Zero change. No new arguments, no new exports.
03 · The tests

Two snapshot updates, the rest documented as drift

Two test snapshots tested the function I changed directly and are updated in-place. The remaining multi-line snapshots in test_draw.py ship one column wider after this change, they are documented in the README so the maintainer (or CI) can pin them up against the actual pytest diff.

  • tests/drawer/test_tape_text.py:267-268, direct unit test for _add_measurement on the default 4-wire wire map. qp.state() now expected as ["╭State", "├State", "├State", "╰State"]; qp.sample() same shape.
  • tests/drawer/test_draw.py:411, test_draw_all_wire_measurements snapshot for the 2-wire (sample, probs, counts) parametrize.
  • Documented as drift in README: multi-line literal snapshots at test_draw.py:1011-1014 and :1032-1036 (multi-wire qp.probs() rendering inside mid-circuit measurement tests). They ship one column wider; their surrounding column alignment needs eyeballing against pytest output, which I could not run locally this session.
Why I left those last lines for the maintainer. They live inside multi-line string literals where the surrounding ╚═══╝ separator and mid-circuit conditional bars are aligned column-perfect. Updating them blind risks a worse snapshot than the one I'd replace. With one local pytest run those updates are mechanical; without one, they are a guess.
04 · The outreach

Where this stands in the conversation funnel

Done

Branch + commit + snapshot updates + README

Local branch fix/7807-draw-wires-consistency, commit 23e005d. Two snapshot tests updated, the remainder documented.

Next

Fork + push + open draft PR

Mail to a named PennyLane drawer maintainer, anchored to the branch URL. The drawer module has a small set of regular contributors; reaching one of them on LinkedIn or via a GitHub @mention is the path to first reply.

Goal

Conversation within 1-2 weeks, contract within 6 weeks

Realistic odds for this lead alone: ~30-50% conversation (PennyLane has active maintainers), ~5-15% contract (Xanadu hires more often for full-time than for contract). The win condition here is more likely a longer-cycle FT pipeline than an immediate retainer.

05 · How to verify

Reproduce every claim

cd c:/Users/FRA/Documents/github/workrepo/pennylane git switch fix/7807-draw-wires-consistency git log --stat -1 # commit 23e005d, 4 files pip install -e .[dev] pytest tests/drawer/test_tape_text.py::TestSpecificFunctions::test_add_measurements -v pytest tests/drawer/test_draw.py::TestMidCircuitMeasurements::test_draw_all_wire_measurements -v # expect both updated tests to PASS, then run the full drawer suite to # surface any remaining snapshot drift in test_draw.py 1011-1014 / 1032-1036. pytest tests/drawer/ -v