From d40dbb871db245dccbc78b740e14f924c80fac59 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 21 May 2026 09:26:55 -0400 Subject: [PATCH] Fix dt-unit Delay handling in the C API A `QuantumCircuit` with `Delay(n, "dt")` stores its integer duration as `Param::Obj(PyAny)` in Rust, since Param has no integer variant. Two consequences: 1. `qk_circuit_get_instruction` panics in `from_packed_instruction_with_numeric` when it encounters a non-Float/non-ParameterExpression param. 2. `CDelayUnit` has no DT variant, so dt-delays can't be constructed from C at all. A. Coerce Python int to f64 only on the Delay extraction path. B. Add `CDelayUnit::DT` and a new `qk_circuit_delay_dt(u64)` C function. C. Add a `Param::Int(i64)` variant. Rejected: every match over Param across the codebase would need updating. D. Have the C readback path extract ints from `Param::Obj`. Rejected: papers over the type confusion at every call site instead of fixing it at ingestion. E. Make qk_circuit_get_instruction return `QkExitCode` instead of void so it can fail gracefully. Rejected: would break the C ABI. After A, the panic site is unreachable for valid circuits anyway. F. Force Python to store dt durations as float. Rejected: user-visible API change requiring a deprecation cycle. This PR implements A and B. Changes A. In `crates/circuit/src/circuit_instruction.rs`, the Delay branch now uses the standard coercing `params.extract::>()`. Python ints land as Param::Float. Round-trip back to Python is preserved by `Delay.validate_parameter`, which already converts fractional-free floats back to int for the dt unit, so delay.duration is still an int for users. B. `CDelayUnit::DT = 5` plus a `new qk_circuit_delay_dt(circuit, qubit, duration: u64) -> QkExitCode` that stores the duration as `Param::Float(duration as f64)`. --- crates/cext/src/circuit.rs | 43 +++++++++++++++++++ crates/circuit/src/circuit_instruction.rs | 13 +++--- crates/circuit/src/operations.rs | 3 +- .../c-api-dt-delay-2d0bc1fcf6b7214c.yaml | 13 ++++++ test/c/test_circuit.c | 22 ++++++++++ test/python/circuit/test_delay.py | 8 ++++ 6 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/c-api-dt-delay-2d0bc1fcf6b7214c.yaml diff --git a/crates/cext/src/circuit.rs b/crates/cext/src/circuit.rs index cbc724b6eb91..53f900429e2b 100644 --- a/crates/cext/src/circuit.rs +++ b/crates/cext/src/circuit.rs @@ -1909,6 +1909,8 @@ pub enum CDelayUnit { NS = 3, /// Picoseconds. PS = 4, + /// QPU clock cycles. + DT = 5, } impl From for DelayUnit { @@ -1919,6 +1921,7 @@ impl From for DelayUnit { CDelayUnit::US => DelayUnit::US, CDelayUnit::NS => DelayUnit::NS, CDelayUnit::PS => DelayUnit::PS, + CDelayUnit::DT => DelayUnit::DT, } } } @@ -1970,6 +1973,46 @@ pub unsafe extern "C" fn qk_circuit_delay( ExitCode::Success } +/// @ingroup QkCircuit +/// Append a delay instruction with unit ``dt`` to the circuit. +/// +/// The duration is stored internally as a floating-point value; values larger +/// than ``2^53`` lose precision. +/// +/// @param circuit A pointer to the circuit to add the delay to. +/// @param qubit The ``uint32_t`` index of the qubit to apply the delay to. +/// @param duration The duration of the delay in clock cycles. +/// +/// @return An exit code. +/// +/// # Safety +/// +/// Behavior is undefined if ``circuit`` is not a valid, non-null pointer to a ``QkCircuit``. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn qk_circuit_delay_dt( + circuit: *mut CircuitData, + qubit: u32, + duration: u64, +) -> ExitCode { + // SAFETY: Per documentation, the pointer is non-null and aligned. + let circuit = unsafe { mut_ptr_as_ref(circuit) }; + + let duration_param: Param = (duration as f64).into(); + let delay_instruction = StandardInstruction::Delay(DelayUnit::DT); + + let params = Parameters::Params(smallvec![duration_param]); + circuit + .push_packed_operation( + PackedOperation::from_standard_instruction(delay_instruction), + Some(params), + &[Qubit(qubit)], + &[], + ) + .unwrap(); + + ExitCode::Success +} + /// The configuration options for the ``qk_circuit_draw`` function. #[repr(C)] pub struct CircuitDrawerConfig { diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 36b75476f58f..6db74319a124 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -1023,14 +1023,11 @@ pub fn extract_params( match &i { StandardInstruction::Barrier(_) => None, StandardInstruction::Delay(_) => { - // If the delay's duration is a Python int, we preserve it rather than - // coercing it to a float (e.g. when unit is 'dt'). - Some(Parameters::Params( - params - .try_iter()? - .map(|p| Param::extract_no_coerce(p?.as_borrowed())) - .collect::>()?, - )) + // Coerce the duration to `Param::Float`. Python `int` durations (used for + // the 'dt' unit) round-trip back to `int` on the Python side via + // `Delay.validate_parameter`. + let params: SmallVec<[Param; 3]> = params.extract()?; + Some(Parameters::Params(params)) } StandardInstruction::Measure => None, StandardInstruction::Reset => None, diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 475519f5047f..2e829c26d4cc 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -181,8 +181,7 @@ impl Param { if coerce_to_float { Ok(Self::Float(i as f64)) // coerce integer to float } else { - // Int is not a param type and only comes from Python so dump it in - // there until we support DT unit delay from C + // Int is not a `Param` variant; store as a Python object. Python::attach(|py| Ok(Self::Obj(i.into_py_any(py)?))) } } diff --git a/releasenotes/notes/c-api-dt-delay-2d0bc1fcf6b7214c.yaml b/releasenotes/notes/c-api-dt-delay-2d0bc1fcf6b7214c.yaml new file mode 100644 index 000000000000..2423414f0b9c --- /dev/null +++ b/releasenotes/notes/c-api-dt-delay-2d0bc1fcf6b7214c.yaml @@ -0,0 +1,13 @@ +--- +features_c: + - | + Added support for ``dt``-unit delays to the C API. The ``QkDelayUnit`` enum + now includes ``QkDelayUnit_DT``, and a new function + ``qk_circuit_delay_dt(circuit, qubit, duration)`` constructs a delay whose + duration is given in integer clock cycles (``uint64_t``). +fixes: + - | + Fixed a crash where reading a ``QkCircuit`` containing a ``Delay`` with + unit ``"dt"`` (built from Python) via the C API would panic. The integer + duration is now stored as a floating-point value internally, so the + instruction is readable by the C-API instruction-readback path. diff --git a/test/c/test_circuit.c b/test/c/test_circuit.c index 495b1f72509b..518a68e69332 100644 --- a/test/c/test_circuit.c +++ b/test/c/test_circuit.c @@ -1153,6 +1153,28 @@ static int test_delay_instruction(void) { goto cleanup; } + QkExitCode delay_dt_code = qk_circuit_delay_dt(qc, 1, 100); + if (delay_dt_code != QkExitCode_Success) { + result = RuntimeError; + goto cleanup; + } + + // Read back the dt-delay and verify the duration. + QkCircuitInstruction inst; + qk_circuit_get_instruction(qc, 1, &inst); + if (inst.num_params != 1) { + printf("Expected 1 parameter in dt-delay, got %u\n", inst.num_params); + result = EqualityError; + qk_circuit_instruction_clear(&inst); + goto cleanup; + } + double duration = qk_param_as_real(inst.params[0]); + if (fabs(duration - 100.0) > 1e-10) { + printf("Unexpected dt-delay duration: %f\n", duration); + result = EqualityError; + } + qk_circuit_instruction_clear(&inst); + cleanup: qk_circuit_free(qc); return result; diff --git a/test/python/circuit/test_delay.py b/test/python/circuit/test_delay.py index 20df94bd2de7..74d2aed8c217 100644 --- a/test/python/circuit/test_delay.py +++ b/test/python/circuit/test_delay.py @@ -12,6 +12,7 @@ """Test delay instruction for quantum circuits.""" + import copy import pickle @@ -40,6 +41,13 @@ def test_keep_units_after_adding_delays_to_circuit(self): self.assertEqual(qc.data[3].operation.unit, "ns") self.assertEqual(qc.data[4].operation.unit, "dt") + def test_dt_duration_stays_int_after_circuit_roundtrip(self): + qc = QuantumCircuit(1) + qc.delay(100, 0, unit="dt") + duration = qc.data[0].operation.duration + self.assertIsInstance(duration, int) + self.assertEqual(duration, 100) + def test_fail_if_non_integer_duration_with_dt_unit_is_supplied(self): qc = QuantumCircuit(1) with self.assertRaises(CircuitError):