Skip to content

Validation API

The qubitos.validation module provides validation utilities for quantum-specific data types with configurable strictness levels.

Overview

The validation system provides:

  • Quantum-specific validators: Hermiticity, unitarity, fidelity ranges
  • Physics constraints: T1/T2 coherence time relationships
  • Strictness modes: STRICT (raises exceptions) or LENIENT (logs warnings)

Quick Start

from qubitos.validation import (
    validate_unitary,
    validate_fidelity,
)
import numpy as np

# Validate a unitary matrix
U = np.array([[0, 1], [1, 0]], dtype=complex)  # Pauli-X
result = validate_unitary(U)
print(f"Valid: {result.valid}")
print(f"Errors: {result.errors}")

# Validate fidelity value
result = validate_fidelity(0.999)
print(f"Valid: {result.valid}")

Strictness Modes

STRICT Mode (Default)

Validation failures raise exceptions:

from qubitos.validation import set_strictness, Strictness, ValidationError

set_strictness(Strictness.STRICT)

try:
    result = validate_fidelity(1.5)  # Invalid: > 1
except ValidationError as e:
    print(f"Error: {e}")

LENIENT Mode

Validation failures log warnings but continue:

set_strictness(Strictness.LENIENT)

result = validate_fidelity(1.5)  # Logs warning, continues
print(f"Valid: {result.valid}")  # False

Environment Variable

Set strictness via environment:

# Strict mode (default)
export QUBITOS_STRICT_VALIDATION=true

# Lenient mode
export QUBITOS_STRICT_VALIDATION=false

Matrix Validators

Hermitian Matrices

Validates \(H = H^\dagger\):

from qubitos.validation import validate_hermitian
import numpy as np

# Valid Hermitian matrix
H = np.array([[1, 1j], [-1j, 2]], dtype=complex)
result = validate_hermitian(H)
print(f"Hermitian: {result.valid}")  # True

# Invalid (not Hermitian)
H_bad = np.array([[1, 1j], [1j, 2]], dtype=complex)
result = validate_hermitian(H_bad)
print(f"Hermitian: {result.valid}")  # False
print(f"Errors: {result.errors}")

Unitary Matrices

Validates \(U^\dagger U = I\):

from qubitos.validation import validate_unitary
import numpy as np

# Valid unitary (Hadamard)
H = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2)
result = validate_unitary(H)
print(f"Unitary: {result.valid}")  # True

# With custom tolerance
result = validate_unitary(H, tolerance=1e-12)

Fidelity Validation

Validates fidelity is in range [0, 1]:

from qubitos.validation import validate_fidelity

# Valid fidelity
result = validate_fidelity(0.999, name="gate_fidelity")
print(f"Valid: {result.valid}")

# Invalid cases
validate_fidelity(-0.1)   # Error: < 0
validate_fidelity(1.5)    # Error: > 1
validate_fidelity(float('nan'))  # Error: NaN

# Warning for suspicious values
result = validate_fidelity(0.3)  # Warning: suspiciously low
print(f"Warnings: {result.warnings}")

Pulse Validation

Validate pulse envelope arrays:

from qubitos.validation import validate_pulse_envelope
import numpy as np

envelope = np.random.randn(100) * 10  # Random pulse

result = validate_pulse_envelope(
    envelope,
    max_amplitude=50.0,    # MHz
    num_time_steps=100,
    name="I_envelope",
)

print(f"Valid: {result.valid}")
print(f"Errors: {result.errors}")
print(f"Warnings: {result.warnings}")

Validate Both I and Q

Validate each quadrature envelope independently:

from qubitos.validation import validate_pulse_envelope

i_result = validate_pulse_envelope(i_pulse, 100.0, 100, name="I envelope")
q_result = validate_pulse_envelope(q_pulse, 100.0, 100, name="Q envelope")
valid = i_result.valid and q_result.valid

Calibration Validation

T1/T2 Coherence Times

Physics constraint: T2 ≤ 2·T1

from qubitos.validation import validate_calibration_t1_t2

# Valid
result = validate_calibration_t1_t2(t1_us=100, t2_us=80)
print(f"Valid: {result.valid}")  # True

# Warning (T2 > T1 is unusual)
result = validate_calibration_t1_t2(t1_us=100, t2_us=120)
print(f"Warnings: {result.warnings}")

# Error (T2 > 2*T1 violates physics)
result = validate_calibration_t1_t2(t1_us=100, t2_us=250)
print(f"Errors: {result.errors}")

Full Calibration Validation

Compose the T1/T2 and fidelity validators to check a full qubit record:

from qubitos.validation import validate_calibration_t1_t2, validate_fidelity

t_result = validate_calibration_t1_t2(t1_us=100, t2_us=80)
ro_result = validate_fidelity(0.99, name="readout_fidelity")
gate_result = validate_fidelity(0.999, name="gate_fidelity")
valid = t_result.valid and ro_result.valid and gate_result.valid

Hamiltonian Validation

A Hamiltonian must be Hermitian. Use validate_hermitian with a descriptive name for clearer error messages:

from qubitos.validation import validate_hermitian
import numpy as np

H = np.array([[1, 0], [0, -1]], dtype=complex)
result = validate_hermitian(H, name="Hamiltonian")
print(f"Valid Hamiltonian: {result.valid}")

ValidationResult

All validators return a ValidationResult object:

from qubitos.validation import ValidationResult

result = ValidationResult(
    valid=True,
    errors=[],
    warnings=["Value is close to limit"],
)

# Boolean conversion
if result:
    print("Validation passed")

# Access details
print(result.valid)
print(result.errors)
print(result.warnings)

API Reference

Enums and Types

qubitos.validation.Strictness

Bases: Enum

Validation strictness level.

qubitos.validation.ValidationResult dataclass

ValidationResult(valid: bool, errors: list[str], warnings: list[str])

Result of a validation check.

qubitos.validation.ValidationError

ValidationError(message: str, field: str | None = None, value: Any = None)

Bases: Exception

Raised when validation fails in strict mode.

Source code in src/qubitos/validation/__init__.py
def __init__(self, message: str, field: str | None = None, value: Any = None):
    self.field = field
    self.value = value
    super().__init__(message)

Strictness Control

qubitos.validation.get_strictness

get_strictness() -> Strictness

Get current validation strictness.

Source code in src/qubitos/validation/__init__.py
def get_strictness() -> Strictness:
    """Get current validation strictness."""
    env_value = os.environ.get("QUBITOS_STRICT_VALIDATION", "true").lower()
    if env_value in ("false", "0", "no", "lenient"):
        return Strictness.LENIENT
    return _strictness

qubitos.validation.set_strictness

set_strictness(strictness: Strictness) -> None

Set validation strictness.

Source code in src/qubitos/validation/__init__.py
def set_strictness(strictness: Strictness) -> None:
    """Set validation strictness."""
    global _strictness
    _strictness = strictness

Matrix Validators

qubitos.validation.validate_hermitian

validate_hermitian(matrix: ndarray, tolerance: float = 1e-10, name: str = 'matrix') -> ValidationResult

Validate that a matrix is Hermitian (H = H^dag).

Parameters:

Name Type Description Default
matrix ndarray

Complex numpy array to validate

required
tolerance float

Maximum allowed deviation from Hermiticity

1e-10
name str

Name of the matrix for error messages

'matrix'

Returns:

Type Description
ValidationResult

ValidationResult with any errors found

Source code in src/qubitos/validation/__init__.py
def validate_hermitian(
    matrix: np.ndarray, tolerance: float = 1e-10, name: str = "matrix"
) -> ValidationResult:
    """Validate that a matrix is Hermitian (H = H^dag).

    Args:
        matrix: Complex numpy array to validate
        tolerance: Maximum allowed deviation from Hermiticity
        name: Name of the matrix for error messages

    Returns:
        ValidationResult with any errors found
    """
    errors = []
    warnings: list[str] = []

    if matrix.ndim != 2:
        errors.append(f"{name} must be 2-dimensional, got {matrix.ndim}D")
        return ValidationResult(False, errors, warnings)

    if matrix.shape[0] != matrix.shape[1]:
        errors.append(f"{name} must be square, got shape {matrix.shape}")
        return ValidationResult(False, errors, warnings)

    # Check Hermiticity: H - H^dag should be zero
    diff = matrix - matrix.conj().T
    max_deviation = np.max(np.abs(diff))

    if max_deviation > tolerance:
        errors.append(
            f"{name} is not Hermitian: max deviation {max_deviation:.2e} > tolerance {tolerance:.2e}"
        )
    elif max_deviation > tolerance / 100:
        warnings.append(f"{name} Hermiticity: deviation {max_deviation:.2e} is close to tolerance")

    return ValidationResult(len(errors) == 0, errors, warnings)

qubitos.validation.validate_unitary

validate_unitary(matrix: ndarray, tolerance: float = 1e-10, name: str = 'matrix') -> ValidationResult

Validate that a matrix is unitary (U^dag @ U = I).

Parameters:

Name Type Description Default
matrix ndarray

Complex numpy array to validate

required
tolerance float

Maximum allowed deviation from unitarity

1e-10
name str

Name of the matrix for error messages

'matrix'

Returns:

Type Description
ValidationResult

ValidationResult with any errors found

Source code in src/qubitos/validation/__init__.py
def validate_unitary(
    matrix: np.ndarray, tolerance: float = 1e-10, name: str = "matrix"
) -> ValidationResult:
    """Validate that a matrix is unitary (U^dag @ U = I).

    Args:
        matrix: Complex numpy array to validate
        tolerance: Maximum allowed deviation from unitarity
        name: Name of the matrix for error messages

    Returns:
        ValidationResult with any errors found
    """
    errors = []
    warnings: list[str] = []

    if matrix.ndim != 2:
        errors.append(f"{name} must be 2-dimensional, got {matrix.ndim}D")
        return ValidationResult(False, errors, warnings)

    if matrix.shape[0] != matrix.shape[1]:
        errors.append(f"{name} must be square, got shape {matrix.shape}")
        return ValidationResult(False, errors, warnings)

    # Check unitarity: U^dag @ U - I should be zero
    identity = np.eye(matrix.shape[0], dtype=complex)
    product = matrix.conj().T @ matrix
    diff = product - identity
    max_deviation = np.max(np.abs(diff))

    if max_deviation > tolerance:
        errors.append(
            f"{name} is not unitary: max deviation {max_deviation:.2e} > tolerance {tolerance:.2e}"
        )
    elif max_deviation > tolerance / 100:
        warnings.append(f"{name} unitarity: deviation {max_deviation:.2e} is close to tolerance")

    return ValidationResult(len(errors) == 0, errors, warnings)

Value Validators

qubitos.validation.validate_fidelity

validate_fidelity(fidelity: float, name: str = 'fidelity') -> ValidationResult

Validate that a fidelity value is in valid range [0, 1].

Parameters:

Name Type Description Default
fidelity float

Fidelity value to validate

required
name str

Name for error messages

'fidelity'

Returns:

Type Description
ValidationResult

ValidationResult with any errors found

Source code in src/qubitos/validation/__init__.py
def validate_fidelity(fidelity: float, name: str = "fidelity") -> ValidationResult:
    """Validate that a fidelity value is in valid range [0, 1].

    Args:
        fidelity: Fidelity value to validate
        name: Name for error messages

    Returns:
        ValidationResult with any errors found
    """
    errors = []
    warnings: list[str] = []

    if not isinstance(fidelity, (int, float)):
        errors.append(f"{name} must be a number, got {type(fidelity).__name__}")
        return ValidationResult(False, errors, warnings)

    if np.isnan(fidelity):
        errors.append(f"{name} is NaN")
    elif np.isinf(fidelity):
        errors.append(f"{name} is infinite")
    elif fidelity < 0:
        errors.append(f"{name} must be >= 0, got {fidelity}")
    elif fidelity > 1:
        errors.append(f"{name} must be <= 1, got {fidelity}")

    # Warn if suspiciously low
    if 0 <= fidelity < 0.5:
        warnings.append(f"{name} = {fidelity} is suspiciously low")

    return ValidationResult(len(errors) == 0, errors, warnings)

qubitos.validation.validate_pulse_envelope

validate_pulse_envelope(envelope: ndarray, max_amplitude: float, num_time_steps: int, name: str = 'envelope') -> ValidationResult

Validate a pulse envelope array.

Parameters:

Name Type Description Default
envelope ndarray

Pulse amplitude array

required
max_amplitude float

Maximum allowed amplitude

required
num_time_steps int

Expected number of time steps

required
name str

Name for error messages

'envelope'

Returns:

Type Description
ValidationResult

ValidationResult with any errors found

Source code in src/qubitos/validation/__init__.py
def validate_pulse_envelope(
    envelope: np.ndarray, max_amplitude: float, num_time_steps: int, name: str = "envelope"
) -> ValidationResult:
    """Validate a pulse envelope array.

    Args:
        envelope: Pulse amplitude array
        max_amplitude: Maximum allowed amplitude
        num_time_steps: Expected number of time steps
        name: Name for error messages

    Returns:
        ValidationResult with any errors found
    """
    errors = []
    warnings: list[str] = []

    if not isinstance(envelope, np.ndarray):
        envelope = np.array(envelope)

    # Check length
    if len(envelope) != num_time_steps:
        errors.append(f"{name} length {len(envelope)} != expected {num_time_steps}")

    # Check for NaN/Inf
    if np.any(np.isnan(envelope)):
        errors.append(f"{name} contains NaN values")
    if np.any(np.isinf(envelope)):
        errors.append(f"{name} contains infinite values")

    # Check amplitude bounds
    max_val = np.max(np.abs(envelope))
    if max_val > max_amplitude:
        errors.append(f"{name} max amplitude {max_val:.2f} exceeds limit {max_amplitude:.2f}")

    # Warn if pulse is very small (might be unintentional)
    if max_val < max_amplitude * 0.01:
        warnings.append(
            f"{name} max amplitude {max_val:.2e} is < 1% of limit - pulse may be too weak"
        )

    return ValidationResult(len(errors) == 0, errors, warnings)

qubitos.validation.validate_calibration_t1_t2

validate_calibration_t1_t2(t1_us: float, t2_us: float) -> ValidationResult

Validate T1/T2 coherence times.

Physics constraint: T2 <= 2*T1 (and typically T2 < T1 in practice)

Parameters:

Name Type Description Default
t1_us float

T1 relaxation time in microseconds

required
t2_us float

T2 dephasing time in microseconds

required

Returns:

Type Description
ValidationResult

ValidationResult with any errors found

Source code in src/qubitos/validation/__init__.py
def validate_calibration_t1_t2(t1_us: float, t2_us: float) -> ValidationResult:
    """Validate T1/T2 coherence times.

    Physics constraint: T2 <= 2*T1 (and typically T2 < T1 in practice)

    Args:
        t1_us: T1 relaxation time in microseconds
        t2_us: T2 dephasing time in microseconds

    Returns:
        ValidationResult with any errors found
    """
    errors = []
    warnings: list[str] = []

    # Basic range checks
    if t1_us <= 0:
        errors.append(f"T1 must be positive, got {t1_us}")
    if t2_us <= 0:
        errors.append(f"T2 must be positive, got {t2_us}")

    if errors:
        return ValidationResult(False, errors, warnings)

    # Physics constraint: T2 <= 2*T1
    if t2_us > 2 * t1_us:
        errors.append(f"T2 ({t2_us} us) > 2*T1 ({2 * t1_us} us) violates physics constraint")

    # Typically T2 < T1 in real systems
    if t2_us > t1_us:
        warnings.append(f"T2 ({t2_us} us) > T1 ({t1_us} us) is unusual - verify calibration")

    return ValidationResult(len(errors) == 0, errors, warnings)

Convenience Functions

qubitos.validation.validate_pulse_physics

validate_pulse_physics(duration_ns: float, drive_amplitude_mhz: float, frequency_ghz: float = 5.0, anharmonicity_mhz: float = -330.0) -> ValidationResult

Physics-aware validation for pulse parameters.

Checks: 1. Pulse duration vs Rabi period — warns if shorter than one cycle. 2. Drive amplitude vs anharmonicity — warns if strong enough to excite the 1→2 transition in a transmon.

The Rabi frequency is Ω = drive_amplitude (in angular frequency units). One Rabi cycle = 1/Ω. If duration < one cycle, the pulse cannot complete a full rotation.

For transmon qubits, the 0→1 drive should satisfy Ω << |α| where α is the anharmonicity, otherwise leakage to |2⟩ occurs.

Koch et al. (2007), Phys. Rev. A 76, 042319.

DOI: 10.1103/PhysRevA.76.042319

Parameters:

Name Type Description Default
duration_ns float

Pulse duration in nanoseconds.

required
drive_amplitude_mhz float

Drive amplitude in MHz.

required
frequency_ghz float

Qubit frequency in GHz (default 5.0).

5.0
anharmonicity_mhz float

Transmon anharmonicity in MHz (default -330).

-330.0

Returns:

Type Description
ValidationResult

ValidationResult with physics-based warnings.

Source code in src/qubitos/validation/__init__.py
def validate_pulse_physics(
    duration_ns: float,
    drive_amplitude_mhz: float,
    frequency_ghz: float = 5.0,
    anharmonicity_mhz: float = -330.0,
) -> ValidationResult:
    """Physics-aware validation for pulse parameters.

    Checks:
    1. Pulse duration vs Rabi period — warns if shorter than one cycle.
    2. Drive amplitude vs anharmonicity — warns if strong enough to
       excite the 1→2 transition in a transmon.

    The Rabi frequency is Ω = drive_amplitude (in angular frequency units).
    One Rabi cycle = 1/Ω. If duration < one cycle, the pulse cannot
    complete a full rotation.

    For transmon qubits, the 0→1 drive should satisfy Ω << |α| where
    α is the anharmonicity, otherwise leakage to |2⟩ occurs.

    Ref: Koch et al. (2007), Phys. Rev. A 76, 042319.
        DOI: 10.1103/PhysRevA.76.042319

    Args:
        duration_ns: Pulse duration in nanoseconds.
        drive_amplitude_mhz: Drive amplitude in MHz.
        frequency_ghz: Qubit frequency in GHz (default 5.0).
        anharmonicity_mhz: Transmon anharmonicity in MHz (default -330).

    Returns:
        ValidationResult with physics-based warnings.
    """
    errors: list[str] = []
    warnings: list[str] = []

    if duration_ns <= 0:
        errors.append(f"Pulse duration must be positive (got {duration_ns} ns)")
        return ValidationResult(False, errors, warnings)

    if drive_amplitude_mhz <= 0:
        errors.append(f"Drive amplitude must be positive (got {drive_amplitude_mhz} MHz)")
        return ValidationResult(False, errors, warnings)

    # Check 1: Duration vs Rabi period
    # Rabi period T_Rabi = 1/Ω (in ns, with Ω in GHz)
    omega_ghz = drive_amplitude_mhz / 1000.0
    if omega_ghz > 0:
        rabi_period_ns = 1.0 / omega_ghz
        if duration_ns < rabi_period_ns:
            warnings.append(
                f"Pulse duration ({duration_ns:.1f} ns) is shorter than one Rabi "
                f"cycle ({rabi_period_ns:.1f} ns at {drive_amplitude_mhz:.1f} MHz "
                f"drive). The pulse cannot complete a full rotation."
            )

    # Check 2: Drive amplitude vs anharmonicity (leakage risk)
    # Rule of thumb: Ω should be < |α|/4 for < 1% leakage
    abs_anharmonicity = abs(anharmonicity_mhz)
    leakage_warn = abs_anharmonicity / 4.0
    leakage_error = abs_anharmonicity / 2.0

    if drive_amplitude_mhz > leakage_error:
        warnings.append(
            f"Drive amplitude ({drive_amplitude_mhz:.1f} MHz) exceeds "
            f"|anharmonicity|/2 = {leakage_error:.1f} MHz. "
            f"High probability of leakage to |2⟩. Consider DRAG correction."
        )
    elif drive_amplitude_mhz > leakage_warn:
        warnings.append(
            f"Drive amplitude ({drive_amplitude_mhz:.1f} MHz) exceeds "
            f"|anharmonicity|/4 = {leakage_warn:.1f} MHz. "
            f"Leakage to |2⟩ may be non-negligible."
        )

    # Check 3: Frequency sanity
    if frequency_ghz < 1.0 or frequency_ghz > 20.0:
        warnings.append(
            f"Qubit frequency {frequency_ghz:.2f} GHz is outside typical "
            f"superconducting qubit range (3-8 GHz)."
        )

    return ValidationResult(len(errors) == 0, errors, warnings)