Skip to content

Pulse Generation API

The qubitos.pulsegen module provides the GRAPE (Gradient Ascent Pulse Engineering) optimizer for synthesizing high-fidelity quantum gate pulses.

Overview

Component Description
GrapeOptimizer Main optimization class
GrapeConfig Configuration dataclass
GrapeResult Optimization result
generate_pulse Convenience function
hamiltonians Hamiltonian construction utilities

Quick Start

from qubitos.pulsegen import generate_pulse

# Generate an X-gate pulse
result = generate_pulse("X", duration_ns=20, target_fidelity=0.999)

print(f"Converged: {result.converged}")
print(f"Fidelity: {result.fidelity:.6f}")
print(f"I envelope: {result.i_envelope[:5]}...")
print(f"Q envelope: {result.q_envelope[:5]}...")

Using GrapeOptimizer

For more control, use the GrapeOptimizer class directly:

from qubitos.pulsegen import GrapeOptimizer, GrapeConfig
from qubitos.pulsegen.hamiltonians import get_target_unitary

# Configure optimization
config = GrapeConfig(
    num_time_steps=100,
    duration_ns=50,
    max_iterations=200,
    target_fidelity=0.999,
    learning_rate=1.0,
)

# Create optimizer
optimizer = GrapeOptimizer(config)

# Get target unitary
target = get_target_unitary("H")  # Hadamard gate

# Run optimization
result = optimizer.optimize(
    target_unitary=target,
    num_qubits=1,
)

With Custom Hamiltonians

from qubitos.pulsegen.hamiltonians import parse_pauli_string

# Define system Hamiltonian
H0 = parse_pauli_string("5.0 * Z0", num_qubits=1)
Hc = [
    parse_pauli_string("X0", num_qubits=1),
    parse_pauli_string("Y0", num_qubits=1),
]

result = optimizer.optimize(
    target_unitary=target,
    num_qubits=1,
    drift_hamiltonian=H0,
    control_hamiltonians=Hc,
)

Configuration Options

Essential Parameters

config = GrapeConfig(
    num_time_steps=100,      # Pulse discretization (more = finer control)
    duration_ns=50.0,        # Total pulse duration
    target_fidelity=0.999,   # Stop when reached
    max_iterations=1000,     # Maximum optimization steps
    learning_rate=1.0,       # Gradient ascent step size
)

Advanced Parameters

config = GrapeConfig(
    # ... essential parameters ...
    convergence_threshold=1e-8,  # Stop if progress < this
    max_amplitude=100.0,         # Maximum pulse amplitude (MHz)
    use_second_order=False,      # Enable GRAPE-II (experimental)
    regularization=0.0,          # L2 penalty on pulses
    random_seed=42,              # For reproducibility
)

Supported Gates

Single-Qubit Gates

Gate Description
X Pauli-X (NOT)
Y Pauli-Y
Z Pauli-Z
H Hadamard
SX √X gate
RX Rotation around X (requires angle)
RY Rotation around Y (requires angle)
RZ Rotation around Z (requires angle)

Two-Qubit Gates

Gate Description
CZ Controlled-Z
CNOT Controlled-NOT (CX)
ISWAP iSWAP gate

Custom Gates

import numpy as np

# Define custom unitary
custom_gate = np.array([
    [1, 0],
    [0, np.exp(1j * np.pi / 8)],
], dtype=np.complex128)

result = optimizer.optimize(
    target_unitary=custom_gate,
    num_qubits=1,
)

Optimization Callbacks

Monitor optimization progress:

def callback(iteration: int, fidelity: float) -> bool:
    print(f"Iter {iteration}: F = {fidelity:.6f}")
    # Return True to stop early
    return fidelity > 0.9999

result = optimizer.optimize(
    target_unitary=target,
    num_qubits=1,
    callback=callback,
)

Warm Starting

Use a previous result as starting point:

# First optimization
result1 = optimizer.optimize(target_unitary=X_gate, num_qubits=1)

# Warm start for similar gate
result2 = optimizer.optimize(
    target_unitary=Y_gate,
    num_qubits=1,
    initial_pulses=(result1.i_envelope, result1.q_envelope),
)

Hamiltonians Module

The hamiltonians submodule provides utilities for constructing quantum Hamiltonians.

Pauli String Parsing

from qubitos.pulsegen.hamiltonians import parse_pauli_string

# Single term
H = parse_pauli_string("X0", num_qubits=1)

# Multiple terms with coefficients
H = parse_pauli_string("5.0 * Z0 + 0.1 * X0", num_qubits=1)

# Multi-qubit
H = parse_pauli_string("0.01 * Z0 Z1", num_qubits=2)

Standard Unitaries

from qubitos.pulsegen.hamiltonians import get_target_unitary
import numpy as np

# Standard gates
X = get_target_unitary("X")
H = get_target_unitary("H")

# Rotation gates
RX_90 = get_target_unitary("RX", angle=np.pi/2)

# Two-qubit gates
CZ = get_target_unitary("CZ", num_qubits=2)

# Embedded in larger space
X_on_q1 = get_target_unitary("X", num_qubits=3, qubit_indices=[1])

Building Hamiltonians

from qubitos.pulsegen.hamiltonians import build_hamiltonian

H0, Hc = build_hamiltonian(
    drift="5.0 * Z0",
    controls=["X0", "Y0"],
    num_qubits=1,
)

API Reference

Main Classes

qubitos.pulsegen.grape.GrapeOptimizer

GrapeOptimizer(config: GrapeConfig | None = None)

GRAPE pulse optimizer.

Implements gradient ascent pulse engineering for quantum gate synthesis.

Initialize the optimizer.

Parameters:

Name Type Description Default
config GrapeConfig | None

Optimization configuration. Uses defaults if None.

None
Source code in src/qubitos/pulsegen/grape.py
def __init__(self, config: GrapeConfig | None = None):
    """Initialize the optimizer.

    Args:
        config: Optimization configuration. Uses defaults if None.
    """
    self.config = config or GrapeConfig()
    self._rng = np.random.default_rng(self.config.random_seed)

optimize

optimize(
    target_unitary: NDArray[complex128],
    num_qubits: int,
    drift_hamiltonian: NDArray[complex128] | None = None,
    control_hamiltonians: list[NDArray[complex128]]
    | None = None,
    initial_pulses: tuple[
        NDArray[float64], NDArray[float64]
    ]
    | None = None,
    callback: Callable[[int, float], bool] | None = None,
) -> GrapeResult

Optimize pulses to implement a target unitary.

Parameters:

Name Type Description Default
target_unitary NDArray[complex128]

Target unitary matrix to implement

required
num_qubits int

Number of qubits

required
drift_hamiltonian NDArray[complex128] | None

Time-independent drift Hamiltonian (optional)

None
control_hamiltonians list[NDArray[complex128]] | None

List of control Hamiltonians for I and Q

None
initial_pulses tuple[NDArray[float64], NDArray[float64]] | None

Initial (I, Q) pulse envelopes (random if None)

None
callback Callable[[int, float], bool] | None

Called each iteration with (iteration, fidelity). Return True to stop optimization early.

None

Returns:

Type Description
GrapeResult

GrapeResult with optimized pulses and metrics

Raises:

Type Description
ValueError

If parameters are invalid

Source code in src/qubitos/pulsegen/grape.py
def optimize(
    self,
    target_unitary: NDArray[np.complex128],
    num_qubits: int,
    drift_hamiltonian: NDArray[np.complex128] | None = None,
    control_hamiltonians: list[NDArray[np.complex128]] | None = None,
    initial_pulses: tuple[NDArray[np.float64], NDArray[np.float64]] | None = None,
    callback: Callable[[int, float], bool] | None = None,
) -> GrapeResult:
    """Optimize pulses to implement a target unitary.

    Args:
        target_unitary: Target unitary matrix to implement
        num_qubits: Number of qubits
        drift_hamiltonian: Time-independent drift Hamiltonian (optional)
        control_hamiltonians: List of control Hamiltonians for I and Q
        initial_pulses: Initial (I, Q) pulse envelopes (random if None)
        callback: Called each iteration with (iteration, fidelity).
                 Return True to stop optimization early.

    Returns:
        GrapeResult with optimized pulses and metrics

    Raises:
        ValueError: If parameters are invalid
    """
    dim = 2**num_qubits
    n_steps = self.config.num_time_steps

    # When a TimePoint with AWG config is provided, derive n_steps from
    # num_samples so each GRAPE step corresponds to one AWG sample.
    if self.config.duration is not None and self.config.duration.num_samples > 0:
        n_steps = self.config.duration.num_samples

    # Compute dt using the effective (AWG-quantized) duration
    dt = self.config.effective_duration_ns * 1e-9 / n_steps  # seconds

    # Additional safety check (should never trigger due to config validation)
    if dt < MIN_DT_SECONDS:
        logger.warning(
            f"Computed dt={dt:.2e}s is very small, clamping to {MIN_DT_SECONDS:.2e}s"
        )
        dt = MIN_DT_SECONDS

    # Validate target unitary
    if target_unitary.shape != (dim, dim):
        raise ValueError(
            f"Target unitary shape {target_unitary.shape} doesn't match "
            f"expected ({dim}, {dim}) for {num_qubits} qubits"
        )

    # Set up Hamiltonians
    if drift_hamiltonian is None:
        drift_hamiltonian = np.zeros((dim, dim), dtype=np.complex128)

    if control_hamiltonians is None:
        control_hamiltonians = self._default_control_hamiltonians(num_qubits)

    # Initialize pulses
    if initial_pulses is not None:
        i_pulse, q_pulse = initial_pulses
    else:
        # Initialize with significant amplitude to avoid saddle point at U=I
        # For trace-zero targets like X, Y, CZ, the gradient vanishes when U~I
        # Starting with ~25% of max amplitude helps escape this saddle point
        init_amp = 0.25 * self.config.max_amplitude
        i_pulse = self._rng.uniform(-init_amp, init_amp, n_steps)
        q_pulse = self._rng.uniform(-init_amp, init_amp, n_steps)

    # Optimization loop
    fidelity_history = []
    best_fidelity = 0.0
    best_i_pulse = i_pulse.copy()
    best_q_pulse = q_pulse.copy()

    for iteration in range(self.config.max_iterations):
        # Compute forward propagators
        propagators = self._compute_propagators(
            i_pulse, q_pulse, drift_hamiltonian, control_hamiltonians, dt
        )

        # Compute total unitary
        total_unitary = self._chain_propagators(propagators)

        # Compute fidelity (clamped to [0, 1])
        fidelity = self._gate_fidelity(total_unitary, target_unitary)
        fidelity_history.append(fidelity)

        # Update best
        if fidelity > best_fidelity:
            best_fidelity = fidelity
            best_i_pulse = i_pulse.copy()
            best_q_pulse = q_pulse.copy()

        # Check convergence
        if fidelity >= self.config.target_fidelity:
            logger.info(
                f"GRAPE converged at iteration {iteration} with fidelity {fidelity:.6f}"
            )
            return GrapeResult(
                i_envelope=best_i_pulse,
                q_envelope=best_q_pulse,
                fidelity=best_fidelity,
                iterations=iteration + 1,
                converged=True,
                fidelity_history=fidelity_history,
                final_unitary=total_unitary,
                duration=self.config.duration,
                awg_config=self.config.awg_config,
            )

        # Check for stagnation
        if len(fidelity_history) > 10:
            recent_improvement = fidelity_history[-1] - fidelity_history[-10]
            if abs(recent_improvement) < self.config.convergence_threshold:
                logger.info(
                    f"GRAPE stagnated at iteration {iteration} with fidelity {fidelity:.6f}"
                )
                break

        # Callback
        if callback is not None and callback(iteration, fidelity):
            logger.info(f"GRAPE stopped by callback at iteration {iteration}")
            break

        # Compute gradients
        grad_i, grad_q = self._compute_gradients(
            propagators, target_unitary, control_hamiltonians, dt, total_unitary
        )

        # Apply regularization
        if self.config.regularization > 0:
            grad_i -= self.config.regularization * i_pulse
            grad_q -= self.config.regularization * q_pulse

        # Update pulses (gradient ascent) with adaptive learning rate
        lr = self._adaptive_learning_rate(iteration, fidelity_history)
        i_pulse += lr * grad_i
        q_pulse += lr * grad_q

        # Clip to amplitude bounds
        i_pulse = np.clip(i_pulse, -self.config.max_amplitude, self.config.max_amplitude)
        q_pulse = np.clip(q_pulse, -self.config.max_amplitude, self.config.max_amplitude)

        # Log progress
        if iteration % 100 == 0:
            logger.debug(f"Iteration {iteration}: fidelity = {fidelity:.6f}")

    # Return best result
    final_propagators = self._compute_propagators(
        best_i_pulse, best_q_pulse, drift_hamiltonian, control_hamiltonians, dt
    )
    final_unitary = self._chain_propagators(final_propagators)

    return GrapeResult(
        i_envelope=best_i_pulse,
        q_envelope=best_q_pulse,
        fidelity=best_fidelity,
        iterations=len(fidelity_history),
        converged=best_fidelity >= self.config.target_fidelity,
        fidelity_history=fidelity_history,
        final_unitary=final_unitary,
        duration=self.config.duration,
        awg_config=self.config.awg_config,
    )

qubitos.pulsegen.grape.GrapeConfig dataclass

GrapeConfig(
    num_time_steps: int = 100,
    duration_ns: int = 20,
    target_fidelity: float = 0.999,
    max_iterations: int = 1000,
    learning_rate: float = 1.0,
    convergence_threshold: float = 1e-08,
    max_amplitude: float = 100.0,
    use_second_order: bool = False,
    regularization: float = 0.0,
    random_seed: int | None = None,
    duration: TimePoint | None = None,
    awg_config: AWGClockConfig | None = None,
)

Configuration for GRAPE optimization.

Attributes:

Name Type Description
num_time_steps int

Number of time discretization steps (must be >= 1). When duration has an AWG config, num_samples is preferred.

duration_ns int

Total pulse duration in nanoseconds (must be > 0). DEPRECATED in favor of duration TimePoint.

target_fidelity float

Target gate fidelity (0 to 1)

max_iterations int

Maximum optimization iterations

learning_rate float

Initial learning rate for gradient ascent

convergence_threshold float

Stop when fidelity improvement < threshold

max_amplitude float

Maximum pulse amplitude (in MHz)

use_second_order bool

Use second-order (GRAPE-II) optimization

regularization float

L2 regularization strength for pulse smoothness

random_seed int | None

Random seed for reproducibility

duration TimePoint | None

TimePoint carrying nominal + quantized duration with precision/jitter metadata. When set, effective_duration_ns returns the AWG-quantized value and num_time_steps may be derived from duration.num_samples.

awg_config AWGClockConfig | None

AWG clock configuration. Stored on the result for provenance tracking.

effective_dt_seconds property

effective_dt_seconds: float

Time step in SI seconds for the GRAPE propagator.

effective_duration_ns property

effective_duration_ns: float

Return the AWG-quantized duration, or fall back to duration_ns.

__post_init__

__post_init__() -> None

Validate configuration parameters.

Source code in src/qubitos/pulsegen/grape.py
def __post_init__(self) -> None:
    """Validate configuration parameters."""
    if self.num_time_steps < MIN_TIME_STEPS:
        raise ValueError(
            f"num_time_steps must be >= {MIN_TIME_STEPS}, got {self.num_time_steps}"
        )
    if self.duration_ns <= 0:
        raise ValueError(f"duration_ns must be > 0, got {self.duration_ns}")
    if not 0 <= self.target_fidelity <= 1:
        raise ValueError(f"target_fidelity must be in [0, 1], got {self.target_fidelity}")
    if self.max_iterations < 1:
        raise ValueError(f"max_iterations must be >= 1, got {self.max_iterations}")
    if self.max_amplitude <= 0:
        raise ValueError(f"max_amplitude must be > 0, got {self.max_amplitude}")

qubitos.pulsegen.grape.GrapeResult dataclass

GrapeResult(
    i_envelope: NDArray[float64],
    q_envelope: NDArray[float64],
    fidelity: float,
    iterations: int,
    converged: bool,
    fidelity_history: list[float] = list(),
    final_unitary: NDArray[complex128] | None = None,
    duration: TimePoint | None = None,
    awg_config: AWGClockConfig | None = None,
)

Result of GRAPE optimization.

Attributes:

Name Type Description
i_envelope NDArray[float64]

Optimized I (in-phase) pulse envelope

q_envelope NDArray[float64]

Optimized Q (quadrature) pulse envelope

fidelity float

Achieved gate fidelity (clamped to [0, 1])

iterations int

Number of iterations performed

converged bool

Whether optimization converged

fidelity_history list[float]

Fidelity at each iteration

final_unitary NDArray[complex128] | None

The unitary implemented by the optimized pulse

duration TimePoint | None

Quantized duration used for this optimization (provenance).

awg_config AWGClockConfig | None

AWG clock config used for this optimization (provenance).

qubitos.pulsegen.grape.GateType

Bases: Enum

Supported quantum gate types.

Convenience Function

qubitos.pulsegen.grape.generate_pulse

generate_pulse(
    gate: str | GateType,
    num_qubits: int = 1,
    duration_ns: int = 20,
    target_fidelity: float = 0.999,
    qubit_indices: list[int] | None = None,
    config: GrapeConfig | None = None,
) -> GrapeResult

Generate an optimized pulse for a quantum gate.

This is the main entry point for pulse generation.

Parameters:

Name Type Description Default
gate str | GateType

Target gate (e.g., "X", "H", "CZ")

required
num_qubits int

Number of qubits in the system

1
duration_ns int

Pulse duration in nanoseconds (must be > 0)

20
target_fidelity float

Target gate fidelity

0.999
qubit_indices list[int] | None

Indices of target qubits (default: [0] or [0,1])

None
config GrapeConfig | None

Advanced configuration options

None

Returns:

Type Description
GrapeResult

GrapeResult with optimized pulse envelopes

Raises:

Type Description
ValueError

If parameters are invalid

Example

result = generate_pulse("X", duration_ns=20, target_fidelity=0.999) print(f"Fidelity: {result.fidelity:.4f}")

Source code in src/qubitos/pulsegen/grape.py
def generate_pulse(
    gate: str | GateType,
    num_qubits: int = 1,
    duration_ns: int = 20,
    target_fidelity: float = 0.999,
    qubit_indices: list[int] | None = None,
    config: GrapeConfig | None = None,
) -> GrapeResult:
    """Generate an optimized pulse for a quantum gate.

    This is the main entry point for pulse generation.

    Args:
        gate: Target gate (e.g., "X", "H", "CZ")
        num_qubits: Number of qubits in the system
        duration_ns: Pulse duration in nanoseconds (must be > 0)
        target_fidelity: Target gate fidelity
        qubit_indices: Indices of target qubits (default: [0] or [0,1])
        config: Advanced configuration options

    Returns:
        GrapeResult with optimized pulse envelopes

    Raises:
        ValueError: If parameters are invalid

    Example:
        >>> result = generate_pulse("X", duration_ns=20, target_fidelity=0.999)
        >>> print(f"Fidelity: {result.fidelity:.4f}")
    """
    from .hamiltonians import get_target_unitary

    # Convert string to enum
    if isinstance(gate, str):
        gate = GateType(gate.upper())

    # Set up configuration
    if config is None:
        config = GrapeConfig(
            duration_ns=duration_ns,
            target_fidelity=target_fidelity,
        )
    else:
        config.duration_ns = duration_ns
        config.target_fidelity = target_fidelity

    # Get target unitary
    target = get_target_unitary(gate, num_qubits, qubit_indices)

    # Run optimization
    optimizer = GrapeOptimizer(config)
    result = optimizer.optimize(target, num_qubits)

    return result

Hamiltonians

qubitos.pulsegen.hamiltonians.parse_pauli_string

parse_pauli_string(
    expression: str, num_qubits: int
) -> NDArray[np.complex128]

Parse a Pauli string expression into a Hamiltonian matrix.

Format: "coeff1 * P1 P2 + coeff2 * P3 - coeff3 * P4 P5"

Parameters:

Name Type Description Default
expression str

Pauli string expression

required
num_qubits int

Number of qubits

required

Returns:

Type Description
NDArray[complex128]

Hamiltonian matrix

Example

H = parse_pauli_string("0.5 * X0 + 0.3 * Z0 Z1", num_qubits=2)

Source code in src/qubitos/pulsegen/hamiltonians.py
def parse_pauli_string(
    expression: str,
    num_qubits: int,
) -> NDArray[np.complex128]:
    """Parse a Pauli string expression into a Hamiltonian matrix.

    Format: "coeff1 * P1 P2 + coeff2 * P3 - coeff3 * P4 P5"

    Args:
        expression: Pauli string expression
        num_qubits: Number of qubits

    Returns:
        Hamiltonian matrix

    Example:
        >>> H = parse_pauli_string("0.5 * X0 + 0.3 * Z0 Z1", num_qubits=2)
    """
    dim = 2**num_qubits
    H = np.zeros((dim, dim), dtype=np.complex128)

    # Normalize expression
    expression = expression.replace("-", "+-")
    terms = [t.strip() for t in expression.split("+") if t.strip()]

    for term in terms:
        # Parse coefficient and operators
        if "*" in term:
            coeff_str, ops_str = term.split("*", 1)
            coeff = float(coeff_str.strip())
        else:
            # No coefficient means 1.0
            coeff = 1.0
            ops_str = term

        # Parse Pauli operators
        ops_str = ops_str.strip()
        if ops_str:
            matrix = pauli_string_to_matrix(ops_str, num_qubits)
            H += coeff * matrix

    return H

qubitos.pulsegen.hamiltonians.get_target_unitary

get_target_unitary(
    gate: str | GateType,
    num_qubits: int = 1,
    qubit_indices: list[int] | None = None,
    angle: float | None = None,
) -> NDArray[np.complex128]

Get the target unitary for a quantum gate.

Parameters:

Name Type Description Default
gate str | GateType

Gate name or GateType enum

required
num_qubits int

Total number of qubits in the system

1
qubit_indices list[int] | None

Which qubits the gate acts on (default: first qubit(s))

None
angle float | None

Rotation angle for parameterized gates (RX, RY, RZ)

None

Returns:

Type Description
NDArray[complex128]

Unitary matrix for the gate

Example

X = get_target_unitary("X", num_qubits=1) CZ = get_target_unitary("CZ", num_qubits=2, qubit_indices=[0, 1]) RX = get_target_unitary("RX", num_qubits=1, angle=np.pi/2)

Source code in src/qubitos/pulsegen/hamiltonians.py
def get_target_unitary(
    gate: str | GateType,
    num_qubits: int = 1,
    qubit_indices: list[int] | None = None,
    angle: float | None = None,
) -> NDArray[np.complex128]:
    """Get the target unitary for a quantum gate.

    Args:
        gate: Gate name or GateType enum
        num_qubits: Total number of qubits in the system
        qubit_indices: Which qubits the gate acts on (default: first qubit(s))
        angle: Rotation angle for parameterized gates (RX, RY, RZ)

    Returns:
        Unitary matrix for the gate

    Example:
        >>> X = get_target_unitary("X", num_qubits=1)
        >>> CZ = get_target_unitary("CZ", num_qubits=2, qubit_indices=[0, 1])
        >>> RX = get_target_unitary("RX", num_qubits=1, angle=np.pi/2)
    """
    # Handle GateType enum
    gate_str: str = gate.value if hasattr(gate, "value") else gate
    gate_str = gate_str.upper()

    # Handle rotation gates
    if gate_str in ("RX", "RY", "RZ"):
        if angle is None:
            raise ValueError(f"{gate_str} requires an angle parameter")
        axis = gate_str[1]  # X, Y, or Z
        base_gate = rotation_gate(axis, angle)
    elif gate_str in STANDARD_GATES:
        base_gate = STANDARD_GATES[gate_str]
    else:
        raise ValueError(f"Unknown gate: {gate_str}")

    # Determine gate size
    gate_qubits = int(np.log2(base_gate.shape[0]))

    # Set default qubit indices
    if qubit_indices is None:
        qubit_indices = list(range(gate_qubits))

    if len(qubit_indices) != gate_qubits:
        raise ValueError(
            f"Gate {gate} acts on {gate_qubits} qubits, but {len(qubit_indices)} indices provided"
        )

    # If system size matches gate size, return directly
    if num_qubits == gate_qubits:
        return base_gate

    # Otherwise, embed gate in larger Hilbert space
    return embed_gate(base_gate, num_qubits, qubit_indices)

qubitos.pulsegen.hamiltonians.build_hamiltonian

build_hamiltonian(
    drift: str | NDArray[complex128] | None = None,
    controls: list[str]
    | list[NDArray[complex128]]
    | None = None,
    num_qubits: int = 1,
) -> tuple[
    NDArray[np.complex128], list[NDArray[np.complex128]]
]

Build drift and control Hamiltonians.

Parameters:

Name Type Description Default
drift str | NDArray[complex128] | None

Drift Hamiltonian (Pauli string or matrix)

None
controls list[str] | list[NDArray[complex128]] | None

List of control Hamiltonians

None
num_qubits int

Number of qubits

1

Returns:

Type Description
tuple[NDArray[complex128], list[NDArray[complex128]]]

Tuple of (drift_hamiltonian, control_hamiltonians)

Source code in src/qubitos/pulsegen/hamiltonians.py
def build_hamiltonian(
    drift: str | NDArray[np.complex128] | None = None,
    controls: list[str] | list[NDArray[np.complex128]] | None = None,
    num_qubits: int = 1,
) -> tuple[NDArray[np.complex128], list[NDArray[np.complex128]]]:
    """Build drift and control Hamiltonians.

    Args:
        drift: Drift Hamiltonian (Pauli string or matrix)
        controls: List of control Hamiltonians
        num_qubits: Number of qubits

    Returns:
        Tuple of (drift_hamiltonian, control_hamiltonians)
    """
    dim = 2**num_qubits

    # Process drift
    if drift is None:
        H0 = np.zeros((dim, dim), dtype=np.complex128)
    elif isinstance(drift, str):
        H0 = parse_pauli_string(drift, num_qubits)
    else:
        H0 = drift

    # Process controls
    if controls is None:
        # Default: X and Y on each qubit
        Hc = []
        for q in range(num_qubits):
            Hc.append(pauli_string_to_matrix(f"X{q}", num_qubits))
            Hc.append(pauli_string_to_matrix(f"Y{q}", num_qubits))
    else:
        Hc = []
        for ctrl in controls:
            if isinstance(ctrl, str):
                Hc.append(parse_pauli_string(ctrl, num_qubits))
            else:
                Hc.append(ctrl)

    return H0, Hc

qubitos.pulsegen.hamiltonians.rotation_gate

rotation_gate(
    axis: str, angle: float
) -> NDArray[np.complex128]

Generate a rotation gate around a Pauli axis.

R_P(theta) = exp(-i * theta/2 * P) = cos(theta/2) * I - i * sin(theta/2) * P

Parameters:

Name Type Description Default
axis str

Rotation axis ("X", "Y", or "Z")

required
angle float

Rotation angle in radians

required

Returns:

Type Description
NDArray[complex128]

2x2 rotation matrix

Source code in src/qubitos/pulsegen/hamiltonians.py
def rotation_gate(
    axis: str,
    angle: float,
) -> NDArray[np.complex128]:
    """Generate a rotation gate around a Pauli axis.

    R_P(theta) = exp(-i * theta/2 * P)
               = cos(theta/2) * I - i * sin(theta/2) * P

    Args:
        axis: Rotation axis ("X", "Y", or "Z")
        angle: Rotation angle in radians

    Returns:
        2x2 rotation matrix
    """
    c = np.cos(angle / 2)
    s = np.sin(angle / 2)

    if axis.upper() == "X":
        return np.array(
            [
                [c, -1j * s],
                [-1j * s, c],
            ],
            dtype=np.complex128,
        )
    elif axis.upper() == "Y":
        return np.array(
            [
                [c, -s],
                [s, c],
            ],
            dtype=np.complex128,
        )
    elif axis.upper() == "Z":
        return np.array(
            [
                [c - 1j * s, 0],
                [0, c + 1j * s],
            ],
            dtype=np.complex128,
        )
    else:
        raise ValueError(f"Unknown rotation axis: {axis}")

qubitos.pulsegen.hamiltonians.tensor_product

tensor_product(
    operators: list[NDArray[complex128]],
) -> NDArray[np.complex128]

Compute tensor product of a list of operators.

Parameters:

Name Type Description Default
operators list[NDArray[complex128]]

List of 2x2 matrices

required

Returns:

Type Description
NDArray[complex128]

Tensor product matrix

Source code in src/qubitos/pulsegen/hamiltonians.py
def tensor_product(operators: list[NDArray[np.complex128]]) -> NDArray[np.complex128]:
    """Compute tensor product of a list of operators.

    Args:
        operators: List of 2x2 matrices

    Returns:
        Tensor product matrix
    """
    result = operators[0]
    for op in operators[1:]:
        result = np.kron(result, op)  # type: ignore[assignment]
    return result

qubitos.pulsegen.hamiltonians.embed_gate

embed_gate(
    gate: NDArray[complex128],
    num_qubits: int,
    qubit_indices: list[int],
) -> NDArray[np.complex128]

Embed a gate in a larger Hilbert space.

Parameters:

Name Type Description Default
gate NDArray[complex128]

Gate unitary matrix

required
num_qubits int

Total number of qubits

required
qubit_indices list[int]

Which qubits the gate acts on

required

Returns:

Type Description
NDArray[complex128]

Embedded gate matrix

Source code in src/qubitos/pulsegen/hamiltonians.py
def embed_gate(
    gate: NDArray[np.complex128],
    num_qubits: int,
    qubit_indices: list[int],
) -> NDArray[np.complex128]:
    """Embed a gate in a larger Hilbert space.

    Args:
        gate: Gate unitary matrix
        num_qubits: Total number of qubits
        qubit_indices: Which qubits the gate acts on

    Returns:
        Embedded gate matrix
    """
    dim = 2**num_qubits

    # Build the full unitary
    result = np.zeros((dim, dim), dtype=np.complex128)

    for i in range(dim):
        for j in range(dim):
            # Extract the bits for the gate qubits
            i_gate = 0
            j_gate = 0
            for k, q in enumerate(qubit_indices):
                i_gate |= ((i >> q) & 1) << k
                j_gate |= ((j >> q) & 1) << k

            # Check if non-gate qubits match
            i_other = i
            j_other = j
            for q in qubit_indices:
                i_other &= ~(1 << q)
                j_other &= ~(1 << q)

            if i_other == j_other:
                result[i, j] = gate[i_gate, j_gate]

    return result

Constants

from qubitos.pulsegen.hamiltonians import (
    PAULI_I,    # 2x2 identity
    PAULI_X,    # Pauli X matrix
    PAULI_Y,    # Pauli Y matrix
    PAULI_Z,    # Pauli Z matrix
    STANDARD_GATES,  # Dict of standard gate matrices
)