v1.5.0 migration guide
Use this guide when upgrading from v1.4.8 to v1.5.0.
For the high-level summary of what changed, see the
v1.5.0 release notes.
Who should read this guide
Read this guide if any of the following apply:
- You run Qubex on real hardware through
ExperimentorMeasurement - You maintain configuration files under
box.yaml,chip.yaml, orwiring.yaml - You import low-level types from
qubex.backend - You use contrib-heavy
Experimenthelpers such as RZX, multipartite entanglement, purity benchmarking, or Stark workflows - You wrote timing-sensitive code that assumed a fixed
2 nssampling period - You rely on simulator
Controlinterpolation or mutate control segment data in place
If you only use the basic high-level QuEL-1 workflow through top-level
qubex imports, and you do not depend on moved helper APIs or backend-side
imports, the upgrade is usually straightforward.
At a glance checklist
- Use Python
3.10+ - Prefer
system_idoverchip_id - Add or validate
system.yaml - Move system-side imports from
qubex.backendtoqubex.system - Rename
shotston_shotsandintervaltoshot_interval - Replace moved
Experimenthelper methods withqubex.contribfunctions - Replace simulator
Controlinterpolation with explicitly sampled waveforms - Remove hardcoded
2 nsassumptions from sweeps, plots, and timing utilities
SQUAD window arguments and units
Standalone beta_mode and beta_sum arguments have been removed from
Squad, Squad.func, and the SQUAD paths of FlatTop and FlatTop.func.
They now raise TypeError, including explicitly supplied former defaults.
There is no deprecation period. Replace them with a window dictionary:
pulse = Squad(
duration=40, amplitude=0.6, delta=-0.8, tau=12, correction_factor=0,
window={"type": "beta", "mode": 0.4, "sum": 6.0},
)
The string window="beta" still uses mode 1/3 and sum 5.
Tukey settings continue to use the same dictionary interface.
Unified CD sign and strength
This is a breaking change to direct Squad / Squad.func:
factor has been removed and is rejected with TypeError, including
explicit factor=None. The instance attribute is now correction_factor.
Both SQUAD entry points use transition-minus-drive delta, an I + i*Q
envelope, and the same negative-sign CD formula:
Q = -correction_factor * delta * dI/dt / (delta**2 + I**2).
Use angular-rate amplitude/delta in rad/ns and time in ns.
correction_factor is dimensionless: 1 is the analytic CD strength,
0.5 halves it, and 0 disables it. The carrier is configured separately.
To preserve an old direct-SQUAD waveform, negate its old factor:
# Before: Squad(..., factor=x)
# After:
pulse = Squad(
duration=40, amplitude=0.6, delta=-0.8, tau=12,
correction_factor=-x,
)
Old omitted/None factor meant +1: use new correction_factor=-1 to
preserve that old waveform. New omitted/None correction_factor means +1
in the unified convention and therefore reverses the old default Q.
There is no silent compatibility alias. FlatTop's existing CD formula,
DRAG behavior, correction-disabled default, ramp sampling and signs are unchanged.
Direct Squad still enables CD by default.
Keep hardware-unit conversion separate from correction strength:
K = 2 * np.pi * rabi_ghz_per_command
pulse = FlatTop(
duration=40, tau=12, type="Squad",
amplitude=K * command_amplitude,
delta=2 * np.pi * (transition_ghz - drive_ghz),
correction_type="CD", correction_factor=1.0,
scale=1 / K,
)
Scale converts both I and Q to command amplitudes. This preserves older command-unit FlatTop waveforms that used delta divided by the Rabi scale and correction_factor divided by K; do not apply both conversions. The Rabi scale remains a caller-supplied hardware model, not a calibration performed by the pulse API.
Recompute delta per drive frequency for carrier-adaptive SQUAD design. Holding delta fixed instead deliberately scans one waveform's carrier; regenerating the waveform changes the interpretation of a chevron. See the API docstrings for the complete conventions.
Installation and environment changes
The v1.5.0 repository workflow assumes a uv-managed environment.
Follow the current installation guide
for the exact supported commands.
At minimum, update these assumptions:
- Python
3.9is no longer supported. Use Python3.10or newer. - Backend-enabled installs use the
backendextra. - In-repository development now assumes
make syncin auvenvironment. qxsimulatorno longer installs JAX, Optax, or IPython. JAX and Optax were used only by the deprecatedPulseOptimizer; existing users who temporarily retain that API must install those two packages separately. Its IPython display integration has been removed.
Configuration changes
Move from chip_id-first to system_id-first loading
In v1.4.8, many workflows were effectively single-chip oriented. In
v1.5.0, the public configuration model is one system_id per runnable
hardware setup.
Old style:
import qubex as qx
exp = qx.Experiment(
chip_id="64Q",
qubits=["Q00", "Q01"],
config_dir="/path/to/64Q/config",
params_dir="/path/to/64Q/params",
)
New style:
import qubex as qx
exp = qx.Experiment(
system_id="64Q-HF-Q1",
qubits=["Q00", "Q01"],
config_dir="/path/to/qubex-config/config",
params_dir="/path/to/qubex-config/params/64Q-HF-Q1",
)
chip_id still works as a compatibility input in v1.5.0, but it is now
deprecated and should not be the long-term selector in updated notebooks.
Add system.yaml
system.yaml is now the canonical place to define one runnable system and its
backend family.
64Q-HF-Q1:
chip_id: 64Q
backend: quel1
144Q-LF-Q3:
chip_id: 144Q
backend: quel3
quel3:
endpoint: localhost
port: 50051
Update your configuration with these rules:
- Key
wiring.yamlbysystem_id, not by chip name alone - Put backend selection in
system.yaml - Treat
config_diras the shared config directory andparams_diras the selected per-system parameter directory
Backend selection is resolved in this order:
- Explicit
backend_kindargument backendfield insystem.yaml- Default
quel1
If you previously stored backend selection in chip.yaml, move that setting to
system.yaml. In v1.5.0, chip.yaml is no longer the authoritative backend
source when system.yaml exists.
Prefer structured parameter files
v1.5.0 prefers one structured YAML file per parameter family.
meta:
unit: GHz
description: Example control frequencies
data:
0: 5.000
1: 5.125
Recommended layout:
qubex-config/
config/
chip.yaml
box.yaml
system.yaml
wiring.yaml
params/
64Q-HF-Q1/
control_frequency.yaml
readout_frequency.yaml
control_amplitude.yaml
readout_amplitude.yaml
measurement_defaults.yaml
Legacy params.yaml and props.yaml are still supported as fallback inputs in
v1.5.0. You do not need to migrate every parameter file at once, but new
work should use the structured per-file format.
Use measurement_defaults.yaml under params/<system_id>/ when you want one
system to carry different default values for n_shots, shot_interval, or
readout timing.
Recheck configuration_mode against control-port channel counts
configuration_mode is now interpreted as a priority-ordered channel layout.
ge-ef-crmeansge, thenef, thencrge-ef-fhmeansge, thenef, thenfhge-cr-crmeansge, thencr, thencr- control ports with fewer channels keep only the leftmost roles
If your hardware profile changes control-port channel counts, the realized
targets change with it. For example, QuEL-1 SE R8 se8_mxfe1_awg2222 gives
2-2-2-2 on the four profile-controlled ports, so
configuration_mode="ge-ef-cr" now builds ge-ef targets there. If you need
CR targets on those ports, use configuration_mode="ge-cr-cr" instead. For
EF/FH workflows on two-channel ports, use configuration_mode="ge-ef-fh"; EF
and FH share the second channel.
API and import changes
Move system-side imports out of qubex.backend
The biggest low-level import change is that system/configuration objects no
longer live in qubex.backend.
Update imports like this:
# v1.4.8
from qubex.backend import ConfigLoader, ControlSystem, ExperimentSystem, SystemManager
# v1.5.0
from qubex.system import ConfigLoader, ControlSystem, ExperimentSystem, SystemManager
The qubex.backend namespace now focuses on backend controller contracts and
concrete backend implementations such as qubex.backend.quel1 and
qubex.backend.quel3.
Rename common kwargs and properties
These changes are not hard breaks in v1.5.0, but they should be migrated now:
| Old usage | New usage |
|---|---|
shots= |
n_shots= |
interval= |
shot_interval= |
exp.linkup() |
exp.connect() |
exp.device_controller |
exp.backend_controller |
measurement.qubits |
measurement.qubit_labels |
Example:
# v1.4.8
result = exp.measure(sequence=sequence, shots=1024, interval=150 * 1024)
# v1.5.0
result = exp.measure(
sequence=sequence,
n_shots=1024,
shot_interval=150 * 1024,
)
Move contrib-style helper APIs out of Experiment
Several specialized helper APIs were removed from Experiment as direct
methods and moved to qubex.contrib. The old methods now warn and raise
NotImplementedError, so you must update direct call sites.
Representative mappings:
| Old usage | New usage |
|---|---|
exp.rzx(...) |
qx.contrib.rzx(exp, ...) |
exp.rzx_gate_property(...) |
qx.contrib.rzx_gate_property(exp, ...) |
exp.measure_cr_crosstalk(...) |
qx.contrib.measure_cr_crosstalk(exp, ...) |
exp.cr_crosstalk_hamiltonian_tomography(...) |
qx.contrib.cr_crosstalk_hamiltonian_tomography(exp, ...) |
exp.measure_ghz_state(...) |
qx.contrib.measure_ghz_state(exp, ...) |
exp.measure_graph_state(...) |
qx.contrib.measure_graph_state(exp, ...) |
exp.measure_bell_states(...) |
qx.contrib.measure_bell_states(exp, ...) |
exp.purity_benchmarking(...) |
qx.contrib.purity_benchmarking(exp, ...) |
exp.interleaved_purity_benchmarking(...) |
qx.contrib.interleaved_purity_benchmarking(exp, ...) |
exp._stark_t1_experiment(...) |
qx.contrib.stark_t1_experiment(exp, ...) |
exp._stark_ramsey_experiment(...) |
qx.contrib.stark_ramsey_experiment(exp, ...) |
exp._simultaneous_measurement_coherence(...) |
qx.contrib.simultaneous_coherence_measurement(exp, ...) |
Example:
import qubex as qx
schedule = qx.contrib.rzx(
exp,
control_qubit="Q00",
target_qubit="Q01",
angle=0.78539816339,
)
Update visualization and result access
v1.5.0 introduces canonical figure accessors on result models.
Update code like this:
# legacy payload access
fig = result["fig"]
figures = result["figures"]
# v1.5.0 canonical access
fig = result.figure
figures = result.figures
detail = result.get_figure("detail")
Also move visualization imports to the new module:
# legacy
from qubex.analysis import visualization as viz
# v1.5.0 canonical
import qubex.visualization as viz
Legacy import shims still exist for many model modules, but new code should
prefer qubex.measurement.models and qubex.experiment.models.
Avoid deep imports into removed internal modules
Top-level package exports such as qubex.pulse and qubex.simulator still
work, but many old internal module paths were removed as part of the package
split onto companion packages.
Update imports like this:
# v1.4.8 deep import
from qubex.pulse.library import Rect
from qubex.simulator.quantum_system import QuantumSystem
# v1.5.0 stable import
from qubex.pulse import Rect
from qubex.simulator import QuantumSystem
If you are building reusable libraries on top of Qubex internals, consider
importing from the companion packages directly (qxpulse, qxsimulator,
qxcore, qxvisualizer) instead of relying on removed internal file layouts.
Update simulator Control sampling
Simulator Control objects now represent finite-duration, piecewise-constant
signals. The interpolation constructor argument and interpolator property
have been removed. Use get_samples() to evaluate the zero-order-held signal.
# v1.4.x
control = Control(..., interpolation="linear")
samples = control.interpolator(times)
# v1.5.0
control = Control(...)
samples = control.get_samples(times)
At an internal segment boundary, get_samples() returns the segment beginning
at that boundary. It returns zero before the control starts and after its total
duration. If you relied on linear, cubic, or FIR-like reconstruction, generate
the desired sampled waveform before constructing Control and provide the
corresponding segment durations.
Control copies waveform and durations and exposes them as read-only
arrays. Construct a new Control instead of modifying these arrays in place.
Every segment duration must be finite and greater than zero; an empty control
may still use empty waveform and duration arrays.
Update simulate() propagation settings
QuantumSimulator.simulate() now uses dt as its fixed propagation interval.
The final interval may be shorter so evolution ends exactly at the common
control duration. Control segment boundaries and requested output times are not
inserted into this integration grid, so discontinuities that do not coincide
with the fixed grid are resolved only as dt is reduced.
The TIME_STEP constant has been removed. simulate() now declares its
default directly as dt=0.1; pass dt explicitly when a different fixed
propagation interval is required.
Within each interval, the zero-order-held control amplitude is selected at the left endpoint. Continuously time-dependent carrier and coupling terms are evaluated at the interval midpoint. Results for detuned drives or rotating couplings can therefore differ from the previous left-endpoint propagation.
Control.frame_shifts and Control.final_frame_shift are logical-frame
metadata and are not applied as physical rotations to states or propagators.
Intermediate shifts from a PulseSchedule are already reflected in the phases
of subsequent waveform samples. The per-segment metadata additionally lets
SimulationResult interpret the returned trajectory in the changing logical
frame. If n_samples is specified, it must be at least 2 so that the initial
and final physical evolution points are both retained. Downsampling occurs only
after the complete fixed-step evolution, so n_samples does not change the
simulated final state. Uniformly spaced trajectory indices are selected, which
need not produce exactly uniform physical times when the terminal interval is
shorter than dt. If the trajectory already contains at most n_samples
points, all points are returned. A zero-duration trajectory contains only its
initial point. If n_samples is omitted, every fixed-step integration point is
returned.
Configure QuTiP solver integration with options
The dt argument no longer appears in the signatures of the QuTiP-based
QuantumSimulator.sesolve(), mesolve(), propagator(), gate_fidelity(),
create_simulation_parameters(), and create_simulation_model() methods.
Calls that still pass dt are accepted for compatibility, emit a
DeprecationWarning, and ignore its value. The model time list is now the union
of all Control segment boundaries, and control amplitudes use exact
zero-order hold between those boundaries. Continuous drive-frame and coupling
phases remain analytic QuTiP coefficients. This list is exposed as
SimulationModel.boundary_times and as the boundary_times entry returned by
create_simulation_parameters(); the previous generic times names are no
longer used.
For sesolve() and mesolve(), n_samples requests exactly that many
uniformly spaced public output times for a positive control duration. Qubex
passes the union of those output times and all control boundaries to QuTiP,
then retains only the requested output trajectory. Thus every zero-order-hold
discontinuity remains a solver checkpoint without forcing the public result
onto the irregular control grid. A zero-duration trajectory contains only its
initial point. If n_samples is omitted, all control boundaries are returned
as before.
QuTiP chooses adaptive internal integration steps. Pass solver settings such as
method, rtol, atol, and max_step through options. When max_step is
omitted, Qubex uses half the shortest control segment duration; an explicitly
provided value takes precedence. When nsteps is omitted, Qubex allows at
least 2500 internal steps and twice the number required by max_step over the
longest solver interval. Qubex otherwise uses QuTiP's defaults, including the
integration method and error tolerances. The dt argument remains meaningful
only for QuantumSimulator.simulate().
QuantumSimulator.propagator() now returns cumulative propagators at the union
of all Control segment boundaries. Use the final list element when only the
complete evolution is needed. Advancing through every boundary also gives each
piecewise-constant discontinuity its own solver interval. For a closed system,
the list contains unitary operators computed in Hilbert space. For a system
with any positive decoherence rate, it contains superoperators computed in
Liouville space. Zero-rate relaxation and dephasing operators are no longer
added to the model. The fidelity methods use the final propagator and accept
either representation. They extract the computational-subspace map by default,
accept levels="full" for the complete physical space, and accept a per-object
level mapping for qudit or non-computational subspaces. This avoids the much
larger Liouville-space integration for closed systems.
gate_fidelity() is deprecated; use average_gate_fidelity() instead. The
deprecated name remains an alias during the compatibility period.
process_fidelity() returns the normalized Choi overlap of the extracted
computational-subspace map with the target unitary. Because extraction can
make the map trace-decreasing, average_gate_fidelity() counts leakage as
failure and uses
\(F_\mathrm{avg}=(dF_\mathrm{pro}+p_\mathrm{surv})/(d+1)\), where
\(p_\mathrm{surv}=\operatorname{Tr}[\mathcal{E}_\mathrm{sub}(I)]/d\). For a
trace-preserving map, \(p_\mathrm{surv}=1\) and this reduces to QuTiP's standard
average-gate-fidelity relation.
Use QuantumSystem.unitary() to construct a target by object label and embed
it in the full physical Hilbert space:
from qxsimulator import gates
target = system.unitary({"Q04-Q01": "CZ"})
fidelity = simulator.average_gate_fidelity(
controls,
target_unitary={"Q04": gates.X},
levels={"Q04": (1, 2)},
)
Named strings include the existing Qubex Clifford gate names and common static
gates. Build parameterized gates with
gates.rotation(generator, angle), which evaluates
exp(-1j * angle * generator / 2). The X, Y, Z, XX, YY, ZZ, and
ZX generators can be combined directly, for example
gates.rotation((gates.XX + gates.YY) / 2, angle). Fidelity methods accept
these labeled gate mappings directly. A Qobj fidelity target may have the
selected subspace dimensions or the full physical-system dimensions.
PulseOptimizer is deprecated and will be removed in a future release. JAX
and Optax are no longer installed with qxsimulator and must be installed
separately to use this compatibility API. Its IPython display integration has
been removed; ordinary simulator imports and workflows load none of these
packages.
Request propagator trajectories explicitly
SimulationResult.states and SimulationResult.propagators are lists of
QuTiP Qobj instances. SimulationResult.unitaries is deprecated; use
propagators instead. The deprecated attribute remains an alias for the same
list during the compatibility period.
SimulationResult.control_frequencies is also deprecated because one target
may have controls at multiple frequencies. Inspect SimulationResult.controls
directly instead. When frame="drive" is requested, the result infers the
analysis frame only if the target has exactly one distinct control frequency.
If the target has no controls or multiple tones, pass frame_frequency
explicitly in GHz.
SimulationResult.get_substates() now returns list[Qobj] instead of an
object-dtype NumPy array, matching the documented result model. Bloch-vector
and density-matrix helpers continue to return numeric NumPy arrays, with
float64 and complex128 dtypes respectively.
The frame, frame_frequency, and apply_frame_shifts arguments of the
substate extraction methods are keyword-only. Update positional calls to use
explicit argument names.
SimulationResult now validates trajectory alignment and system dimensions at
construction. It copies the supplied control, state, and propagator containers,
and stores times as a copied, read-only float64 array. Times must be finite and
strictly increasing; invalid result objects now raise ValueError immediately.
Equality is identity-based, and repr() reports trajectory counts without
expanding large arrays or QuTiP objects.
QuantumSimulator.simulate() computes propagators by default. Pass
compute_propagators=False to retain only its state trajectory.
QuantumSimulator.sesolve() and mesolve() do not compute propagators by
default. Request them explicitly when both trajectories are required:
result = simulator.sesolve(
controls,
compute_propagators=True,
)
For sesolve(), each propagator is an operator acting on a ket. For
mesolve(), each propagator is a superoperator acting on a vectorized density
matrix. Computing a full propagator is more expensive than evolving one state,
especially for mesolve(), where the superoperator contains d ** 4 elements
for Hilbert-space dimension d. An empty propagators list indicates that the
trajectory was not computed.
States and propagators remain in the simulator's physical rotating frame.
Controls converted from a PulseSchedule retain both per-segment
frame_shifts and the terminal final_frame_shift as coordinate metadata.
SimulationResult.get_substates() and the density-matrix and Bloch-vector
helpers apply the accumulated frame shifts at every returned time by default.
Pass apply_frame_shifts=False to inspect the raw physical-frame trajectory.
At an internal boundary, the shift of the segment starting at that boundary is
used; from the final boundary onward, the terminal shift is used.
Timing and result-model updates
Stop assuming a fixed 2 ns sampling period
Key execution paths in v1.5.0 now resolve timing from the active backend.
Replace hardcoded 2 or 2.0 sampling-period values with backend-derived
values where possible.
Recommended pattern:
import numpy as np
wait_range = exp.util.discretize_time_range(
np.geomspace(100, 100e3, 51),
sampling_period=exp.measurement.sampling_period,
)
For low-level measurement results, use per-capture sampling metadata instead of assuming one global constant. This is especially important if you are adapting scripts for QuEL-3.
Use canonical measurement models when you adopt async or low-level flows
Synchronous compatibility flows such as measure() and execute() still
return legacy MeasureResult and MultipleMeasureResult objects where
expected. New async-first and low-level flows return canonical measurement
models such as MeasurementResult, CaptureData, and
SweepMeasurementResult.
These canonical models support structured persistence:
result = await exp.run_measurement(schedule=schedule, n_shots=1024)
path = result.save("result.nc")
restored = type(result).load(path)
Validation steps
After migrating code and configuration, run a small but real validation set:
- Create a fresh Python
3.10+environment and install Qubex with the required extras. - Load one real system through
Experiment(system_id=..., config_dir=..., params_dir=...). - Run
exp.connect()and, if your workflow requires it,exp.configure(). - Execute one smoke measurement with
measure()orexecute(). - Execute one timing-sensitive sweep or notebook that previously relied on a
fixed
2 nsassumption. - Run one contrib workflow if your project uses moved helper APIs.
- Confirm your project no longer emits migration warnings for
chip_id,shots,interval, legacy figure payload keys, or old import paths.
Rollback notes
If you need to roll back:
- Restore the previous
v1.4.8environment or reinstall from thev1.4.8tag. - Restore the previous configuration snapshot if you changed file layout or
introduced
system.yaml. - Revert notebook and script updates that depend on
qubex.system,qubex.contrib, or backend-derived timing.
Because v1.5.0 still accepts several legacy inputs as compatibility paths,
you can often stage the migration gradually: update imports and runtime
selection first, then move parameter files and warning-producing call sites.