Source code for qward.metrics.structural_metrics

"""
Structural Metrics implementation for QWARD.

This module provides the StructuralMetrics class for analyzing the structural
properties of quantum circuits within the QWARD framework. Structural metrics
characterize how a circuit is organized, independent of the specific gates or
quantum states involved.

These metrics capture global and topological attributes such as circuit depth,
width, size, density, connectivity, and structural complexity indicators adopted
from software engineering.

[1]J. Zhao, “Some Size and Structure Metrics for Quantum Software.” 2021.
[Online]. Available: https://arxiv.org/abs/2103.08815

"""

import math
from typing import Dict, Set, Any, List, Tuple, Optional

from qiskit import QuantumCircuit
from qiskit.converters import circuit_to_dag
from qiskit.circuit import Parameter

from qward.metrics.base_metric import MetricCalculator
from qward.metrics.types import MetricsType, MetricsId

# Import schemas for structured data validation
try:
    from qward.schemas.structural_metrics_schema import StructuralMetricsSchema

    SCHEMAS_AVAILABLE = True
except ImportError:
    SCHEMAS_AVAILABLE = False

# =============================================================================
# Quantum Gate Classification Constants (from Halstead metrics)
# =============================================================================

# Quantum operators (gates)
QUANTUM_OPERATORS = {
    # Single-qubit gates
    "x",
    "y",
    "z",
    "h",
    "s",
    "sdg",
    "t",
    "tdg",
    "rx",
    "ry",
    "rz",
    "u1",
    "u2",
    "u3",
    "p",
    "u",
    "sx",
    "sxdg",
    "rzx",
    "phase",
    "reset",
    # Two-qubit gates
    "cx",
    "cy",
    "cz",
    "swap",
    "iswap",
    "dcx",
    "ecr",
    "rxx",
    "ryy",
    "rzz",
    "xx_minus_yy",
    "xx_plus_yy",
    # Multi-qubit gates
    "ccx",
    "cswap",
    "mcx",
    "mcphase",
    "mcu1",
    "mcu2",
    "mcu3",
    "mcrx",
    "mcry",
    "mcrz",
    "mcp",
    "mcu",
    "mcswap",
}

# Classical operators
CLASSICAL_OPERATORS = {"measure", "barrier", "delay", "snapshot", "save", "initialize", "finalize"}

# Quantum operands (qubits)
QUANTUM_OPERANDS = {"q", "qubit"}  # Qubit references

# Classical operands
CLASSICAL_OPERANDS = {"c", "clbit", "cbit"}  # Classical bit references

# Parameter operands
PARAMETER_OPERANDS = {"theta", "phi", "lambda", "gamma", "beta", "alpha"}


[docs] class StructuralMetrics(MetricCalculator): """ Extract structural metrics from QuantumCircuit objects. This class analyzes the structural organization of a quantum circuit and computes metrics that describe its shape, topology, and logical arrangement. Structural metrics include circuit depth, width, size, density, connectivity, and software-engineering-inspired metrics such as LOC and Halstead adapted to quantum circuit representations. Attributes: circuit (QuantumCircuit): The quantum circuit to analyze (inherited from MetricCalculator). _circuit_dag (DAGCircuit | None): DAG representation of the circuit, used to compute depth, connectivity, structural relationships, and topological features. """ @property def id(self): return MetricsId.STRUCTURAL.value
[docs] def __init__(self, circuit: QuantumCircuit): """ Initialize the StructuralMetrics calculator. Args: circuit: The quantum circuit to analyze """ super().__init__(circuit) self._circuit_dag = circuit_to_dag(circuit) if circuit else None self._ensure_schemas_available()
def _get_metric_type(self) -> MetricsType: """ Get the type of this metric. Returns: MetricsType: PRE_RUNTIME (can be calculated without execution) """ return MetricsType.PRE_RUNTIME def _get_metric_id(self) -> MetricsId: """ Get the ID of this metric. Returns: MetricsId: STRUCTURAL """ return MetricsId.STRUCTURAL
[docs] def is_ready(self) -> bool: """ Check if the metric is ready to be calculated. Returns: bool: True if the circuit is available, False otherwise """ return self.circuit is not None
[docs] def get_metrics(self) -> StructuralMetricsSchema: """ Calculate and return structural metrics combining LOC, Halstead, and circuit structure. Returns: StructuralMetricsSchema: Validated schema with all metrics Raises: ImportError: If schemas are not available """ if not SCHEMAS_AVAILABLE: raise ImportError("StructuralMetricsSchema is not available") # Calculate LOC metrics loc_metrics = self._calculate_loc_metrics() # Calculate Halstead metrics halstead_metrics = self._calculate_halstead_metrics() # Calculate circuit structure metrics structure_metrics = self._calculate_structure_metrics() return StructuralMetricsSchema( # LOC Metrics phi1_total_loc=loc_metrics["phi1_total_loc"], phi2_gate_loc=loc_metrics["phi2_gate_loc"], phi3_measure_loc=loc_metrics["phi3_measure_loc"], phi4_quantum_total_loc=loc_metrics["phi4_quantum_total_loc"], phi5_num_qubits=loc_metrics["phi5_num_qubits"], phi6_num_gate_types=loc_metrics["phi6_num_gate_types"], # Halstead Metrics unique_operators=halstead_metrics["unique_operators"], unique_operands=halstead_metrics["unique_operands"], total_operators=halstead_metrics["total_operators"], total_operands=halstead_metrics["total_operands"], program_length=halstead_metrics["program_length"], vocabulary=halstead_metrics["vocabulary"], estimated_length=halstead_metrics["estimated_length"], volume=halstead_metrics["volume"], difficulty=halstead_metrics["difficulty"], effort=halstead_metrics["effort"], # Circuit Structure Metrics width=structure_metrics["width"], depth=structure_metrics["depth"], max_dens=structure_metrics["max_dens"], avg_dens=structure_metrics["avg_dens"], size=structure_metrics["size"], )
def _calculate_loc_metrics(self) -> Dict[str, Any]: """ Calculate LOC (Lines of Code) related metrics. Returns: Dict[str, Any]: Dictionary with LOC metrics """ ( phi1_total_loc, phi2_gate_loc, phi3_measure_loc, phi4_quantum_total_loc, ) = self._estimate_loc_from_circuit() # ϕ5: Número de cúbits usados phi5_num_qubits = self.circuit.num_qubits # ϕ6: Número del tipo de compuertas usadas unique_gates = set() for instruction in self.circuit.data: gate_name = instruction.operation.name if gate_name not in {"measure", "barrier", "reset"}: unique_gates.add(gate_name) phi6_num_gate_types = len(unique_gates) return { "phi1_total_loc": phi1_total_loc, "phi2_gate_loc": phi2_gate_loc, "phi3_measure_loc": phi3_measure_loc, "phi4_quantum_total_loc": phi4_quantum_total_loc, "phi5_num_qubits": phi5_num_qubits, "phi6_num_gate_types": phi6_num_gate_types, } def _calculate_halstead_metrics(self) -> Dict[str, Any]: """ Calculate Halstead complexity metrics. Returns: Dict[str, Any]: Dictionary with Halstead metrics """ # Initialize counters operators = set() operands = set() total_operators = 0 total_operands = 0 quantum_operands = set() classical_operands = set() parameter_operands = set() qubit_operands = set() classical_bit_operands = set() # Analyze circuit instructions for instruction in self.circuit.data: gate_name = instruction.operation.name.lower() if gate_name == "barrier": continue # Skip barrier instructions total_operators += 1 operators.add(gate_name) # Analyze operands (qubits and classical bits) for qubit in instruction.qubits: qubit_index = self.circuit.find_bit(qubit).index qubit_ref = f"q{qubit_index}" operands.add(qubit_ref) quantum_operands.add(qubit_ref) qubit_operands.add(qubit_ref) total_operands += 1 for clbit in instruction.clbits: clbit_index = self.circuit.find_bit(clbit).index clbit_ref = f"c{clbit_index}" operands.add(clbit_ref) classical_operands.add(clbit_ref) classical_bit_operands.add(clbit_ref) total_operands += 1 # Analyze parameters for param in instruction.operation.params: if isinstance(param, Parameter): param_name = param.name.lower() # Check if parameter name matches known patterns for pattern in PARAMETER_OPERANDS: if pattern in param_name: parameter_operands.add(param_name) operands.add(param_name) total_operands += 1 break # Calculate derived metrics unique_operators = len(operators) unique_operands = len(operands) program_length = total_operators + total_operands vocabulary = unique_operators + unique_operands # Estimated length calculation if unique_operators > 0 and unique_operands > 0: estimated_length = unique_operators * math.log2( unique_operators ) + unique_operands * math.log2(unique_operands) else: estimated_length = 0.0 # Volume calculation if vocabulary > 0: volume = program_length * math.log2(vocabulary) else: volume = 0.0 # Difficulty calculation if unique_operands > 0: difficulty = (unique_operators / 2) * (total_operands / unique_operands) else: difficulty = 0.0 # Effort calculation effort = difficulty * volume return { "unique_operators": unique_operators, "unique_operands": unique_operands, "total_operators": total_operators, "total_operands": total_operands, "program_length": program_length, "vocabulary": vocabulary, "estimated_length": estimated_length, "volume": volume, "difficulty": difficulty, "effort": effort, } def _calculate_structure_metrics(self) -> Dict[str, Any]: """ Calculate circuit structure metrics (width, depth, density, size). Returns: Dict[str, Any]: Dictionary with structure metrics """ # Width: Number of qubits width = self.circuit.num_qubits # Depth: Maximum number of operations applied to any qubit depth = self.circuit.depth() # Size: Total number of operations/gates size = len(self.circuit.data) # Density calculations max_dens, avg_dens = self._calculate_circuit_density() return { "width": width, "depth": depth, "max_dens": max_dens, "avg_dens": avg_dens, "size": size, } def _calculate_circuit_density(self) -> Tuple[int, float]: """ Calculate the maximum and average number of operations applied to qubits. Returns: Tuple[int, float]: (maximum_density, average_density) """ # Convertir a DAG para acceder a niveles de paralelismo dag = circuit_to_dag(self.circuit) # Obtener las capas (cada capa son operaciones paralelas) layers = list(dag.layers()) # Contar operaciones por capa densities = [] for layer in layers: ops = list(layer["graph"].op_nodes()) densities.append(len(ops)) if not densities: return 0, 0.0 # Calcular métricas max_dens = max(densities) avg_dens = sum(densities) / len(densities) return max_dens, avg_dens def _estimate_loc_from_circuit(self) -> Tuple[int, int, int, int]: """ Estimate LOC metrics from circuit instructions Returns: Tuple[int, int, int, int]: (total_loc, gate_loc, measure_loc, quantum_total_loc) """ total_loc = len(self.circuit.data) gate_loc = 0 measure_loc = 0 for instruction in self.circuit.data: gate_name = instruction.operation.name.lower() if gate_name != "barrier": if gate_name == "measure": measure_loc += 1 elif gate_name in QUANTUM_OPERATORS: gate_loc += 1 else: # Treat unknown gates as quantum gates gate_loc += 1 quantum_total_loc = gate_loc + measure_loc return total_loc, gate_loc, measure_loc, quantum_total_loc def _ensure_schemas_available(self): """ Ensure that the required schemas are available. Raises: ImportError: If schemas are not available """ if not SCHEMAS_AVAILABLE: raise ImportError( "StructuralMetricsSchema is not available. " "Please ensure that the schemas module is properly imported." )