Skip to content

[BUG] dt duration unit incorrectly mapped to nanoseconds #301

Description

@ryanhill1

Description

The dt duration unit in OpenQASM 3 is backend-dependent — it represents the duration of one waveform sample on the target hardware. It cannot be converted to SI units (ns, us, ms, s) without knowing the backend's sample rate.

However, pyqasm currently treats dt as equivalent to ns during unrolling, producing incorrect duration values.

Reproduction

from pyqasm import loads
import openqasm3.ast as ast

qasm = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
delay[100dt] q[0];
"""

m = loads(qasm)
m.unroll()

for s in m.unrolled_ast.statements:
    if isinstance(s, ast.DelayInstruction):
        print(f"value={s.duration.value}, unit={s.duration.unit.name}")
        # Output: value=100.0, unit=ns

Expected Behavior

delay[100dt] should either:

  1. Preserve dt as the unit in the unrolled AST (value=100.0, unit=dt), or
  2. Raise an error/warning that dt cannot be converted to SI units

Actual Behavior

The duration is reported as value=100.0, unit=ns — treating 1 dt = 1 ns, which is incorrect. The TIME_UNITS_MAP in src/pyqasm/maps/expressions.py does not include a dt entry, so the unit appears to fall through without proper handling.

Reference

From the OpenQASM 3 spec:

The duration type is used for timing of operations. SI units of time (ns, µs, ms, s) are supported. There is also a backend-dependent unit dt, equivalent to the duration of one waveform sample on the backend.

Activity

added theissue type on Mar 24, 2026
changed the title [-]dt duration unit incorrectly mapped to nanoseconds[/-] [+][BUG] dt duration unit incorrectly mapped to nanoseconds[/+] on Mar 24, 2026

ashmitjsg commented on May 25, 2026

@ashmitjsg
Contributor

Hey @ryanhill1, I looked into this and tried to reproduce the bug and traced the root cause, but I think it differs slightly from the diagnosis in the description, so I wanted to confirm the intended fix before opening a PR.

from pyqasm import loads, dumps

qasm = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
delay[100dt] q[0];
"""

m = loads(qasm); m.unroll()
print(dumps(m))   # delay[100.0ns] q[0];  <- should be 100.0dt

Root cause

The expression evaluator in src/pyqasm/expressions.py handles dt correctly, the DurationLiteral branch returns the raw value 100.0 and never touches TIME_UNITS_MAP for the dt case. The mislabeling happens afterwards, in the visitor, in _visit_delay_statement (src/pyqasm/visitor.py, ~L2830):

statement.duration = qasm3_ast.DurationLiteral(
    duration_val,
    unit=(
        qasm3_ast.TimeUnit.dt
        if self._module._device_cycle_time
        else qasm3_ast.TimeUnit.ns   # <- assigns ns even when source unit was dt
    ),
)

The unit is chosen solely from whether device_cycle_time is set, ignoring the original statement.duration.unit. When no device_cycle_time is provided and the source unit is dt, it's incorrectly relabeled ns. SI units (us, ms, s) are fine here, because the evaluator genuinely converts them to ns first, only dt is affected.

The same pattern exists for the box duration in _visit_box_statement (~L2911), so box[200dt] { ... } also emits box[200.0ns].

Question on intended behaviour

The description lists two options. Which one should we prefer?

  1. Preserve dt in the unrolled AST when no device_cycle_time is set (value=100.0, unit=dt), or
  2. Raise/warn that dt can't be converted to SI units without a sample rate

I have a working draft for option 1 that fixes both the delay and box paths with no regressions in the existing test suite.

Minimal change in _visit_delay_statement - preserve the source unit when it was dt:

source_is_dt = (
    isinstance(_delay_time_var, qasm3_ast.DurationLiteral)
    and _delay_time_var.unit == qasm3_ast.TimeUnit.dt
)
statement.duration = qasm3_ast.DurationLiteral(
    duration_val,
    unit=(
        qasm3_ast.TimeUnit.dt
        if self._module._device_cycle_time or source_is_dt
        else qasm3_ast.TimeUnit.ns
    ),
)

(Same adjustment applies to the box path.) With this, delay[100dt] -> delay[100.0dt], delay[2us] -> delay[2000.0ns] (unchanged), and the full test suite shows no new failures.

Let me know if I missed something, and if I can open a PR for this with the suitable fix.

ryanhill1 commented on May 27, 2026

@ryanhill1
MemberAuthor

Hi @ashmitjsg, thanks for your interest in this issue!

Your proposed fix for Option 1 sounds great. Feel free to open a PR

ashmitjsg commented on May 27, 2026

@ashmitjsg
Contributor

Thanks @ryanhill1. Opened #317 with the Option 1 fix (preserves dt for both delay and box). Please check it out, and let me know if there are any other changes required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions