Circuit

The Circuit class in QDK/Chemistry represents a quantum circuit. It stores the circuit in one or more formats and converts between them on demand.

Worked example

The following example defines a Q# operation from a string, wraps the compiled circuit in a Circuit, inspects the gate-level diagram, and runs resource estimation:

import json
from pathlib import Path

import qdk
from qdk import qsharp
from qdk_chemistry.data import Circuit
from qdk_chemistry.data.circuit import QsharpFactoryData

# 1. Define a Q# operation from string
qsharp.eval(
    """
    operation GHZSample(n: Int) : Result[] {
    use qs = Qubit[n];

    H(qs[0]);
    ApplyToEach(CNOT(qs[0], _), qs[1...]);

    let results = MeasureEachZ(qs);
    ResetAll(qs);
    return results;
    }
    """
)

# 2. Get a Q# circuit object and wrap it
qsharp_factory = QsharpFactoryData(
    program=qdk.code.GHZSample,
    parameter={"n": 3},
)
circuit = Circuit(qsharp_factory=qsharp_factory)

# 3. Inspect the circuit
qsharp_circuit = circuit.get_qsharp_circuit()
print(qsharp_circuit)
# Return an ASCII diagram of the circuit
#    q_0    ── H ──── ● ──── ● ──── M ──── |0〉 ──
#                     │      │      ╘════════════
#    q_1    ───────── X ─────┼───── M ──── |0〉 ──
#                            │      ╘════════════
#    q_2    ──────────────── X ──── M ──── |0〉 ──
#                                   ╘════════════

# 4. Access the gate-level JSON structure
circuit_json = json.loads(qsharp_circuit.json())
print(f"Qubits: {len(circuit_json['qubits'])}")

# 5. Resource estimation
estimate_result = circuit.estimate()
formatted = estimate_result["physicalCountsFormatted"]
print(f"Physical qubits: {formatted['physicalQubits']}")
print(f"Runtime: {formatted['runtime']}")

Overview

A Circuit wraps a quantum circuit that may be represented in any combination of the following formats:

Format

Description

qasm

An OpenQASM string.

qir

A QIR (Quantum Intermediate Representation) object for cross-platform compilation.

qsharp

A compiled qdk._native.Circuit object for inspection and visualization.

qsharp_op

A Q# callable — a native Q# operation that can be composed with other Q# operations.

qsharp_factory

A QsharpFactoryData — a deferred Q# program that compiles on demand (see Q# factory harness below).

At least one representation must be provided at construction. When a format that is not stored is requested, the class converts automatically — for example, calling get_qsharp_circuit() on a circuit that only has qasm will compile it to Q# via the qsharp package.

The full constructor signature is:

Circuit(
    qasm: str | None = None,
    qir: QirInputData | str | None = None,
    qsharp: qdk._native.Circuit | None = None,
    qsharp_op: Callable | None = None,
    qsharp_factory: QsharpFactoryData | None = None,
    encoding: str | None = None,
)

The encoding parameter records the fermion-to-qubit mapping assumed by the circuit (e.g., "jordan-wigner").

Q# factory harness

The Q# factory is the primary mechanism by which QDK/Chemistry builds circuits internally. Rather than constructing a circuit as a gate sequence, the library stores a reference to a Q# callable along with the classical parameters that configure it. The circuit is compiled only when a concrete representation is actually needed.

A QsharpFactoryData is a frozen dataclass with two fields:

program

A Q# callable — a reference to a Q# operation that constructs the circuit. In practice this is typically a Q# factory function exposed via the qsharp Python package (e.g., QSHARP_UTILS.StatePreparation.MakeStatePreparationCircuit).

parameter

A dict[str, Any] of classical parameters that are passed to the Q# callable at compilation time. These are Python-native values — lists, ints, floats, nested dicts, or even other Q# operations — that the Q# program uses to configure the circuit it produces.

The standard pattern for building a factory is:

# Prepare single-reference state with Q# factory parameters
from qdk_chemistry.data import Circuit
from qdk_chemistry.utils.qsharp import QSHARP_UTILS

# 1. Build a parameter object (a dictionary or a Q# structured parameter class)
bitstring = [1, 1, 0, 0]
params = QSHARP_UTILS.StatePreparation.SingleReferenceParams(
    bitStrings=bitstring, numQubits=len(bitstring)
)

# 2. Create the factory — vars() converts the dataclass to a dict
factory = QsharpFactoryData(
    program=QSHARP_UTILS.StatePreparation.MakeSingleReferenceStateCircuit,
    parameter=vars(params),
)

# 3. Wrap in a Circuit — nothing is compiled yet
circuit = Circuit(qsharp_factory=factory, encoding="jordan-wigner")

# 4. Compilation happens on demand:
circuit.get_qsharp_circuit()  # → qsharp.circuit(program, **params)
circuit.get_qir()  # → qsharp.compile(program, **params)

This pattern is what enables end-to-end Q# composition: because the factory stores Q# callables rather than serialized gate sequences, multi-stage circuits compose natively in the Q# runtime without intermediate format conversions.

Shared Q# context

QDK enforces that Q# operations composed together belong to the same qdk.Context. Composing a callable from one context into a program owned by another raises QSharpError.

Three scenarios cover how users interact with this:

Using QDK/Chemistry algorithms only. You never touch the Q# context. Every circuit the library builds is created against one internal shared context, so everything composes automatically.

Bringing your own Q# operation. When you define your own Q# program (for example, a custom BellState state preparation) and want to compose it with a chemistry builder such as QPE, it must be built against that same shared context. Obtain the context with get_qsharp_context(), define your operation on it, and pass the resulting callable to the builder.

from qdk_chemistry.algorithms import create
from qdk_chemistry.data import AlgorithmRef, Circuit, QubitOperator
from qdk_chemistry.data.circuit import QsharpFactoryData
from qdk_chemistry.utils.qsharp import get_qsharp_context

# get_qsharp_context() returns the context QDK/Chemistry uses internally. Define your
# own Q# operation against it so it composes with the chemistry builders.
context = get_qsharp_context()
context.eval(
    "operation BellState(qs : Qubit[]) : Unit is Adj + Ctl { H(qs[0]); CNOT(qs[0], qs[1]); }"
)
bell_state = context.eval("BellState")
custom_state_prep = Circuit(
    qsharp_op=bell_state,
    qsharp_factory=QsharpFactoryData(program=bell_state, parameter={}),
)

# Feed the user-defined state prep straight into the standard QPE builder.
builder = create("qpe_circuit_builder", "qdk_standard", num_bits=2)
builder.settings().set(
    "controlled_circuit_mapper",
    AlgorithmRef("controlled_circuit_mapper", "pauli_sequence"),
)
builder.settings().set(
    "unitary_builder", AlgorithmRef("hamiltonian_unitary_builder", "trotter", time=1.0)
)
hamiltonian = QubitOperator(pauli_strings=["XX", "ZZ"], coefficients=[0.25, 0.5])
qpe_circuit = builder.run(
    state_preparation=custom_state_prep, qubit_hamiltonian=hamiltonian
)[0]
qpe_circuit.get_qsharp_circuit()

Bringing your own Q# context. If you want to change the target profile — or any other qdk.Context option — build a context with create_qsharp_context() (it keeps the chemistry utilities loaded on the project root), then make the library use it: set_qsharp_context() installs it process-wide (pass None to restore the default), while use_qsharp_context() switches to it temporarily on the current thread and restores it automatically on exit.

from qdk import TargetProfile
from qdk_chemistry.utils.qsharp import (
    create_qsharp_context,
    get_qsharp_context,
    use_qsharp_context,
)

# create_qsharp_context forwards target_profile (and any other qdk.Context kwargs)
# while keeping the Q# chemistry utilities loaded on the project root.
custom_context = create_qsharp_context(target_profile=TargetProfile.Adaptive_RIF)

# use_qsharp_context temporarily makes it the shared context on this thread, so the
# chemistry utilities and your own operations both resolve against it inside the block.
with use_qsharp_context(custom_context):
    context = get_qsharp_context()  # -> custom_context
    context.eval(
        "operation BellState(qs : Qubit[]) : Unit is Adj + Ctl { H(qs[0]); CNOT(qs[0], qs[1]); }"
    )
    bell_state = context.eval("BellState")
    custom_state_prep = Circuit(
        qsharp_op=bell_state,
        qsharp_factory=QsharpFactoryData(program=bell_state, parameter={}),
    )
    builder = create("qpe_circuit_builder", "qdk_standard", num_bits=2)
    builder.settings().set(
        "controlled_circuit_mapper",
        AlgorithmRef("controlled_circuit_mapper", "pauli_sequence"),
    )
    builder.settings().set(
        "unitary_builder",
        AlgorithmRef("hamiltonian_unitary_builder", "trotter", time=1.0),
    )
    hamiltonian = QubitOperator(pauli_strings=["XX", "ZZ"], coefficients=[0.25, 0.5])
    qpe_circuit = builder.run(
        state_preparation=custom_state_prep, qubit_hamiltonian=hamiltonian
    )[0]
    qpe_circuit.get_qsharp_circuit()

# For a permanent, process-wide switch use set_qsharp_context(custom_context) instead,
# and set_qsharp_context(None) to restore the default.

The four helpers in qdk_chemistry.utils.qsharp that manage this are:

get_qsharp_context()

Returns the shared context that owns QSHARP_UTILS. Build your own operations against it so they compose with the chemistry builders.

create_qsharp_context()

Creates a fresh context preloaded with the chemistry utilities. Forwards target_profile and any other qdk.Context keyword arguments; use it when you need a non-default configuration.

set_qsharp_context()

Installs a caller-supplied context process-wide. Pass None to reset to the default context.

use_qsharp_context()

A context manager that overrides the active context for the current thread only and restores it on exit.

Conversion methods

Each method returns the circuit in the requested format, converting from whatever representation is available:

Method

Description

get_qsharp_circuit()

Returns a qdk._native.Circuit for inspection. Accepts prune_classical_qubits to remove unused qubits.

get_qir()

Returns the QIR representation. Compiles from Q# factory or converts from QASM if needed.

get_qasm()

Returns the OpenQASM string. Converts from QIR via Qiskit if only QIR is available.

get_qiskit_circuit()

Returns a Qiskit QuantumCircuit. Requires qiskit to be installed.

estimate()

Runs Q#’s resource estimator on the circuit. Accepts optional params for estimation configuration.

Example: Convert state preparation circuits to different formats

import numpy as np
from qdk_chemistry.algorithms import create
from qdk_chemistry.data import Circuit, Structure

# When algorithms produce circuits, they carry native Q# operations internally.
# This enables end-to-end Q# composition without format conversions.
coords = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]])
structure = Structure(coords, symbols=["H", "H"])

scf = create("scf_solver")
_, wfn = scf.run(structure, charge=0, spin_multiplicity=1, basis_or_guess="sto-3g")
ham = create("hamiltonian_constructor").run(wfn.get_orbitals())
_, wfn_cas = create("multi_configuration_calculator").run(ham, 1, 1)

# StatePreparation produces a Circuit with a native Q# factory
state_prep = create("state_prep", "sparse_isometry_gf2x")
circuit = state_prep.run(wfn_cas)

# Inspect the Q# circuit (prune unused qubits for clarity)
pruned_qsharp_circuit = circuit.get_qsharp_circuit(prune_classical_qubits=True)
print(pruned_qsharp_circuit)

# Get the QIR representation
qir = circuit.get_qir()
print(qir)

# Export to OpenQASM or Qiskit when needed (if qiskit is installed)
try:
    qasm_str = circuit.get_qasm()
    print(qasm_str)

    qiskit_circuit = circuit.get_qiskit_circuit()
    print(qiskit_circuit)
except (ImportError, RuntimeError):
    print("Qiskit not installed — skipping OpenQASM/Qiskit export")

Properties

Property

Type

Description

qasm

str | None

OpenQASM circuit string, if available.

qir

QirInputData | str | None

QIR representation, if available. Lazily compiled from the Q# factory on first access.

qsharp

qdk._native.Circuit | None

Compiled Q# circuit object, if available.

encoding

str | None

Qubit encoding label (e.g., "jordan-wigner"), if set.

Serialization

Circuit supports the same serialization formats as other QDK/Chemistry data classes:

# Save to JSON
circuit.to_json_file("example_circuit.circuit.json")

# Load from JSON
loaded = Circuit.from_json_file("example_circuit.circuit.json")

# Save to HDF5
circuit.to_hdf5_file("example_circuit.circuit.h5")

# Load from HDF5
loaded_h5 = Circuit.from_hdf5_file("example_circuit.circuit.h5")

Note

Serialization persists the OpenQASM and QIR representations. Q# callables and factories are not directly serializable — they are reconstructed when the producing algorithm re-creates the circuit.

Further reading