Skip to content

Dependency issues

This page lists defects and version-specific behavior of NWQLib's dependencies that change results or constrain installation, and what NWQLib does about each. The Python package versions are those of the validated environment in docs/ENVIRONMENT_LOCK.txt of the repository: Qiskit 2.5.2, qiskit-aer 0.17.2, SciPy 1.18.1 and urllib3 2.8.0. The NWQ-Sim revision is efd0226 or a descendant with the same roundoff source files, such as 202f5ca (NWQ-Sim). Where a test or review detects a change in the dependency, the entry names it.

Dependency Issue Effect NWQLib's handling
Qiskit 2.5.2 Two-qubit synthesis replaces Weyl coordinates close to a special class Entry errors measured up to 4.2e-5 for two-qubit unitaries near the identity Exact synthesis in subroutines/_dense_synthesis.py
Qiskit 2.5.2 Quantum Shannon decomposition omits small multiplexed rotations Rotations of at most 1e-10 rad are dropped The same exact synthesis
Qiskit 2.5.2 UnitaryGate.control accepts a synthesis under a loose check and falls back to Isometry Entry errors up to 7.9e-6 measured, and ValueError on valid input Controlled matrices are synthesized exactly
Qiskit 2.5.2 The UnitaryGate constructor rechecks unitarity with a fixed tolerance ValueError for a power of a unitary that NWQLib accepted Powers skip the constructor check after NWQLib's own unitarity check
Qiskit 2.5.2 The instruction counts of a controlled U or RZ gate have no published closed form The gate-wise control law depends on the Qiskit version Measured tables, checked against the installed Qiskit
Qiskit 2.5.2 TwoQubitWeylDecomposition(..., fidelity=None).circuit() adds the wrong global phase Returns -U for some inputs Not used
Qiskit 2.5.2 TwoQubitPeepholeOptimization on a CX target Global phase of pi for some CX patterns, which leaves IBM counts and expectation values unchanged Levels 2 and 3 run the pass, the default level 1 does not
Qiskit 2.5.2 QPY writes a native UCGate that it cannot load Saved circuits fail to load Versioned NWQLIB-QPY-UC1 container
Qiskit 2.5.2 Multi-controlled X synthesis counts have no published closed form Resource laws would depend on the version Measured table in subroutines/_mcx_counts.py, checked against the installed Qiskit
Qiskit 2.5.2 The rotation inventory of MCPhaseGate and its dirty-ancilla multi-controlled X gates is defined only by the gate definitions The QHD rotation law depends on the version Law written from the 2.5.2 definitions, checked against the installed definitions
NWQEC 0.1.2 Rz cleanup with a fixed 1e-4, angle grouping to four significant digits and decimal-string angle transport, all outside the requested synthesis tolerance A compiled rotation can err by 2500 times the requested epsilon Requested epsilon recorded as requested, QHD's achieved synthesis error unavailable
Qiskit 2.5.2 The public gate methods check every appended gate, and the fast path uses undocumented CircuitInstruction.from_standard and StandardGate Appending the gates of a large synthesis took five times as long through the public methods Fast path, compared with the public gate methods by a test
SciPy 1.18.1 scipy.linalg.expm recomputes the superdiagonal of a triangular input with a cancelling divided difference Relative errors up to 2.7e-2 measured for close diagonal entries The LCHS references send triangular matrices to scipy.sparse.linalg.expm
SciPy 1.18.1 onenormest draws from NumPy's global random generator scipy.sparse.linalg.expm and expm_multiply advance the caller's random stream, and their chosen parameters depend on it These calls run inside _linalg_laws.seeded_norm_estimates
SciPy 1.18.1 scipy.linalg.expm derives its squaring count from an overflowed norm A 3 x 3 input of 1-norm 2**39 did not return within 20 s The LCHS references refuse a 1-norm above 2**37
qiskit-aer 0.17.2 Fusion and kernels set the roundoff of exact readouts Probability windows depend on the version Constant derived for 0.17.2, other versions listed as an exclusion
NWQ-Sim b35763d Uninitialized memory counters and U phase overflow Undefined behavior Revision rejected, roundoff constant derived for efd0226
urllib3 before 2.8.0 Streaming-reader defects Unbounded buffering and known security issues The ionq extra requires urllib3 2.8.0 or later
qnexus 0.49.0 Declares pandas >=2,<3 pip check reports a conflict with pandas 3 Not worked around, the qualified installation is documented
Platform C library and NumPy No documented error bound for math.sin, math.cos and NumPy's float64 sin The QHD binary angle-formation and AQFT bounds assume at most one ulp Stated as a conditional premise, tested on macOS arm64 and Linux aarch64, not checked on Linux x86-64

Qiskit 2.5.2

Two-qubit synthesis replaces Weyl coordinates

TwoQubitBasisDecomposer always builds its TwoQubitWeylDecomposition with fidelity 1 - 1e-9, whether or not approximate=False is passed (Rust sources of the 2.5.2 tag, crates/synthesis/src/two_qubit_decompose/basis_decomposer.rs, call_inner, and common.rs, DEFAULT_FIDELITY). The decomposition then replaces the Weyl coordinates by those of the first special class, starting with the identity class, whose average gate fidelity reaches that value (weyl_decomposition.rs, new_inner). Average gate fidelity is quadratic in the dropped coordinates, so coordinates up to about 3.5e-5 can disappear together with the entangling part of the gate. For U = exp(-i t G), with G the Hermitian part of a 4-by-4 matrix whose entries have standard normal real and imaginary parts, the definition of UnitaryGate(U) had entry errors between 0.5 t and 3.0 t for t up to 1e-5 over 60 generators, and at most 4.2e-5 for larger t. A coordinate just above the identity threshold can go to another special class, so exp(i a XX) lost its entangling part for a up to 3.5e-5 and still had entry error a at a = 4.2e-5. Qiskit uses this decomposer for two-qubit UnitaryGate definitions and for the two-qubit leaves of its Shannon decomposition.

NWQLib synthesizes dense unitaries with dense_unitary_circuit in subroutines/_dense_synthesis.py, which computes the KAK decomposition without that replacement. Only deviations within ROUNDING_WINDOW (2^-46) are rounded to a special value. Exact dense synthesis lists the constructions that use it: LCHS dense_exact branches, dense block encodings in QLS and QSP, dense LCU SELECT, QPE dense powers, coherent QPE and the MPS preparation. exact_dense_unitaries in subroutines/qiskit_compat.py applies it before compilation for NWQ-Sim, Nexus, IonQ with its QIS gateset, IBM optimization levels 0 and 1, and Aer when a noise model's gate basis omits the unitary instruction. Noiseless Aer, and Aer whose noise basis includes it, apply the matrix directly. Recheck with tests/test_dense_synthesis.py, which bounds the entry error of the synthesis by 64 unit roundoffs times the dimension for t from 1e-10 to 10.

The Shannon decomposition omits small rotations

Qiskit's quantum Shannon decomposition drops multiplexed RZ rotations of at most 1e-10 rad and treats a block within 1e-12 of the identity as the identity (crates/synthesis/src/qsd.rs, get_ucrz and quantum_shannon_decomposition). NWQLib's block-ZXZ synthesis in subroutines/_dense_synthesis.py keeps every nonzero multiplexed rotation, and tests/test_dense_synthesis.py covers it with the same sweep.

Controlled UnitaryGate

UnitaryGate.control (qiskit/circuit/library/generalized_gates/unitary.py) decomposes the whole controlled matrix with qs_decomposition and keeps the result when matrix_equal holds with atol=1e-7 and the default relative tolerance 1e-5. Otherwise it uses Isometry. UnitaryGate._define does the same for an uncontrolled gate on three or more qubits. The check accepts entry errors of about 1e-7 + 1e-5 |C_ij| for entry C_ij of the controlled matrix, and a sweep of X, Y and Z rotations by angles from 1e-8 to 1e-2 rad with one to three controls measured errors up to 7.9e-6. The fallback raised ValueError("Input matrix is not unitary.") for RX(1e-7) with three controls, which is a valid input.

controlled in subroutines/qiskit_compat.py builds a ControlledGate whose definition is controlled_unitary_circuit, the exact synthesis of the closed-control matrix. It uses no check and no fallback. For generic inputs its CX count equals Qiskit's or is lower. For some inputs with one system qubit and two controls, Qiskit took 8 CX where the exact synthesis took 10. Every count stays within the law in Exact dense synthesis. The base gate stays the UnitaryGate, so inverse() of the result still goes through Qiskit's UnitaryGate.control, and inverse_realized_gate in the same module reverses the exact definition instead. tests/test_dense_synthesis.py includes the three-control RX(1e-7) case.

The cost of Qiskit's path lies mostly in its check, not in its decomposition. For a random 512 × 512 unitary with one control, measured with Qiskit 2.5.2 on macOS arm64 with one thread, Qiskit's qs_decomposition of the 1024 × 1024 controlled matrix took 4.9 s and gave 272,044 CX in 751,616 instructions. controlled_unitary_circuit took 5.3 s and gave the same 272,044 CX in 718,848 instructions. In a separate run, UnitaryGate.control did not finish within 120 s, because its check forms the operator of the whole definition, a few M² multiply-adds for each of its roughly 750,000 instructions with M = 1024. The two decompositions scale alike, as M³ in classical work and as M² in CX. NWQLib relies on the exactness of each factorization, checked by tests/test_dense_synthesis.py at small sizes, and forms no operator on each call.

The UnitaryGate constructor

UnitaryGate(U) checks that U^dagger U equals the identity within atol=1e-8 and rtol=1e-5 (qiskit/quantum_info/operators/predicates.py). A power of a unitary that NWQLib accepted accumulates the input's defect, so the constructor rejected U^10 for a unitary with defect 8e-9. QPE checks the unitarity of a supplied matrix once when it plans the method and builds the dense and coherent QPE powers with check_input=False. test_dense_qpe_power_of_an_admitted_near_unitary_input_builds in tests/test_dense_synthesis.py builds the tenth power of such an input.

Instructions of a controlled gate

When qiskit_compat.controlled controls a composite gate, Qiskit's add_control (qiskit/circuit/_add_control.py, apply_basic_controlled_gate) unrolls it and replaces each gate by a multi-controlled one. With k >= 2 controls a U gate becomes two multi-controlled RZ, a multi-controlled RY and an MCPhase, and each multi-controlled RZ is the circuit of Qiskit's _mcsu2_real_diagonal, composed inline. Its dirty-ancilla X blocks are synthesized in Rust (synth_mcx_n_dirty_i15), and their size has no published closed form. CONTROLLED_U_INSTRUCTIONS and CONTROLLED_RZ_INSTRUCTIONS in subroutines/_dense_synthesis.py store the number of instructions that Qiskit 2.5.2 emits for 1 to 64 controls, and _controlled_heavy_instructions gives those that hold angles or are Python objects, including the fifteen P gates of each dirty-ancilla X block on three controls. gatewise_control_counts reads them for the gate-wise control law (exact dense synthesis). The gate-wise route of dense_control_route depends on these counts for each dense unitary it controls. The whole-matrix route synthesizes the controlled matrix with controlled_unitary_circuit instead, and the counts still price a later gate-wise control of that circuit, such as the parity control of a QSP pass (exact dense synthesis). test_gatewise_control_tables_match_installed_qiskit and test_gatewise_heavy_counts_match_installed_qiskit in tests/test_synthesis_admission.py recompute every entry and fail when the installed Qiskit gives other counts.

Sign of the unspecialized two-qubit circuit

TwoQubitWeylDecomposition(U, fidelity=None).circuit() adds the global phase of the wrong one-qubit factor (weyl_decomposition.rs, where the sum uses c2r.global_phase in place of c1r.global_phase). It returned -U for 45 of 100 random two-qubit unitaries, and for near-identity inputs. NWQLib does not call this method.

Two-qubit peephole optimization

On a target whose two-qubit gate is CX, the TwoQubitPeepholeOptimization pass of optimization levels 2 and 3 turned cx(0,1) cx(1,0) cx(0,1) cx(0,1) sx(0) sx(0) cx(0,1) into minus the same operator when it ran alone. Its entry error is 2.0, and 2e-16 after removing a global phase of pi. A global phase does not change the counts or expectation values of a whole compiled circuit. Separately, levels 2 and 3 resynthesize two-qubit blocks with Qiskit's own synthesis, so IBM Runtime preparation at those levels leaves dense UnitaryGates to Qiskit and their circuits carry the synthesis errors above. Level 1, the default, uses the exact synthesis (IBM Runtime).

QPY and UCGate

QPY in Qiskit 2.5.2 writes a native UCGate but cannot load it, because it passes the gate's matrix table as separate positional constructor arguments, whereas UCGate expects one list. This is a constructor reconstruction defect, not evidence of a reversed qubit order.

NWQLib stores such a gate as a compact QPY instruction carrying its matrix table and explicit reconstruction fields, in a versioned container whose file prefix is NWQLIB-QPY-UC1. The reader restores the nominal number of qubits, the existing simplified table, the active controls, up_to_diagonal, the label and any already materialized definition. Circuit phase and wire associations remain attached to their original circuit. Under Qiskit's convention, the target is wire 0 and simplified control indices are one-based within the gate, from 1 through num_qubits - 1. Loading does not rerun multiplexor simplification or expand the table to all nominal controls, and saving does not synthesize a UCGate or construct its full matrix.

Load these .qpy entries with NWQLib's loaders load_run and load_result (Circuit files). Circuit entries and Qiskit UCGate compatibility describes the container. The prefix makes them containers rather than standalone input for qiskit.qpy.load. Circuit files without this adaptation remain ordinary QPY. An old raw QPY entry affected by the defect still reports its original Qiskit failure with the entry path, because the loader cannot infer missing reconstruction fields from it.

test_native_ucgate_qpy_limitation_canary in tests/test_qpy_archive.py exercises raw QPY on one two-qubit UCGate, with no simulation or synthesis. It fails, and so calls for a compatibility review, when native loading starts to succeed or its failure changes. A Qiskit fix does not by itself end support for existing UC1 files. The decoder stays, and the simplified controls, diagonal flags and executable round trips are verified before the writer changes. The UC1 storage is not switched off automatically on the basis of a Qiskit version number.

Multi-controlled X counts

Qiskit's synthesis of a multi-controlled X gate without ancillas has no published closed-form CX count. MCX_CX_BY_CONTROLS in subroutines/_mcx_counts.py stores the counts measured with Qiskit 2.5.2 for 1 to 64 controls, and the resource laws of QLS and LCHS read them. test_multi_controlled_x_cx_table_matches_installed_qiskit_synthesis in tests/test_mcx_counts.py synthesizes the gates again and fails when the installed Qiskit gives other counts.

Standard-gate instructions

dense_unitary_circuit appends up to 11 M²/8 gates for an M-square unitary. QuantumCircuit.u, cx, rz and h check and broadcast their arguments for every gate in QuantumCircuit._append_standard_gate, which took 0.27 s for the 179,200 gates of a 512 × 512 controlled matrix (macOS arm64, one thread). _dense_synthesis._standard_gate_appender builds each instruction with CircuitInstruction.from_standard, as _append_standard_gate does, and adds it with QuantumCircuit._append, which Qiskit documents as a fast path for callers that have checked their arguments. The same gates took 0.055 s. from_standard is undocumented, and its first argument is a member of StandardGate, which the private module qiskit._accelerate.circuit defines. qiskit.circuit.quantumcircuit also imports it, but neither that module nor qiskit.circuit lists it in __all__, and the Qiskit 2.5.2 API reference documents no public location for it. The exactness tests in tests/test_dense_synthesis.py compare each synthesized circuit with its target matrix, so they fail when either interface changes. A missing name stops every synthesis, and a changed meaning of the parameters or qubits of an appended gate changes the operator of the circuit.

SciPy 1.18.1

Triangular matrix exponential

scipy.linalg.expm is implemented in scipy/linalg/src/_matfuncs_expm.c. For a triangular input that needs squarings, matrix_exponential_z and its real and single-precision variants recompute the diagonal and the first superdiagonal after each squaring, as Code Fragment 2.1 of Al-Mohy and Higham (2009), doi:10.1137/09074721X, prescribes (References). They evaluate the superdiagonal entry as t (exp(l2) - exp(l1))/(l2 - l1) and switch to t exp(l1) only when the two diagonal entries are exactly equal. When the entries are close but distinct, the difference cancels, and the relative error grows like the unit roundoff divided by their distance. For the augmented matrix [[-A*T, b*T], [0, 0]] of the LCHS closed form with A = diag(40, s), T = 1, u0 = (0.5, i) and b = (0.2, -0.1+0.3i), the relative error of the solution against a 60-digit mpmath evaluation was 3.9e-12, 2.6e-10, 5.4e-6 and 2.7e-2 at s = 1e-6, 1e-8, 1e-12 and 1e-16. The same spectrum rotated to a matrix that is not triangular gave at most 1.5e-15, and A = [[40, 1], [0, s]] behaved like the diagonal case. The homogeneous exponential of the upper triangular -[[40, 1], [0, 40 + 1e-10]] had a relative entry error of 4.5e-7.

scipy.sparse.linalg.expm evaluates that superdiagonal with _eq_10_42 in scipy/sparse/linalg/_matfuncs.py, Eq. (10.42) of Higham's Functions of Matrices, doi:10.1137/1.9780898717778, which does not cancel. On upper and lower triangular inputs with diagonal spreads up to 4000 and gaps down to 1e-16, its solution error stayed at or below 6.5e-16. On stiff inputs that are not triangular it is less accurate, because _solve_P_Q forms the Padé quotient as (V - U)^{-1}(V + U) where the C kernel forms I + 2 (V - U)^{-1} U, and the difference grows with the number of squarings. At ||A|| T of about 1e4 its closed-form error was 2.3e-13, against 3.1e-16 for scipy.linalg.expm.

The LCHS expm and closed_form references therefore evaluate a triangular matrix with scipy.sparse.linalg.expm, a lower triangular one through its transpose, and every other matrix with scipy.linalg.expm (_exponential in algorithms/lchs/references.py). The call runs inside the random-state guard described below. test_closed_form_is_accurate_for_close_diagonal_entries_and_singular_a in tests/test_lchs_verification.py fails when a triangular input reaches scipy.linalg.expm. The other scipy.linalg.expm calls in NWQLib exponentiate a skew-Hermitian matrix, the LCHS branch unitary exp(-i t (k L + H)), or a real antisymmetric one, a fermionic generator block. Such a matrix is diagonal when it is triangular, and the C kernel exponentiates a diagonal matrix entry by entry without squaring.

Random draws of the norm estimates

onenormest in scipy/sparse/linalg/_onenormest.py draws the +-1 starting columns of its 1-norm estimate and their resamples with np.random.randint, from NumPy's legacy global generator. scipy.sparse.linalg.expm calls it from dimension 200, and expm_multiply for a shifted 1-norm above 63.36, condition (3.13) of Al-Mohy and Higham (2011), doi:10.1137/100788860. A call therefore advances the caller's random stream, and the estimated norms, which choose the Taylor or Padé parameters, depend on the caller's state. _linalg_laws.seeded_norm_estimates seeds the generator for the call and restores the caller's state afterwards, so the same input gives the same draws. The LCHS references and the classical QHD evolutions, including their fidelity references, make these calls inside it. test_classical_host_evolution_keeps_the_global_random_state in tests/test_qhd_workflow.py and test_triangular_reference_exponential_keeps_the_global_random_state in tests/test_lchs_verification.py check both properties. scipy.linalg.expm estimates its norms with LAPACK zlacn2, which draws nothing.

Overflowing norms in the matrix exponential

Both kernels choose the Padé degree and the squaring count from 1-norms of M**k for k up to 10 and of |M|**k for k up to 27, formed before scaling. When one of them overflows, pick_pade_structure_z in the C kernel converts the infinite or NaN value to its integer squaring count, a conversion that C leaves undefined. On macOS arm64, 3 x 3 inputs did not return within 20 s at 1-norm 2**39 (a nilpotent block 2**38 [[1, -1], [1, -1]] with a source column) and at 1-norm 1.4e41 (a dissipative matrix), although both exponentials are representable in binary64. scipy.sparse.linalg.expm raises OverflowError or ValueError on the same inputs. The bound ||M||_1 <= 2**37 keeps every one of those norms below 2**999, and the LCHS references report a matrix above it as unknown before calling either kernel. fermionic_circuits.build_generator_circuit refuses an angle whose product with one of its shared-index occupation blocks has a 1-norm above the bound.

qiskit-aer 0.17.2

The roundoff window of an exact Aer readout uses a per-instruction constant derived from the fusion pass, kernels and gate matrices of qiskit-aer 0.17.2 (the comment above the constants in _validation.py). Aer's statevector method fuses only circuits wider than its default fusion_threshold of 14 qubits. A 14-qubit circuit reported no fusion, and a 15-qubit circuit did. Another installed Aer version adds "unchecked qiskit-aer version" to the preparation record's probability_window_exclusions (_ROUNDOFF_CHECKED_AER_VERSION in backends/qiskit_aer.py). After an upgrade, compare the fusion pass, kernels and gate matrices with the derivation before changing _ROUNDOFF_CHECKED_AER_VERSION (Aer).

NWQEC 0.1.2

In NWQEC 0.1.2 (tag v0.1.2, commit d93299c2a0fe47fb7758bff02b456acfb3ac4416), RemoveTrivialRzPass removes Rz angles within a fixed 1e-4 of trivial values, angle grouping keeps four significant digits, and GridSynth receives each angle as std::to_string(angle). These steps lie outside the tolerance requested through rz_err and epsilon. Compiling H followed by Rz(pi/2 + 5e-5) through compile_logical with rz_err="total" and epsilon=1e-8 gave H followed by S, with no T gate and a phase-aligned operator error of 2.5e-5, about 2500 times the request (Qiskit 2.5.2, macOS arm64). LogicalCompilation therefore records the epsilon as requested and states that it is not a verified achieved precision, and the QHD error ledger keeps the achieved synthesis and compiler errors unavailable (QHD guide).

Repeated compiles of one circuit give different Clifford counts. The one-step QHD circuit of the QHD guide, on (x - 1/5)**2 over [-1, 1] with three grid points, compiled with rz_err="total" and epsilon=1e-4 from the same input QASM digest, gave 347 or 355 H gates, 168 to 210 S gates and 6 or 7 X gates in four compiles, two in each of two processes, while the 347 T and 1 T-inverse gates and the 10 CX gates stayed the same (Qiskit 2.5.2, macOS arm64). The QHD documentation quotes only the T count of a compiled circuit. A statement about a compiled Clifford count needs the counts of repeated compiles.

NWQ-Sim

At revision b35763d, the CPU constructors increment an uninitialized memory counter, and the U matrix factory can overflow the sum of finite phases. backends/nwqsim.py rejects that revision before lowering. Upstream revision efd0226 fixes both, and the roundoff constant of NWQ-Sim readouts was derived for it. The preparation record of a runner whose revision does not descend from efd0226 lists "unchecked NWQ-Sim revision" in probability_window_exclusions, and a descendant that changed any file the derivation reads lists "changed NWQ-Sim roundoff sources". Those files are the value type and fusion switch (include/config.hpp, include/nwq_util.hpp), the circuit's gate records (include/circuit.hpp, include/private/sim_gate.hpp), the fusion pass, the U/CX matrices and the CPU/SV kernels (_ROUNDOFF_SOURCES and _ROUNDOFF_BASE_REVISION in backends/nwqsim.py). When upstream changes those files, compare them with the derivation before moving _ROUNDOFF_BASE_REVISION to a later revision. The NVGPU_MPI route stays blocked, because its constructors increment an uninitialized gpu_mem at both revisions (NWQ-Sim).

urllib3

The IonQ response reader relies on HTTPResponse.read(amt) returning at most amt decoded bytes, which holds from urllib3 2. Versions 2.6.0 through 2.8.0 fix streaming-reader security issues: decompression into the library buffer on small reads (GHSA-2xpw-w6gg-jr37), a bypass of that fix, and unbounded chunk-size lines in chunked responses. The ionq extra therefore requires urllib3 2.8.0 or later (IonQ, Install and first result).

Platform math libraries

QHD's binary angle-formation bound (algorithms/qhd/circuit_errors.py::binary_angle_formation) encloses the finite-difference kinetic energies with at most one ulp of error in math.sin, math.cos and NumPy's float64 sin, and the AQFT bound (algorithms/qhd/binary.py::qft_error_bound) assumes the same of math.sin. Python takes these functions from the platform C library, NumPy may use its own SIMD implementations, and neither documents a uniform error bound. The bounds were tested against independent exact evaluations on macOS arm64 and Linux aarch64, by the full test suites on both platforms (Python 3.12.14 with the same package versions on both hosts). NumPy's SIMD sin on Linux x86-64 hosts, where it can dispatch to AVX-512 code, has not been checked, so there the bound status of these entries rests on an unverified premise. The spectral enclosures use no trigonometric function.

qnexus

qnexus 0.49.0 declares pandas>=2,<3, while the qualified environment keeps pandas 3.0.5. pip check reports this conflict. Offline conversion, reference and result checks passed in that environment, and Quantinuum Nexus describes the qualified installation. NWQLib does not work around the declaration. The Nexus Offline Qualification workflow in .github/workflows/nexus-qualification.yml requires exactly this conflict and fails on any other. qnexus 0.49.0 also imports selene_core.trace without declaring selene-core, so the nexus extra requires selene-core>=0.3.2.