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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions