Case Study 1: The Chapter That Would Not Import

The plan

This chapter was outlined nine months before it was written, and the outline said: pulse-level programming. Shaped microwave envelopes. DRAG corrections and why square pulses leak population into $|2\rangle$. Calibrating a gate from Rabi oscillations upward. Attaching a custom Schedule to a circuit instruction with add_calibration.

It is a good chapter. It has been written many times, by many people, and there are excellent tutorials for it.

The first line of the probe script was:

import qiskit.pulse
   ModuleNotFoundError: No module named 'qiskit.pulse'

The full extent

   qiskit 2.5.1
     qiskit.pulse                             GONE
     QuantumCircuit.calibrations              REMOVED
     QuantumCircuit.add_calibration           REMOVED
     backend.defaults                         REMOVED
     backend.instruction_schedule_map         REMOVED
     backend.drive_channel                    REMOVED

Not deprecated. Not renamed. Removed — deprecated across the Qiskit 1.x series and deleted in 2.0. The entire subsystem, the circuit-level attachment points, the backend accessors that supplied the calibration data, and the channel abstractions the schedules were written against.

Every tutorial written against that API — and there are many, some still reachable from current documentation and search results — describes something that cannot be imported.

Why it went away

Three reasons, and they are worth understanding because they generalize.

The abstraction did not survive contact with hardware diversity. A pulse schedule written for one superconducting backend is meaningless on another with different qubit frequencies and pulse calibrations, and completely meaningless on trapped ions (Chapter 17), where the physical operations are laser-driven Mølmer–Sørensen gates rather than microwave envelopes. Qiskit's other abstractions — circuits, gates, transpilation — port across hardware. This one could not, and an abstraction that does not abstract is a maintenance burden wearing a library's clothes.

The user base was tiny and largely internal. The overwhelming majority of Qiskit users never touched pulses. Most who did were calibration teams inside vendors, working with better, device-specific tools. Maintaining a public, cross-vendor, stable API for that audience was disproportionate.

And the hardware moved past it. Fractional gates, parametrized native operations, and richer Target metadata now cover cases that previously required dropping to pulses. The genuinely pulse-level work moved to purpose-built stacks — Qiskit Dynamics, vendor control software, and selectively-exposed OpenPulse-derived interfaces.

None of that is a failure. It is what a maturing stack does with an abstraction that stopped paying for itself.

What actually broke for users

Not much, for most. A great deal, for a few.

The people affected were those who had built on it: dynamical-decoupling implementations predating PadDynamicalDecoupling, custom two-qubit gate calibrations, error-mitigation techniques requiring precise pulse timing, and research code from papers whose reproducibility now depends on pinning Qiskit below 2.0.

That last category is the expensive one. A published result whose code cannot run on any supported version of its dependency is a result that gets harder to build on every year.

The generalizable lesson

This is the fourth API disappearance this book has hit, and the others were smaller only in scale:

   Ch. 19  mcx(mode="v-chain")               removed in Qiskit 2.1
   Ch. 24  shots= on a PennyLane device      deprecated in PennyLane 0.45
   Ch. 28  basis_gates=['h','barrier']       now raises
   Ch. 31  qiskit.pulse, entirely            removed in Qiskit 2.0

⚠️ An API being present, documented, demonstrated in official tutorials, and built upon by published research is not a commitment that it will exist next year.

This is not cynicism about the maintainers — every one of these removals was announced, deprecated on a schedule, and justified. It is a statement about what a dependency is.

The defence is Chapter 27's, and it is cheap:

def test_the_pulse_api_is_gone():
    with pytest.raises(ModuleNotFoundError):
        importlib.import_module("qiskit.pulse")

    qc = QuantumCircuit(1)
    assert not hasattr(qc, "add_calibration")

A test suite that exercises the APIs you depend on tells you at upgrade time rather than in production, and the failure names the thing that moved. Chapter 27 §27.2 priced exact assertions at 174,000 per CI-minute; an import check costs nothing measurable.

The project module goes one step further and turns the removal into documentation:

REMOVED_IN_QISKIT_2 = {
    "qiskit.pulse": "no direct replacement; use target.dt and instruction durations",
    "backend.defaults": "backend.target",
    "backend.instruction_schedule_map": "backend.target[gate][qubits].duration",
    ...
}

A raise with a pointer beats an AttributeError from three frames inside a tutorial.

What the chapter became

Better, as it happens.

The planned chapter would have taught an API that no longer exists, using a device model that no vendor exposes, to an audience that mostly does not need it. What replaced it is what actually survives below the gatedt, instruction durations, $T_1$ and $T_2$, and the scheduling passes — and those turned out to hold the two most interesting numbers in the chapter:

   rz duration:       0.0 ns    -- the VIRTUAL Z gate: free, exact, error-free
   measure duration:  1,216 ns  -- 2.3x the median two-qubit gate

And they led to §31.4's dynamical-decoupling measurement, which produced the sharpest result in Part V — one that a pulse tutorial would never have surfaced.

When a planned approach turns out to be impossible, the honest chapter is the one about what is actually there. That is also the correct response when it happens to your project.

The lessons

Check that your dependencies still exist before planning around them. One import, at the start.

Test the APIs you depend on, so removals surface at upgrade time with a clear message.

Document removals with their replacements, in your own code, where the next person will look.

Pin versions for reproducibility, and know that pinning is a decay clock. Research code pinned below Qiskit 2.0 still runs, and gets harder to build on each year.

And when the plan is impossible, write about what is real. The chapter improved.


Reproduce it: code/example-01-what-survives-below-the-gate.py documents each removal by attempting to use it, then reports what remains; REMOVED_IN_QISKIT_2 and pulse_api_replacement in code/vqelab/timing.py turn the removal into a pointer, and test_the_pulse_api_is_gone in code/project-checkpoint.py is the upgrade-time alarm.