Skip to content

Qiskit interop: warning hygiene, 3.0-proofing, honest dependency floors#839

Merged
rileyjmurray merged 5 commits into
developfrom
qiskit-integration
Jul 15, 2026
Merged

Qiskit interop: warning hygiene, 3.0-proofing, honest dependency floors#839
rileyjmurray merged 5 commits into
developfrom
qiskit-integration

Conversation

@rileyjmurray

@rileyjmurray rileyjmurray commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

This makes pyGSTi's qiskit interop clean under error::pygsti.tools.exceptions.QiskitInteropWarning, which this PR promotes from a defined-but-inert exception class to a pytest.ini error. Getting there required fixing several real bugs that were hiding behind warnings nobody could see (or that were misclassified so they wouldn't fire at all), removing qiskit APIs slated for removal in 3.0, and correcting the ibmq extra's dependency floors, which were stale enough to break on qiskit versions we claim to support.

Five commits, one per phase, each leaving the suite green under the strict filter: 7e39339dc73a4f54bcf4178a8c3962c67a1e1bb2ae93f.

What changed

Warning hygiene (pygsti/tools/_qiskit_interop.py, new)

  • Eleven copy-pasted exact-version checks against qiskit==2.1.1, scattered across circuit.py, internalgates.py, subcircuit_selection.py, mirror_edesign.py, and scarab.py, are replaced by one helper, check_qiskit_version(context, required=True), checked against a tested range (TESTED_QISKIT_RANGE = ((1, 4), (3,))) instead of a single pin.
  • That consolidation incidentally fixed real bugs the old pattern was causing: a warn-inside-try/bare-except that turned the promoted warning into a false "qiskit does not appear to be installed" RuntimeError; a MissingDependencyWarning misclassification in subcircuit_selection.py where a genuine version mismatch was reported as a missing dependency and execution silently continued; and a NameError fall-through in internalgates.py when qiskit is truly absent.
  • from_qiskit now warns about discarded classical data only when the circuit actually has clbits — previously it fired on len(circuit.cregs), so a pure pygsti→qiskit→pygsti round trip warned about discarding the empty cr register that convert_to_qiskit always attaches, even when nothing was measured.
  • The plain-UserWarning 'skipping measure' is now a QiskitInteropWarning with an actual message.
  • from_qiskit gains lossy={'warn', 'ignore', 'raise'}; the mirror-edesign pipeline passes lossy='ignore' at call sites where the loss is intentional.
  • mirror_edesign's fullstack path no longer forwards coupling_map to transpile() alongside a backend — the warning claimed it was ignored, but an explicit coupling_map actually overrides the backend's map, so the old behavior silently used the wrong topology.
  • New test/unit/tools/test_qiskit_interop.py exercises the warning paths directly.

Qiskit 3.0-proofing

  • Removed the last uses of APIs slated for removal in qiskit 3.0: iterating a CircuitInstruction as a tuple (→ .operation/.qubits), qubit._index and layout.initial_layout[idx]._register.name == 'ancilla' (→ circuit.find_bit(qubit).index and isinstance(..., AncillaQubit)), and circuit._qbit_argument_conversion(i)[0] (→ the public, stable-since-1.0 circuit.qubits[i]).
  • Test files: the deprecated QFT class (built on the also-removed BlueprintCircuit) is replaced with QFTGate.
  • Considered and rejected: a global pytest filter promoting qiskit DeprecationWarnings to errors. Warning attribution follows stacklevel, so such a filter would promote all pygsti-attributed DeprecationWarnings, not just qiskit-related ones — too much blast radius on the full suite. Documented in the phase-2 commit message.

extras/ibmq guard split + honest floors

  • ibmqexperiment.py imported ConvertToMidCircuitMeasure (added in qiskit-ibm-runtime 0.44) in the same try/except as the core runtime names. On any older runtime the whole import failed and _Sampler was set to None, disabling all IBMQ submission — even though nothing else needed the new pass. It's now guarded separately, with the assertion only in the code path that actually converts mid-circuit measurements (message names the real fix: upgrade runtime, or pass use_ibm_mcm=False).
  • pyproject.toml's ibmq extra goes from the stale, unbounded qiskit>1, qiskit-ibm-runtime>=0.17.1 to qiskit>=1.4,<3, qiskit-ibm-runtime>=0.40, matching TESTED_QISKIT_RANGE.
  • Validated: all 11 ibmq tests now pass on both qiskit 2.5.0/runtime 0.47 and qiskit 2.1.1/runtime 0.41.1 — the latter previously failed solely because of the coupled import.

Coverage

  • test/unit/protocols/test_scarab.py (new): smoke tests for all three scarab benchmark wrappers (lowlevel/fullstack/subcircuit_mirror_benchmark), which are public API in pygsti.protocols.scarab but previously had zero coverage — these would have caught the phase-1 bare-except bug.
  • test_circuit.py: a convert_to_qiskit argument matrix (qubits_to_measure in None/'all'/'active'/list form, oversized num_qubits, ordered_data_indices metadata, the invalid-option error path).
  • test_subcirc_selection.py: greedy_growth_subcirc_selection coverage (direct call and via sample_subcircuits(strategy='greedy')), plus a stochastic_2q_drops test — see below. subcircuit_selection.py coverage goes from 42% to 80%.
  • extras/devices gets no new tests, per plan: its calibration formats predate the current runtime API, and deprecation is proposed to maintainers instead of investing in coverage.

CI

  • New .github/workflows/qiskit-interop.yml, weekly: a bare qiskit-1.4-floor matrix leg (runtime uninstalled), a runtime-0.40-floor leg, and a latest leg. Keeps TESTED_QISKIT_RANGE honest and should flag qiskit 3.0 breakage the week it ships rather than at user-report time.

Reviewer attention

  1. New bug found, not fixed: stochastic_2q_drops=True is broken. Writing the greedy/stochastic coverage in phase 4 (previously 0% covered) surfaced that simple_weighted_subcirc_selection(..., stochastic_2q_drops=True) re-adds dangling two-qubit gates whose support, by definition, includes at least one qubit outside the selected subset — and the subcircuit is then built with line_labels=sorted(qubit_subset), so Circuit.__init__ raises ValueError on essentially any connected circuit. The new test documents the intended behavior and is marked @unittest.expectedFailure. Fixing it needs a maintainer call: should retained dangling gates expand the subcircuit's line labels (changing its effective width), or should they be truncated to their in-subset qubits? Whoever picks a direction should also remove the expectedFailure marker.
  2. ibmq extra's dependency floor moved from qiskit>1 to qiskit>=1.4,<3 and from qiskit-ibm-runtime>=0.17.1 to >=0.40. This is a correction, not a new restriction — anything outside the old, unbounded range was already broken (that's how finding 5 was found), but it's technically a tightening of the declared floor that downstream pins might notice.
  3. No global DeprecationWarning promotion. See the qiskit-3.0-proofing section above — flagging so it isn't independently proposed and then hit the same blast-radius problem.

Testing

Cross-version validation, all with the strict error::QiskitInteropWarning filter, 141 passed + 2 xfailed on each:

  • qiskit 2.5.0 + qiskit-ibm-runtime 0.47 (py313 conda env)
  • qiskit 2.1.1 + qiskit-ibm-runtime 0.41.1
  • qiskit 1.4.6 + qiskit-ibm-runtime 0.37.0

Promote QiskitInteropWarning to an error in pytest.ini so interop concerns
surface as test failures.

Centralize the qiskit version check in a new pygsti.tools._qiskit_interop
helper with a tested version *range* (currently [1.4, 3)) instead of ten
copy-pasted exact-match checks against 2.1.1. The helper also removes the
warn-inside-try/bare-except pattern that converted the promoted warning into
a false "Qiskit is required ... does not appear to be installed" RuntimeError,
the MissingDependencyWarning misclassification in subcircuit_selection, and
the NameError fall-through in internalgates when qiskit is truly absent.

Other warning fixes:
- from_qiskit warns about discarded classical data only when the circuit
  actually has clbits, not for the empty 'cr' register convert_to_qiskit
  always attaches, so measurement-free round trips are now silent.
- The plain-UserWarning 'skipping measure' becomes a QiskitInteropWarning
  with a real message.
- from_qiskit gains a lossy={'warn','ignore','raise'} parameter; the
  mirror-edesign pipeline passes lossy='ignore' where loss is by design.
- fullstack mirror edesign no longer forwards coupling_map to transpile()
  alongside a backend (the warning said it was ignored but it actually
  overrode the backend's map); ignored-arg warnings now fire once, up front.
- simple_weighted_subcirc_selection keeps its qiskit module binding (the old
  bare-except silently disabled qiskit-native instruction-duration lookups)
  and no longer converts a CouplingMap type error into a bogus ImportError.

Add warning-path unit tests in test/unit/tools/test_qiskit_interop.py.
Remove all uses of qiskit APIs that are deprecated for removal in qiskit 3.0,
plus remaining private-API usage:

- mirror_edesign.get_active_qubits_no_dag: stop iterating CircuitInstruction
  as a tuple (removed in 3.0); use the .operation/.qubits attributes.
- mirror_edesign ancilla detection: replace qubit._index and
  layout.initial_layout[idx]._register.name == 'ancilla' with the public
  circuit.find_bit(qubit).index and isinstance(..., AncillaQubit).
- Replace circuit._qbit_argument_conversion(i)[0] with circuit.qubits[i]
  (public and stable since qiskit 1.0) in mirror_edesign; the circuit.py
  sites were converted in the previous commit.
- Tests: replace the deprecated QFT class (removed in 3.0, and built on the
  also-removed BlueprintCircuit) with QFTGate appended to a QuantumCircuit.

With this, the only deprecation warning left in the qiskit test files fires
inside qiskit-ibm-runtime's own plugin loader, not from pyGSTi code. A global
promote-qiskit-DeprecationWarnings pytest filter was considered (plan phase 2)
but not added: warning module attribution follows the warn() stacklevel, so
such a filter would have to promote DeprecationWarnings attributed to pygsti
modules generally, which risks unrelated failures across the full suite.
qiskit-ibm-runtime's ConvertToMidCircuitMeasure transpiler pass only exists in
runtime >=0.44 (Dec 2025), but it was imported in the same try/except as the
core runtime names, so on any older runtime the whole block failed and
_Sampler was set to None -- disabling all IBMQ submission even though nothing
else required the new pass. Import it under its own guard and assert only in
the code path that actually converts mid-circuit measurements (with a message
that names the real fix: upgrade runtime or pass use_ibm_mcm=False).

Also delete the stale qiskit-1.1.1/runtime-0.25.0 era comment block, narrow
the bare excepts to ImportError, and update the 'ibmq' extra in pyproject.toml
from the stale/unbounded 'qiskit>1, qiskit-ibm-runtime>=0.17.1' to
'qiskit>=1.4,<3, qiskit-ibm-runtime>=0.40', matching TESTED_QISKIT_RANGE in
pygsti.tools._qiskit_interop.

Validated: all 11 ibmq tests pass on qiskit 2.5.0 + runtime 0.47 (py313) and
now also on qiskit 2.1.1 + runtime 0.41.1, where they previously failed solely
because of the coupled import.
…(phase 4)

- test_scarab.py: smoke tests for all three scarab benchmark wrappers
  (lowlevel/fullstack/subcircuit_mirror_benchmark), which previously had zero
  test coverage despite being exported from pygsti.protocols. These would have
  caught the phase-1 bare-except bug and will catch future signature drift.
- test_circuit.py: convert_to_qiskit argument matrix -- qubits_to_measure
  None/'all'/'active'/list, num_qubits larger than the circuit width,
  metadata ordered_data_indices, and the invalid-option error path.
- test_subcirc_selection.py: greedy_growth_subcirc_selection (direct call
  with return_depth_info, and via sample_subcircuits strategy='greedy') plus
  a stochastic_2q_drops test.

The stochastic_2q_drops test is marked expectedFailure: exercising the
previously-uncovered path revealed it is broken -- retained dangling gates
have support outside the selected qubit subset, so building the subcircuit
with line_labels=sorted(qubit_subset) always raises ValueError. Recorded as
finding 7 in the active-project findings; the fix needs a maintainer decision
(expand line labels vs truncate dangling gates).

Per the plan, extras/devices gets no new tests; deprecation is proposed to
the maintainers instead (its calibration formats predate the runtime API).
Runs the qiskit-facing test files against three environments: bare qiskit 1.4
(conversion floor, runtime-dependent tests skip), the declared runtime floor
(qiskit-ibm-runtime 0.40 + qiskit<3), and whatever pip resolves today. Keeps
TESTED_QISKIT_RANGE continuously honest and flags qiskit 3.0 breakage the week
it ships rather than at user-report time.

Local cross-version validation of the full change set, all with the strict
error::QiskitInteropWarning filter and all 141 passed + 2 xfailed:
- qiskit 2.5.0 + qiskit-ibm-runtime 0.47 (py313)
- qiskit 2.1.1 + qiskit-ibm-runtime 0.41.1
- qiskit 1.4.6 + qiskit-ibm-runtime 0.37.0
Comment thread .github/workflows/qiskit-interop.yml
@rileyjmurray rileyjmurray changed the title WIP: qiskit integration Qiskit interop: warning hygiene, 3.0-proofing, honest dependency floors Jul 15, 2026
@rileyjmurray
rileyjmurray marked this pull request as ready for review July 15, 2026 02:59
@rileyjmurray
rileyjmurray requested review from a team as code owners July 15, 2026 02:59
@coreyostrove
coreyostrove requested a review from ndsieki July 15, 2026 09:39

@sserita sserita left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 on Nick's suggestion for the subseq for easier selection of tests, but not blocking. Other than that, looks great, merge as you will. I'll make an issue for the 2q_drops problem, which will be on my plate most likely.

Comment thread .github/workflows/qiskit-interop.yml
Comment thread test/unit/objects/test_subcirc_selection.py
@rileyjmurray
rileyjmurray merged commit 85c37c2 into develop Jul 15, 2026
4 checks passed
@rileyjmurray
rileyjmurray deleted the qiskit-integration branch July 15, 2026 17:00
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