Skip to content

Add :ucc_simplify pre-process strategy via UCC integration #17

Description

@jack-champagne

Note

Regular enhancement issue — not part of unitaryHACK 2026, no bounty attached.

Motivation

UCC (repo) is Unitary Foundation's frontend-agnostic gate-level meta-compiler — it orchestrates Qiskit, Cirq, PyTKET, BQSKit, and PyZX behind a single ucc.compile entry point. It is complementary to Stretto, not competitive: UCC optimizes at the gate level, Stretto synthesizes at the pulse level. The natural pipeline is:

user circuit → UCC (gate-level optimization) → Stretto (pulse synthesis) → hardware

A UCC pre-process gives Stretto users best-of-N gate-level optimization across UCC's full backend roster for the price of one optional dependency, and positions Stretto inside an existing OSS compiler ecosystem rather than asking researchers to adopt a parallel one.

Scope

Register a new compilation strategy :ucc_simplify (or a pre_process hook reusable by other strategies) that:

  1. Serializes the input GateCircuit to OpenQASM 3 via to_openqasm3.
  2. Calls ucc.compile(qasm_str, return_format="qasm3", target_gateset=device_native_gateset(device)) via PythonCall.jl.
  3. Parses the resulting QASM 3 string back to a GateCircuit via from_openqasm3.
  4. Hands the simplified GateCircuit to the existing partitioner.

Where

Recommended: a new package extension StrettoUCCExt at ext/StrettoUCCExt.jl, with PythonCall as a weak dep in Project.toml. Keeps the core package free of the Python dependency. The strategy registers itself in the extension's __init__().

Fallback: register the strategy directly in src/strategy.jl and gate execution behind a runtime PythonCall check.

API sketch

register_strategy!(
    :ucc_simplify;
    pre_process = ucc_simplify_pre_process,
)

function ucc_simplify_pre_process(circuit::GateCircuit, device::AbstractDevice; ucc_kwargs...)
    qasm_in  = to_openqasm3(circuit)
    ucc      = pyimport("ucc")
    gateset  = device_native_gateset(device)
    qasm_out = pyconvert(String, ucc.compile(
        qasm_in;
        return_format = "qasm3",
        target_gateset = pylist(gateset),
        ucc_kwargs...
    ))
    return from_openqasm3(qasm_out)
end

New helper: device_native_gateset

Add device_native_gateset(device::AbstractDevice) -> Set{String} in src/devices.jl. For TransmonDevice: e.g. {"cz", "rz", "rx", "ry"} (or {"cx", ...} — see (a) below). Dispatch on device type for other platforms.

Pinning and environment

  • Pin UCC to v0.4.12 (latest as of 2025-12-18) in docs and the PR description.
  • UCC requires Python ≥ 3.12.
  • Install:
    pip install "ucc==0.4.12"
    using Pkg; Pkg.add("PythonCall")
  • CI: do not auto-install UCC on every PR — keep it opt-in. A separate tag-gated job runs the integration tests with UCC available.

Definition of done

  • Extension loads without error when using PythonCall, Stretto is executed.
  • compile(circuit, device, :ucc_simplify) returns a CompilationReport with the same logical-unitary fidelity as compile(circuit, device, :default) to ≤ 1e-10.
  • Benchmark testitem on QFT-3, Toffoli, CCZ showing pulse-level fidelity-at-fixed-duration is at least non-worse under :ucc_simplify vs :default, and ideally improves on at least one. Report wall-clock for both stages.
  • New docs/literate/ucc_integration.jl walking through install + usage on QFT-3.
  • register_strategy! lives in the extension's __init__() so the strategy is discoverable via the existing registry once the extension loads.

Notes

  • UCC's target_backend (Qiskit BackendV2) would give connectivity-aware compilation but requires building a Qiskit backend from a Stretto TransmonDevice. Skip for v1 — target_gateset alone is a meaningful win. Connectivity-aware compilation is a separate follow-up.
  • UCC's custom_passes accepts Qiskit TransformationPass subclasses. Not needed for v1 (UCCDefault1 is already strong) but flag as a future hook for Stretto-specific simplifications.
  • BQSKit / MPSPass are opt-in via custom_passes=[BQSKitTransformationPass()]. Expose via a kwarg like enable_bqskit::Bool=false rather than enabling unconditionally.
  • Prefer PythonCall.jl over PyCall.jl. Use @pyconst for the imported ucc module to avoid repeated import overhead.

Maintainer decisions (open)

  • (a) Native-gateset convention. {"cx", "rz", "rx", "ry"} (Heron-style) vs {"cz", "rz", "rx", "ry"} (Sycamore-style). Probably dispatched on device type — define the rule in device_native_gateset.
  • (b) Strategy location. Extension (StrettoUCCExt) vs core with optional dep. Extension is cleaner but strategy availability becomes import-order-dependent.
  • (c) Accept the dep at all. PythonCall + UCC in the test matrix has maintenance cost. If the answer is "not in core, but yes in a separate Stretto-UCC.jl shim," redirect there.
  • (d) CI policy. UCC integration on every PR (slow) vs tag/release only (faster, less coverage)?

Prerequisites

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions