diff --git a/crates/binding-python/cqlib/circuit/operation.pyi b/crates/binding-python/cqlib/circuit/operation.pyi index 0951457..cfde275 100644 --- a/crates/binding-python/cqlib/circuit/operation.pyi +++ b/crates/binding-python/cqlib/circuit/operation.pyi @@ -18,7 +18,6 @@ or classical control). :class:`ValueOperation` pairs an instruction with qubits and parameters to form a complete circuit operation. """ -from typing import Any import numpy as np from numpy.typing import NDArray from .bit import Qubit @@ -112,6 +111,37 @@ class ValueInstruction: @property def is_instruction(self) -> bool: ... @property + def name(self) -> str: + """Human-readable name (e.g. ``"h"``, ``"cx"``, ``"measure"``).""" + ... + @property + def instruction_type(self) -> str: + """One of ``"standard"``, ``"mcgate"``, ``"unitary"``, ``"circuit"``, + ``"directive"``, ``"classical_data"``, ``"classical_control"``, ``"delay"``.""" + ... + @property + def is_standard(self) -> bool: ... + @property + def is_mcgate(self) -> bool: ... + @property + def is_unitary(self) -> bool: ... + @property + def is_circuit_gate(self) -> bool: ... + @property + def is_directive(self) -> bool: ... + @property + def is_classical_data(self) -> bool: ... + @property + def is_delay(self) -> bool: ... + @property + def standard_gate(self) -> StandardGate | None: + """The :class:`StandardGate` if this is a standard-gate instruction.""" + ... + @property + def directive(self) -> Directive | None: + """The :class:`Directive` if this is a directive instruction.""" + ... + @property def instruction(self) -> Instruction | None: """The inner :class:`Instruction` when this is a plain instruction.""" ... @@ -130,17 +160,32 @@ class ValueOperation: This is the public construction boundary. Use the static factories to create operations from gates with their bound parameters preserved. """ - def __init__(self, instruction: ValueInstruction, qubits: list[Qubit], params: list[float | Parameter] | None = ..., label: str | None = ...) -> None: ... + def __init__( + self, + instruction: ValueInstruction, + qubits: list[Qubit], + params: list[float | Parameter] | None = ..., + label: str | None = ..., + ) -> None: ... @staticmethod - def from_instruction(instruction: Instruction, qubits: list[Qubit], params: list[float | Parameter] | None = ..., label: str | None = ...) -> ValueOperation: + def from_instruction( + instruction: Instruction, + qubits: list[Qubit], + params: list[float | Parameter] | None = ..., + label: str | None = ..., + ) -> ValueOperation: """Create from a storage :class:`Instruction` with explicit parameters.""" ... @staticmethod - def from_standard_gate(gate: StandardGate, qubits: list[Qubit], label: str | None = ...) -> ValueOperation: + def from_standard_gate( + gate: StandardGate, qubits: list[Qubit], label: str | None = ... + ) -> ValueOperation: """Create while preserving parameters bound to a :class:`StandardGate`.""" ... @staticmethod - def from_mc_gate(gate: MCGate, qubits: list[Qubit], label: str | None = ...) -> ValueOperation: + def from_mc_gate( + gate: MCGate, qubits: list[Qubit], label: str | None = ... + ) -> ValueOperation: """Create while preserving parameters bound to an :class:`MCGate`.""" ... @staticmethod @@ -154,6 +199,39 @@ class ValueOperation: @property def params(self) -> list[float | Parameter]: ... @property + def name(self) -> str: + """Human-readable instruction name for this operation.""" + ... + @property + def num_qubits(self) -> int: + """Number of qubits used by this operation instance.""" + ... + @property + def num_params(self) -> int: + """Number of parameters carried by this operation instance.""" + ... + @property + def instruction_type(self) -> str: + """One of ``"standard"``, ``"mcgate"``, ``"unitary"``, ``"circuit"``, + ``"directive"``, ``"classical_data"``, ``"classical_control"``, ``"delay"``.""" + ... + @property + def is_standard(self) -> bool: ... + @property + def is_mcgate(self) -> bool: ... + @property + def is_unitary(self) -> bool: ... + @property + def is_circuit_gate(self) -> bool: ... + @property + def is_directive(self) -> bool: ... + @property + def is_classical_data(self) -> bool: ... + @property + def is_classical_control(self) -> bool: ... + @property + def is_delay(self) -> bool: ... + @property def label(self) -> str | None: ... def matrix(self) -> NDArray[np.complex128]: """Compute the unitary matrix (fixed-parameter operations only). diff --git a/crates/binding-python/src/circuit/gate/mc_gate.rs b/crates/binding-python/src/circuit/gate/mc_gate.rs index 4b0b838..aaf172d 100644 --- a/crates/binding-python/src/circuit/gate/mc_gate.rs +++ b/crates/binding-python/src/circuit/gate/mc_gate.rs @@ -121,13 +121,9 @@ impl PyMcGate { /// /// The inverse of a controlled gate C(U) is C(U†). /// - /// # Arguments - /// - /// * `params` - Optional parameters for the base gate. - /// /// # Returns /// - /// A tuple of (inverse gate, inverse parameters), or None if not invertible. + /// A new multi-controlled gate with inverse parameters already bound. pub fn inverse(&self) -> PyResult { if self.params.len() != self.inner.num_params() { return Err(PyCircuitError::new_err(format!( diff --git a/crates/binding-python/src/circuit/instruction.rs b/crates/binding-python/src/circuit/instruction.rs index 868347b..230f198 100644 --- a/crates/binding-python/src/circuit/instruction.rs +++ b/crates/binding-python/src/circuit/instruction.rs @@ -228,6 +228,63 @@ impl PyValueInstruction { self.inner.is_instruction() } + #[getter] + fn name(&self) -> String { + self.inner.name() + } + + #[getter] + fn instruction_type(&self) -> &'static str { + self.inner.instruction_type() + } + + #[getter] + fn is_standard(&self) -> bool { + self.inner.is_standard() + } + + #[getter] + fn is_mcgate(&self) -> bool { + self.inner.is_mcgate() + } + + #[getter] + fn is_unitary(&self) -> bool { + self.inner.is_unitary() + } + + #[getter] + fn is_circuit_gate(&self) -> bool { + self.inner.is_circuit_gate() + } + + #[getter] + fn is_directive(&self) -> bool { + self.inner.is_directive() + } + + #[getter] + fn is_classical_data(&self) -> bool { + self.inner.is_classical_data() + } + + #[getter] + fn is_delay(&self) -> bool { + self.inner.is_delay() + } + + #[getter] + fn standard_gate(&self) -> Option { + self.inner + .standard_gate() + .map(|gate| PyStandardGate::from(gate, vec![])) + } + + #[getter] + fn directive(&self) -> Option { + self.inner.directive().map(PyDirective::from) + } + #[getter] fn instruction(&self) -> Option { self.inner diff --git a/crates/binding-python/src/circuit/operation.rs b/crates/binding-python/src/circuit/operation.rs index 50eb2c0..0073316 100644 --- a/crates/binding-python/src/circuit/operation.rs +++ b/crates/binding-python/src/circuit/operation.rs @@ -341,6 +341,66 @@ impl PyValueOperation { Ok(result) } + #[getter] + fn name(&self) -> String { + self.inner.name() + } + + #[getter] + fn num_qubits(&self) -> usize { + self.inner.num_qubits() + } + + #[getter] + fn num_params(&self) -> usize { + self.inner.num_params() + } + + #[getter] + fn instruction_type(&self) -> &'static str { + self.inner.instruction_type() + } + + #[getter] + fn is_standard(&self) -> bool { + self.inner.is_standard() + } + + #[getter] + fn is_mcgate(&self) -> bool { + self.inner.is_mcgate() + } + + #[getter] + fn is_unitary(&self) -> bool { + self.inner.is_unitary() + } + + #[getter] + fn is_circuit_gate(&self) -> bool { + self.inner.is_circuit_gate() + } + + #[getter] + fn is_directive(&self) -> bool { + self.inner.is_directive() + } + + #[getter] + fn is_classical_data(&self) -> bool { + self.inner.is_classical_data() + } + + #[getter] + fn is_classical_control(&self) -> bool { + self.inner.is_classical_control() + } + + #[getter] + fn is_delay(&self) -> bool { + self.inner.is_delay() + } + #[getter] fn label(&self) -> Option { self.inner.label.as_ref().map(|s| s.to_string()) diff --git a/crates/binding-python/src/compile/compiler.rs b/crates/binding-python/src/compile/compiler.rs index d17d773..3d0ac0d 100644 --- a/crates/binding-python/src/compile/compiler.rs +++ b/crates/binding-python/src/compile/compiler.rs @@ -145,8 +145,8 @@ impl From for CompileMode { impl PyCompileMode { pub(crate) fn repr_label(&self) -> &'static str { match self.inner { - CompileMode::Normal => "CompileMode.normal()", - CompileMode::Enhanced => "CompileMode.enhanced()", + CompileMode::Normal => "CompileMode.Normal", + CompileMode::Enhanced => "CompileMode.Enhanced", } } } diff --git a/crates/binding-python/src/device/mod.rs b/crates/binding-python/src/device/mod.rs index 2ce06be..f921e56 100644 --- a/crates/binding-python/src/device/mod.rs +++ b/crates/binding-python/src/device/mod.rs @@ -83,7 +83,7 @@ pub mod topology; /// use pyo3::prelude::*; /// use _native::device::register_device_module; /// -/// Python::with_gil(|py| { +/// Python::try_attach(|py| { /// let module = PyModule::new(py, "cqlib").unwrap(); /// register_device_module(&module).unwrap(); /// }); diff --git a/crates/binding-python/tests/test_compile_workflow.py b/crates/binding-python/tests/test_compile_workflow.py index 169f469..811409b 100644 --- a/crates/binding-python/tests/test_compile_workflow.py +++ b/crates/binding-python/tests/test_compile_workflow.py @@ -45,8 +45,8 @@ def test_workflow_types_are_public_compile_types() -> None: assert CompilerWorkflow.__module__ == "cqlib.compile" assert "CompileConfig" in compile_module.__all__ assert "CompilerWorkflow" in compile_module.__all__ - assert repr(CompileMode.normal()) == "CompileMode.normal()" - assert repr(CompileMode.enhanced()) == "CompileMode.enhanced()" + assert repr(CompileMode.normal()) == "CompileMode.Normal" + assert repr(CompileMode.enhanced()) == "CompileMode.Enhanced" def test_compiler_errors_are_public_compile_exceptions() -> None: @@ -77,7 +77,7 @@ def test_compile_config_exposes_immutable_defaults_and_copy_protocol() -> None: assert config.seed is None assert copy.copy(config) is not config assert copy.deepcopy(config) is not config - assert repr(config).startswith("CompileConfig(mode=CompileMode.normal(),") + assert repr(config).startswith("CompileConfig(mode=CompileMode.Normal,") with pytest.raises(AttributeError): config.seed = 3 @@ -143,10 +143,16 @@ def test_compile_and_explicit_workflow_have_equivalent_results() -> None: direct = compile(circuit, target_basis=basis) explicit = CompilerWorkflow(CompileConfig(target_basis=basis)).run(circuit) - direct_names = [str(operation.instruction) for operation in direct.circuit.operations] - explicit_names = [str(operation.instruction) for operation in explicit.circuit.operations] + direct_names = [ + str(operation.instruction) for operation in direct.circuit.operations + ] + explicit_names = [ + str(operation.instruction) for operation in explicit.circuit.operations + ] assert direct_names == explicit_names == ["H", "CZ", "H"] - assert [step.name for step in direct.steps] == [step.name for step in explicit.steps] + assert [step.name for step in direct.steps] == [ + step.name for step in explicit.steps + ] assert direct == explicit assert direct.__eq__(object()) is NotImplemented @@ -185,7 +191,9 @@ def test_workflow_validates_cross_field_configuration_when_run() -> None: initial_layout=Layout.from_pairs([(0, 0)], physical_count=1), ) - with pytest.raises(CompilerConfigError, match="initial layout requires a target device"): + with pytest.raises( + CompilerConfigError, match="initial layout requires a target device" + ): CompilerWorkflow(config).run(Circuit(1)) @@ -197,5 +205,7 @@ def test_compile_config_rejects_unknown_target_gate_name() -> None: def test_workflow_rejects_non_standard_target_instruction_when_run() -> None: config = CompileConfig(target_basis=(Instruction.delay(),)) - with pytest.raises(CompilerConfigError, match="unsupported workflow target instruction"): + with pytest.raises( + CompilerConfigError, match="unsupported workflow target instruction" + ): CompilerWorkflow(config).run(Circuit(1)) diff --git a/crates/cqlib-core/src/circuit/operation.rs b/crates/cqlib-core/src/circuit/operation.rs index 8c1c68d..99dec1a 100644 --- a/crates/cqlib-core/src/circuit/operation.rs +++ b/crates/cqlib-core/src/circuit/operation.rs @@ -139,6 +139,66 @@ impl ValueOperation { label: None, } } + + /// Returns the human-readable instruction name. + pub fn name(&self) -> String { + self.instruction.name() + } + + /// Returns the number of qubits used by this operation instance. + pub fn num_qubits(&self) -> usize { + self.qubits.len() + } + + /// Returns the number of parameters carried by this operation instance. + pub fn num_params(&self) -> usize { + self.params.len() + } + + /// Returns a stable category name for this operation's instruction. + pub fn instruction_type(&self) -> &'static str { + self.instruction.instruction_type() + } + + /// Returns `true` if this operation uses a standard-gate instruction. + pub fn is_standard(&self) -> bool { + self.instruction.is_standard() + } + + /// Returns `true` if this operation uses a multi-controlled-gate instruction. + pub fn is_mcgate(&self) -> bool { + self.instruction.is_mcgate() + } + + /// Returns `true` if this operation uses a user-defined unitary instruction. + pub fn is_unitary(&self) -> bool { + self.instruction.is_unitary() + } + + /// Returns `true` if this operation uses a circuit-backed gate instruction. + pub fn is_circuit_gate(&self) -> bool { + self.instruction.is_circuit_gate() + } + + /// Returns `true` if this operation uses a directive instruction. + pub fn is_directive(&self) -> bool { + self.instruction.is_directive() + } + + /// Returns `true` if this operation uses a classical-data instruction. + pub fn is_classical_data(&self) -> bool { + self.instruction.is_classical_data() + } + + /// Returns `true` if this operation uses a classical-control instruction. + pub fn is_classical_control(&self) -> bool { + self.instruction.is_classical_control() + } + + /// Returns `true` if this operation uses a delay instruction. + pub fn is_delay(&self) -> bool { + self.instruction.is_delay() + } } impl fmt::Display for ValueOperation { @@ -163,3 +223,25 @@ impl fmt::Display for ValueOperation { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn value_operation_exposes_instance_queries() { + let operation = ValueOperation::from_standard( + StandardGate::RX, + [Qubit::new(0)], + [ParameterValue::Fixed(0.5)], + ); + + assert_eq!(operation.name(), "RX"); + assert_eq!(operation.num_qubits(), 1); + assert_eq!(operation.num_params(), 1); + assert_eq!(operation.instruction_type(), "standard"); + assert!(operation.is_standard()); + assert!(!operation.is_directive()); + assert!(!operation.is_classical_control()); + } +} diff --git a/crates/cqlib-core/src/circuit/value_instruction.rs b/crates/cqlib-core/src/circuit/value_instruction.rs index 4fab726..f7131e2 100644 --- a/crates/cqlib-core/src/circuit/value_instruction.rs +++ b/crates/cqlib-core/src/circuit/value_instruction.rs @@ -46,7 +46,9 @@ use crate::circuit::classical::{ClassicalValue, ClassicalVar}; use crate::circuit::classical_expr::ClassicalExpr; use crate::circuit::control_flow::ControlBody; use crate::circuit::error::CircuitError; +use crate::circuit::gate::directive::Directive; use crate::circuit::gate::instruction::Instruction; +use crate::circuit::gate::standard_gate::StandardGate; use crate::circuit::operation::{Operation, ValueOperation}; use alloc::collections::BTreeSet; use std::fmt; @@ -422,6 +424,78 @@ impl ValueInstruction { matches!(self, Self::Instruction(_)) } + /// Returns the human-readable instruction name. + pub fn name(&self) -> String { + self.to_string() + } + + /// Returns a stable category name for this instruction. + pub fn instruction_type(&self) -> &'static str { + match self { + Self::Instruction(Instruction::Standard(_)) => "standard", + Self::Instruction(Instruction::McGate(_)) => "mcgate", + Self::Instruction(Instruction::UnitaryGate(_)) => "unitary", + Self::Instruction(Instruction::CircuitGate(_)) => "circuit", + Self::Instruction(Instruction::Directive(_)) => "directive", + Self::Instruction(Instruction::ClassicalData(_)) => "classical_data", + Self::Instruction(Instruction::ClassicalControl(_)) | Self::ClassicalControl(_) => { + "classical_control" + } + Self::Instruction(Instruction::Delay) => "delay", + } + } + + /// Returns `true` if this is a standard-gate instruction. + pub fn is_standard(&self) -> bool { + matches!(self, Self::Instruction(Instruction::Standard(_))) + } + + /// Returns `true` if this is a multi-controlled-gate instruction. + pub fn is_mcgate(&self) -> bool { + matches!(self, Self::Instruction(Instruction::McGate(_))) + } + + /// Returns `true` if this is a user-defined unitary instruction. + pub fn is_unitary(&self) -> bool { + matches!(self, Self::Instruction(Instruction::UnitaryGate(_))) + } + + /// Returns `true` if this is a circuit-backed gate instruction. + pub fn is_circuit_gate(&self) -> bool { + matches!(self, Self::Instruction(Instruction::CircuitGate(_))) + } + + /// Returns `true` if this is a non-unitary directive. + pub fn is_directive(&self) -> bool { + matches!(self, Self::Instruction(Instruction::Directive(_))) + } + + /// Returns `true` if this is a classical-data instruction. + pub fn is_classical_data(&self) -> bool { + matches!(self, Self::Instruction(Instruction::ClassicalData(_))) + } + + /// Returns `true` if this is a delay instruction. + pub fn is_delay(&self) -> bool { + matches!(self, Self::Instruction(Instruction::Delay)) + } + + /// Returns the standard gate when this is a standard-gate instruction. + pub fn standard_gate(&self) -> Option { + match self { + Self::Instruction(Instruction::Standard(gate)) => Some(*gate), + _ => None, + } + } + + /// Returns the directive when this is a directive instruction. + pub fn directive(&self) -> Option { + match self { + Self::Instruction(Instruction::Directive(directive)) => Some(*directive), + _ => None, + } + } + /// Returns the wrapped storage instruction when this is a non-control-flow value instruction. pub fn as_instruction(&self) -> Option<&Instruction> { match self { @@ -570,7 +644,7 @@ mod tests { use super::*; use crate::circuit::ClassicalType; use crate::circuit::bit::Qubit; - use crate::circuit::gate::{ClassicalDataOp, StandardGate}; + use crate::circuit::gate::{ClassicalDataOp, Directive, StandardGate}; use std::num::NonZeroU32; #[test] @@ -609,6 +683,42 @@ mod tests { assert!(!vi.is_instruction()); } + #[test] + fn value_instruction_exposes_instruction_queries() { + let standard = ValueInstruction::from_instruction(Instruction::Standard(StandardGate::H)); + assert_eq!(standard.name(), "H"); + assert_eq!(standard.instruction_type(), "standard"); + assert!(standard.is_standard()); + assert_eq!(standard.standard_gate(), Some(StandardGate::H)); + assert_eq!(standard.directive(), None); + + let directive = + ValueInstruction::from_instruction(Instruction::Directive(Directive::Barrier)); + assert_eq!(directive.name(), "Barrier"); + assert_eq!(directive.instruction_type(), "directive"); + assert!(directive.is_directive()); + assert_eq!(directive.directive(), Some(Directive::Barrier)); + + let value = + ClassicalValue::new(crate::circuit::CircuitId::default(), 0, ClassicalType::Bit); + let classical_data = ValueInstruction::from_instruction(Instruction::ClassicalData( + ClassicalDataOp::MeasureBit { result: value }, + )); + assert_eq!(classical_data.name(), "measure_bit"); + assert_eq!(classical_data.instruction_type(), "classical_data"); + assert!(classical_data.is_classical_data()); + + let control = ValueInstruction::from(ValueClassicalControlOp::Break); + assert_eq!(control.name(), "break"); + assert_eq!(control.instruction_type(), "classical_control"); + assert!(control.is_classical_control()); + + let delay = ValueInstruction::from_instruction(Instruction::Delay); + assert_eq!(delay.name(), "delay"); + assert_eq!(delay.instruction_type(), "delay"); + assert!(delay.is_delay()); + } + #[test] fn value_classical_control_reads_and_writes() { let var = ClassicalVar::new( diff --git a/tests/python/circuit/test_indexing.py b/tests/python/circuit/test_indexing.py index 9b96a45..969bc0a 100644 --- a/tests/python/circuit/test_indexing.py +++ b/tests/python/circuit/test_indexing.py @@ -46,9 +46,9 @@ def test_operation_method_and_getitem_report_out_of_range(): with pytest.raises(CircuitError): circuit.operation(1) - with pytest.raises(CircuitError): + with pytest.raises(IndexError): circuit[1] empty = Circuit(1) - with pytest.raises(CircuitError): + with pytest.raises(IndexError): empty[0] diff --git a/tests/python/circuit/test_value_ir.py b/tests/python/circuit/test_value_ir.py index 17d9245..b4051ee 100644 --- a/tests/python/circuit/test_value_ir.py +++ b/tests/python/circuit/test_value_ir.py @@ -34,7 +34,12 @@ def test_operation_exposes_value_ir_fields(): operation = circuit[0] assert isinstance(operation, ValueOperation) - assert operation.instruction.instruction.name == "RX" + assert operation.name == "RX" + assert operation.num_qubits == 1 + assert operation.num_params == 1 + assert operation.instruction_type == "standard" + assert operation.is_standard + assert operation.instruction.name == "RX" assert list(operation.params) == [0.5] assert [qubit.index for qubit in operation.qubits] == [0] @@ -50,9 +55,15 @@ def test_instruction_factory_methods_expose_names_and_kinds(): def test_value_instruction_wraps_instruction_and_control_variants(): - standard = ValueInstruction.from_instruction(Instruction.from_standard_gate(StandardGate.X())) + standard = ValueInstruction.from_instruction( + Instruction.from_standard_gate(StandardGate.X()) + ) assert standard.is_instruction + assert standard.name == "X" + assert standard.instruction_type == "standard" + assert standard.is_standard assert standard.instruction.name == "X" + assert repr(standard.standard_gate) == "X" body_circuit = Circuit(1) body_circuit.h(0) @@ -63,6 +74,8 @@ def test_value_instruction_wraps_instruction_and_control_variants(): controlled = ValueInstruction.from_classical_control(control) assert controlled.is_classical_control + assert controlled.name == "if" + assert controlled.instruction_type == "classical_control" assert controlled.classical_control.kind == "if" @@ -77,8 +90,11 @@ def test_value_operation_factories_can_build_and_append_operations(): circuit.append(operation) assert operation.label == "rx-label" + assert operation.name == "RX" + assert operation.num_qubits == 1 + assert operation.num_params == 1 assert list(operation.params) == [0.25] - assert circuit[0].instruction.instruction.name == "RX" + assert circuit[0].name == "RX" def test_value_operation_matrix_for_unitary_gate(): @@ -91,6 +107,11 @@ def test_value_operation_matrix_rejects_directive_without_matrix(): instruction = Instruction.from_directive(Directive.barrier()) operation = ValueOperation.from_instruction(instruction, [Qubit(0)]) + assert operation.name == "Barrier" + assert operation.is_directive + assert operation.instruction.is_directive + assert operation.instruction.directive.name() == "Barrier" + with pytest.raises(ValueError): operation.matrix() @@ -104,9 +125,10 @@ def test_value_operation_from_classical_control(): ) operation = ValueOperation.from_classical_control(control) + assert operation.name == "if" + assert operation.is_classical_control assert operation.instruction.is_classical_control assert operation.instruction.classical_control.kind == "if" assert [ - op.instruction.instruction.name - for op in operation.instruction.classical_control.then_body.operations + op.name for op in operation.instruction.classical_control.then_body.operations ] == ["Z"] diff --git a/tests/python/compile/test_compile.py b/tests/python/compile/test_compile.py index 93f835a..61b8bb1 100644 --- a/tests/python/compile/test_compile.py +++ b/tests/python/compile/test_compile.py @@ -17,7 +17,13 @@ import pytest from cqlib.circuit import Circuit, Instruction, MCGate, Parameter, StandardGate -from cqlib.compile import CompileMode, CompileResult, WorkflowStepReport, compile +from cqlib.compile import ( + CompileMode, + CompileResult, + CompilerConfigError, + WorkflowStepReport, + compile, +) from cqlib.compile.compiler import compile as compile_from_submodule from cqlib.device import Device, Layout @@ -363,7 +369,7 @@ def test_compile_target_basis_accepts_mixed_names_and_instructions(): def test_compile_target_basis_rejects_unknown_names_and_invalid_types(): - with pytest.raises(ValueError, match="NOT_A_GATE"): + with pytest.raises(CompilerConfigError, match="NOT_A_GATE"): compile(bell_circuit(), target_basis=["NOT_A_GATE"]) with pytest.raises(TypeError): @@ -450,27 +456,30 @@ def test_compile_rejects_unsupported_and_non_standard_target_basis(): circuit = Circuit(1) circuit.h(0) - with pytest.raises(ValueError, match="H"): + with pytest.raises(CompilerConfigError, match="H"): compile(circuit, target_basis=[instruction(StandardGate.CZ)]) - with pytest.raises(ValueError, match="unsupported workflow target instruction"): + with pytest.raises( + CompilerConfigError, match="unsupported workflow target instruction" + ): compile( bell_circuit(), target_basis=[Instruction.from_mc_gate(MCGate(2, StandardGate.X))], ) - with pytest.raises(ValueError, match="FSIM"): - compile( - ising_exchange_circuit(), - target_basis=[ - instruction(StandardGate.H), - instruction(StandardGate.RX), - instruction(StandardGate.RY), - instruction(StandardGate.RZ), - instruction(StandardGate.RZZ), - instruction(StandardGate.GPhase), - ], - ) + reduced_ising_basis = [ + instruction(StandardGate.H), + instruction(StandardGate.RX), + instruction(StandardGate.RY), + instruction(StandardGate.RZ), + instruction(StandardGate.RZZ), + instruction(StandardGate.GPhase), + ] + result = compile(ising_exchange_circuit(), target_basis=reduced_ising_basis) + names = [operation_name(operation) for operation in result.circuit.operations] + assert "FSIM" not in names + assert_only_standard_basis(result.circuit, standard_names(reduced_ising_basis)) + assert_unitary_equivalent(ising_exchange_circuit(), result.circuit) def test_compile_routes_long_range_circuit_on_line_device(): @@ -570,11 +579,13 @@ def test_compile_rejects_invalid_device_and_layout_configurations(): too_wide = Circuit(4) too_wide.h(0) - with pytest.raises(ValueError, match="4 logical qubits"): + with pytest.raises(CompilerConfigError, match="4 logical qubits"): compile(too_wide, device=Device.line("line-2", 2)) layout = Layout.from_pairs([(0, 0)], 1) - with pytest.raises(ValueError, match="initial layout requires a target device"): + with pytest.raises( + CompilerConfigError, match="initial layout requires a target device" + ): compile(Circuit(1), initial_layout=layout) diff --git a/tests/python/compile/test_sabre.py b/tests/python/compile/test_sabre.py index 406c068..44ce33d 100644 --- a/tests/python/compile/test_sabre.py +++ b/tests/python/compile/test_sabre.py @@ -16,6 +16,7 @@ import pytest from cqlib.circuit import Circuit +from cqlib.compile import CompilerConfigError from cqlib.compile import sabre from cqlib.compile.sabre import ( SabreConfig, @@ -157,10 +158,10 @@ def test_route_rejects_invalid_configuration_and_disconnected_interaction(): line = Device.line("line2", 2) layout = Layout(logical=[0, 1], physical=[0, 1]) - with pytest.raises(ValueError, match="routing_trials"): + with pytest.raises(CompilerConfigError, match="routing_trials"): sabre_route(circuit, line, layout, SabreConfig(routing_trials=0)) - with pytest.raises(ValueError, match="basic_weight"): + with pytest.raises(CompilerConfigError, match="basic_weight"): sabre_route( circuit, line, @@ -173,5 +174,5 @@ def test_route_rejects_invalid_configuration_and_disconnected_interaction(): [0, 1], Topology([0, 1], []), ) - with pytest.raises(ValueError, match="disconnected"): + with pytest.raises(CompilerConfigError, match="disconnected"): sabre_route(circuit, disconnected, layout) diff --git a/tests/python/compile/test_transform_decompose.py b/tests/python/compile/test_transform_decompose.py index 5cf6fcf..8932218 100644 --- a/tests/python/compile/test_transform_decompose.py +++ b/tests/python/compile/test_transform_decompose.py @@ -20,6 +20,7 @@ StandardGate, UnitaryGate, ) +from cqlib.compile import CompilerConfigError, CompilerTransformError from cqlib.compile.resource import ResourceLimits, ResourcePolicy from cqlib.compile.transform import TransformResult, decompose from cqlib.compile.transform.decompose import ( @@ -122,7 +123,9 @@ def test_decompose_unitaries_preserves_matrix_and_reports_cache_stats() -> None: assert result.changed is True assert operation_names(circuit) == ["x_matrix", "x_matrix"] assert all(name == "U" for name in operation_names(result.circuit)) - np.testing.assert_allclose(result.circuit.to_matrix(), circuit.to_matrix(), atol=1e-10) + np.testing.assert_allclose( + result.circuit.to_matrix(), circuit.to_matrix(), atol=1e-10 + ) assert (stats.hits, stats.misses, stats.inserts) == (1, 1, 1) assert copy.copy(stats) == stats @@ -131,7 +134,7 @@ def test_decompose_unitaries_rejects_missing_matrix() -> None: circuit = Circuit(1) circuit.append_unitary_gate(UnitaryGate("undefined", 1), [0]) - with pytest.raises(ValueError, match="no matrix representation"): + with pytest.raises(CompilerTransformError, match="no matrix representation"): decompose_unitaries(circuit) @@ -145,8 +148,13 @@ def test_decompose_mc_gates_returns_standard_operations_and_stats() -> None: assert result.changed is True assert len(circuit.operations) == 2 - assert all(not operation.instruction.instruction.is_mcgate for operation in result.circuit.operations) - np.testing.assert_allclose(result.circuit.to_matrix(), circuit.to_matrix(), atol=1e-10) + assert all( + not operation.instruction.instruction.is_mcgate + for operation in result.circuit.operations + ) + np.testing.assert_allclose( + result.circuit.to_matrix(), circuit.to_matrix(), atol=1e-10 + ) assert stats.hits >= 1 assert stats.misses >= 1 assert stats.inserts >= 1 @@ -170,7 +178,7 @@ def test_decompose_mc_gates_for_device_enforces_capacity() -> None: assert result.changed is True assert result.circuit.num_qubits == 4 - with pytest.raises(ValueError, match="capacity exceeded"): + with pytest.raises(CompilerConfigError, match="capacity exceeded"): decompose_mc_gates_for_device(circuit, Device.line("line3", 3)) @@ -182,10 +190,14 @@ def test_numeric_unitary_module_is_registered() -> None: def test_numeric_1q_synthesis_reconstructs_matrix_and_phase() -> None: - source = np.exp(0.37j) * np.array( - [[1, 1], [1, -1]], - dtype=np.complex128, - ) / np.sqrt(2) + source = ( + np.exp(0.37j) + * np.array( + [[1, 1], [1, -1]], + dtype=np.complex128, + ) + / np.sqrt(2) + ) decomposition = synthesize_numeric_1q_unitary(source) circuit = Circuit(1) @@ -263,9 +275,9 @@ def test_kak_decomposition_reconstructs_matrix_and_returns_owned_arrays() -> Non def test_numeric_synthesis_rejects_invalid_inputs() -> None: - with pytest.raises(ValueError, match="2x2"): + with pytest.raises(CompilerConfigError, match="2x2"): synthesize_numeric_1q_unitary(np.eye(3)) - with pytest.raises(ValueError, match="distinct qubits"): + with pytest.raises(CompilerConfigError, match="distinct qubits"): synthesize_numeric_2q_unitary(np.eye(4), 0, 0) with pytest.raises(TypeError, match="two-dimensional"): kak_decompose([1, 0, 0, 1]) @@ -330,9 +342,11 @@ def test_exact_mcx_primitives_match_mcgate_matrix( def test_mcx_small_emits_standard_gate_and_validates_inputs() -> None: operations = mc.decompose_mcx_small([0, 1], 2) - assert [operation.instruction.instruction.name for operation in operations] == ["CCX"] + assert [operation.instruction.instruction.name for operation in operations] == [ + "CCX" + ] - with pytest.raises(ValueError, match="duplicate"): + with pytest.raises(CompilerTransformError, match="duplicate"): mc.decompose_mcx_no_aux([0, 0, 1], 2) with pytest.raises(ValueError, match="exactly 2"): mc.decompose_mcx_2_clean([0, 1, 2], 3, [4]) @@ -348,7 +362,9 @@ def test_mc_su2_and_rotation_primitives_preserve_semantics() -> None: rotation = mc.decompose_rotation_no_aux(StandardGate.RY, theta, [0, 1], 2) rotation_result = circuit_from_value_operations(3, rotation) - np.testing.assert_allclose(result.to_matrix(), rotation_result.to_matrix(), atol=1e-10) + np.testing.assert_allclose( + result.to_matrix(), rotation_result.to_matrix(), atol=1e-10 + ) assert mc.Su2RotationAxis.x() != mc.Su2RotationAxis.z() assert copy.copy(mc.Su2RotationAxis.y()) == mc.Su2RotationAxis.y() @@ -377,9 +393,9 @@ def test_representative_composite_primitives_return_equivalent_circuits() -> Non def test_clean_composite_primitives_validate_ancilla_contracts() -> None: - with pytest.raises(ValueError, match="requires"): + with pytest.raises(CompilerTransformError, match="requires"): mc.decompose_swap_n_clean([0, 1, 2], 3, 4, []) - with pytest.raises(ValueError, match="distinct|duplicate"): + with pytest.raises(CompilerTransformError, match="distinct|duplicate"): mc.decompose_mc_rzz_n_clean(0.2, [0, 1], 2, 3, [0]) with pytest.raises(ParameterError, match="finite"): mc.decompose_unitary_no_aux(float("inf"), 0.0, 0.0, [0], 1) diff --git a/tests/python/device/test_layout.py b/tests/python/device/test_layout.py index 16427ad..4a15e92 100644 --- a/tests/python/device/test_layout.py +++ b/tests/python/device/test_layout.py @@ -28,20 +28,20 @@ def test_layout_mapping_and_swap(self): ) assert layout.num_logical == 2 assert layout.num_physical == 3 - assert layout.num_ancilla == 1 + assert layout.num_vacant_physical == 1 assert set(layout.logical_qubits) == {Qubit(0), Qubit(1)} assert set(layout.physical_qubits) == {Qubit(10), Qubit(11), Qubit(12)} - assert set(layout.v2p_map.keys()).issuperset({Qubit(0), Qubit(1)}) - assert set(layout.p2v_map.keys()).issubset({Qubit(10), Qubit(11), Qubit(12)}) + assert set(layout.l2p_map.keys()).issuperset({Qubit(0), Qubit(1)}) + assert set(layout.p2l_map.keys()).issubset({Qubit(10), Qubit(11), Qubit(12)}) assert layout.get_physical(0) == Qubit(11) - v_on_11 = layout.get_virtual(11) - v_on_12 = layout.get_virtual(12) + v_on_11 = layout.get_logical(11) + v_on_12 = layout.get_logical(12) layout.swap_physical(11, 12) - assert layout.get_virtual(11) == v_on_12 - assert layout.get_virtual(12) == v_on_11 + assert layout.get_logical(11) == v_on_12 + assert layout.get_logical(12) == v_on_11 def test_layout_swap_rejects_unknown_physical(self): """swap_physical should reject physical qubits outside layout.""" diff --git a/tests/python/device/test_properties.py b/tests/python/device/test_properties.py index f727e5d..cf64188 100644 --- a/tests/python/device/test_properties.py +++ b/tests/python/device/test_properties.py @@ -37,7 +37,7 @@ def test_instruction_qubit_edge_prop_builders(self): qp.t1 = 120.0 qp.t2 = 95.0 qp.frequency = 5.1 - qp.native_instruction = ip + qp.add_native_instruction(ip) assert qp.readout_error == pytest.approx(0.02) assert qp.t1 == pytest.approx(120.0) assert qp.t2 == pytest.approx(95.0) @@ -49,7 +49,7 @@ def test_instruction_qubit_edge_prop_builders(self): eip = InstructionProp(cx_inst, 0.08) eip.length = 300.0 ep = EdgeProp() - ep.native_instruction = eip + ep.add_native_instruction(eip) assert len(ep.native_instructions) == 1 assert ep.native_instructions[0].instruction.name == "CX" @@ -78,7 +78,7 @@ def test_device_add_and_query(self): ep01 = EdgeProp() cx_inst = Instruction.from_standard_gate(StandardGate.CX) - ep01.native_instruction = InstructionProp(cx_inst, 0.06) + ep01.add_native_instruction(InstructionProp(cx_inst, 0.06)) device.add_edge_properties(0, 1, ep01) assert device.name == "mock_backend" diff --git a/tests/python/device/test_topology.py b/tests/python/device/test_topology.py index 8f2595a..72f3431 100644 --- a/tests/python/device/test_topology.py +++ b/tests/python/device/test_topology.py @@ -29,8 +29,8 @@ def test_topology_basic_queries(self): assert topo.contains_qubit(1) is True assert topo.contains_qubit(99) is False # Topology uses directed couplings; neighbors/degree are based on outgoing edges. - assert set(topo.neighbors(1)) == {Qubit(2)} - assert topo.degree(1) == 1 + assert set(topo.successors(1)) == {Qubit(2)} + assert topo.out_degree(1) == 1 assert topo.get_coupling_name(1, 2) == "G2" def test_topology_add_remove_qubits_and_couplings(self): @@ -39,9 +39,9 @@ def test_topology_add_remove_qubits_and_couplings(self): topo.add_qubits([3]) topo.add_couplings([(2, 3, "G2")]) assert topo.contains_qubit(3) is True - assert topo.is_connected(2, 3) or topo.is_connected(3, 2) + assert topo.supports_coupling_either_direction(2, 3) topo.remove_couplings([(2, 3)]) - assert not (topo.is_connected(2, 3) or topo.is_connected(3, 2)) + assert not topo.supports_coupling_either_direction(2, 3) topo.remove_qubits([3]) assert topo.contains_qubit(3) is False diff --git a/tests/python/gates/test_circuit_gate.py b/tests/python/gates/test_circuit_gate.py index 253c36f..ac1ed40 100644 --- a/tests/python/gates/test_circuit_gate.py +++ b/tests/python/gates/test_circuit_gate.py @@ -69,7 +69,7 @@ def test_apply_circuit_gate(self): bell_gate = sub.to_gate("Bell") main = Circuit(4) - main.circuit_gate(bell_gate, [0, 1]) + main.append_circuit_gate(bell_gate, [0, 1]) assert len(main) == 1 assert main[0].name == "Bell" @@ -82,8 +82,8 @@ def test_apply_multiple_instances(self): bell_gate = sub.to_gate("Bell") main = Circuit(4) - main.circuit_gate(bell_gate, [0, 1]) - main.circuit_gate(bell_gate, [2, 3]) + main.append_circuit_gate(bell_gate, [0, 1]) + main.append_circuit_gate(bell_gate, [2, 3]) assert len(main) == 2 # Verify both operations use the same gate name @@ -98,7 +98,7 @@ def test_apply_to_different_qubits(self): bell_gate = sub.to_gate("Bell") main = Circuit(4) - main.circuit_gate(bell_gate, [1, 2]) + main.append_circuit_gate(bell_gate, [1, 2]) op = main[0] assert op.qubits[0].index == 1 @@ -116,7 +116,7 @@ def test_decompose_expands_gates(self): bell_gate = sub.to_gate("Bell") main = Circuit(2) - main.circuit_gate(bell_gate, [0, 1]) + main.append_circuit_gate(bell_gate, [0, 1]) decomposed = main.decompose() # Should expand to H and CX @@ -133,7 +133,7 @@ def test_decompose_preserves_operation_order(self): gate = sub.to_gate("Ordered") main = Circuit(2) - main.circuit_gate(gate, [0, 1]) + main.append_circuit_gate(gate, [0, 1]) decomposed = main.decompose() names = [op.name for op in decomposed] @@ -146,8 +146,8 @@ def test_decompose_multiple_circuit_gates(self): h_gate = sub.to_gate("H") main = Circuit(2) - main.circuit_gate(h_gate, [0]) - main.circuit_gate(h_gate, [1]) + main.append_circuit_gate(h_gate, [0]) + main.append_circuit_gate(h_gate, [1]) decomposed = main.decompose() assert len(decomposed) == 2 @@ -174,8 +174,8 @@ def test_apply_parametric_circuit_gate(self): rx_gate = c.to_gate("Rx") main = Circuit(2) - main.circuit_gate(rx_gate, [0]) - main.circuit_gate(rx_gate, [1]) + main.append_circuit_gate(rx_gate, [0], [theta]) + main.append_circuit_gate(rx_gate, [1], [theta]) assert len(main) == 2 @@ -200,11 +200,11 @@ def test_nested_circuit_gates(self): inner_gate = inner.to_gate("H") middle = Circuit(1) - middle.circuit_gate(inner_gate, [0]) + middle.append_circuit_gate(inner_gate, [0]) middle_gate = middle.to_gate("Middle") outer = Circuit(1) - outer.circuit_gate(middle_gate, [0]) + outer.append_circuit_gate(middle_gate, [0]) assert len(outer) == 1 @@ -215,11 +215,11 @@ def test_decompose_nested_gates(self): inner_gate = inner.to_gate("X") middle = Circuit(1) - middle.circuit_gate(inner_gate, [0]) + middle.append_circuit_gate(inner_gate, [0]) middle_gate = middle.to_gate("Middle") outer = Circuit(1) - outer.circuit_gate(middle_gate, [0]) + outer.append_circuit_gate(middle_gate, [0]) # Decompose once expands middle to inner decomposed_once = outer.decompose() @@ -247,7 +247,7 @@ def test_circuit_gate_operation_name(self): gate = sub.to_gate("CustomH") main = Circuit(1) - main.circuit_gate(gate, [0]) + main.append_circuit_gate(gate, [0]) op = main[0] assert op.name == "CustomH" diff --git a/tests/python/gates/test_mc_gate.py b/tests/python/gates/test_mc_gate.py index 464f18e..c95f23f 100644 --- a/tests/python/gates/test_mc_gate.py +++ b/tests/python/gates/test_mc_gate.py @@ -185,31 +185,28 @@ class TestMcGateInverse: def test_mc_x_inverse(self): """Multi-controlled X is self-inverse""" ccx = MCGate(2, X) - inv_result = ccx.inverse([]) - assert inv_result is not None - inv_gate, inv_params = inv_result + inv_gate = ccx.inverse() # Inverse should be another MCGate with same controls assert inv_gate.num_ctrl_qubits == 2 # X is self-inverse, so inverse is itself assert inv_gate.base_gate == X + assert inv_gate.params == [] def test_mc_rx_inverse_negates_angle(self): """Controlled-RX inverse negates the angle""" - crx = MCGate(1, RX) - inv_result = crx.inverse([Parameter("theta")]) - assert inv_result is not None - inv_gate, inv_params = inv_result + crx = MCGate(1, RX(Parameter("theta"))) + inv_gate = crx.inverse() assert inv_gate.num_ctrl_qubits == 1 # Verify parameter is negated - assert len(inv_params) == 1 + assert len(inv_gate.params) == 1 + assert str(inv_gate.params[0]) == "-theta" def test_mc_h_inverse(self): """Controlled-H inverse is itself (H is self-inverse)""" ch = MCGate(1, H) - inv_result = ch.inverse([]) - assert inv_result is not None - inv_gate, _ = inv_result + inv_gate = ch.inverse() assert inv_gate.base_gate == H + assert inv_gate.params == [] class TestMcGateMatrixShape: diff --git a/tests/python/gates/test_unitary.py b/tests/python/gates/test_unitary.py index 664d981..3faec99 100644 --- a/tests/python/gates/test_unitary.py +++ b/tests/python/gates/test_unitary.py @@ -180,7 +180,7 @@ def test_apply_single_qubit_unitary(self): c = Circuit(2) h_mat = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2) gate = UnitaryGate("CustomH", 1).with_matrix(h_mat) - c.unitary(gate, [0]) + c.append_unitary_gate(gate, [0]) assert len(c) == 1 # Verify operation name is the gate label op = c[0] @@ -193,7 +193,7 @@ def test_apply_two_qubit_unitary(self): [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dtype=complex ) gate = UnitaryGate("CustomCNOT", 2).with_matrix(cnot_mat) - c.unitary(gate, [0, 1]) + c.append_unitary_gate(gate, [0, 1]) assert len(c) == 1 def test_apply_multiple_unitaries(self): @@ -205,8 +205,8 @@ def test_apply_multiple_unitaries(self): x_gate = UnitaryGate("X", 1).with_matrix(x_mat) h_gate = UnitaryGate("H", 1).with_matrix(h_mat) - c.unitary(x_gate, [0]) - c.unitary(h_gate, [1]) + c.append_unitary_gate(x_gate, [0]) + c.append_unitary_gate(h_gate, [1]) assert len(c) == 2 @@ -217,7 +217,7 @@ def test_apply_unitary_to_different_qubits(self): gate = UnitaryGate("CustomH", 1).with_matrix(h_mat) # Apply to qubit 2 - c.unitary(gate, [2]) + c.append_unitary_gate(gate, [2]) assert len(c) == 1 assert c[0].qubits[0].index == 2 diff --git a/tests/python/ir/test_qasm2_dump.py b/tests/python/ir/test_qasm2_dump.py index da13a43..37a4955 100644 --- a/tests/python/ir/test_qasm2_dump.py +++ b/tests/python/ir/test_qasm2_dump.py @@ -39,7 +39,7 @@ def test_dumps_empty_circuit(self): # Verify header structure exactly lines = qasm.strip().split("\n") - assert lines[0] == "// This file is auto-generated by Cqlib." + assert lines[0].startswith("// This file is auto-generated by Cqlib v") assert lines[1] == "OPENQASM 2.0;" assert lines[2] == 'include "qelib1.inc";' assert "qreg q[1];" in qasm @@ -156,10 +156,10 @@ def test_dumps_measurement(self): c.measure(1) qasm = dumps(c) - # Note: measurement without conditional is commented out - # Only measurements for qubits used in conditional operations are output - assert "// measure q[0] -> c0[0];" in qasm or "measure q[0] -> c0[0];" in qasm - assert "// measure q[1] -> c1[0];" in qasm or "measure q[1] -> c1[0];" in qasm + assert "creg v0[1];" in qasm + assert "creg v1[1];" in qasm + assert "measure q[0] -> v0[0];" in qasm + assert "measure q[1] -> v1[0];" in qasm def test_dumps_reset(self): """Generate QASM with reset.""" @@ -275,7 +275,7 @@ def test_dumps_simple_custom_gate(self): # Use in main circuit c = Circuit(2) - c.circuit_gate(bell, [0, 1]) + c.append_circuit_gate(bell, [0, 1]) qasm = dumps(c) # Should include gate definition @@ -297,11 +297,12 @@ def test_dumps_parameterized_custom_gate(self): # Use with fixed value c = Circuit(1) - c.circuit_gate(my_rx, [0]) + c.append_circuit_gate(my_rx, [0], [0.5]) qasm = dumps(c) assert "gate my_rx(theta) q0 {" in qasm assert "rx(theta) q0;" in qasm + assert "my_rx(0.5) q[0];" in qasm class TestQasm2DumpFileIO: @@ -323,7 +324,7 @@ def test_dump_to_file(self): with open(temp_path, "r") as f: content = f.read() - assert content.startswith("// This file is auto-generated by Cqlib.") + assert content.startswith("// This file is auto-generated by Cqlib v") assert "OPENQASM 2.0;" in content assert "h q[0];" in content assert "cx q[0],q[1];" in content diff --git a/tests/python/ir/test_qasm2_load.py b/tests/python/ir/test_qasm2_load.py index 72abfe8..736b41c 100644 --- a/tests/python/ir/test_qasm2_load.py +++ b/tests/python/ir/test_qasm2_load.py @@ -17,7 +17,7 @@ - QASM2 string parsing (loads) - QASM2 file loading (load) - Standard gates (single-qubit, multi-qubit, parametric) -- Barrier, Reset, and Measurement directives +- Barrier and Reset directives; measurement classical-data operations - Parameter expressions (pi, arithmetic) - Custom gate definitions - Error handling @@ -26,7 +26,7 @@ import tempfile import os from cqlib.circuit import Circuit -from cqlib.ir.qasm2 import loads, load +from cqlib.ir.qasm2 import loads, load, dumps class TestQasm2Loads: @@ -59,10 +59,10 @@ def test_loads_single_qubit_gates(self): assert len(c) == 4 # Verify each gate type - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "X" - assert c[2].instruction.name == "Y" - assert c[3].instruction.name == "Z" + assert c[0].name == "H" + assert c[1].name == "X" + assert c[2].name == "Y" + assert c[3].name == "Z" # Verify qubit indices assert c[0].qubits[0].index == 0 @@ -82,10 +82,10 @@ def test_loads_phase_gates(self): c = loads(qasm) assert len(c) == 4 - assert c[0].instruction.name == "S" - assert c[1].instruction.name == "SDG" - assert c[2].instruction.name == "T" - assert c[3].instruction.name == "TDG" + assert c[0].name == "S" + assert c[1].name == "SDG" + assert c[2].name == "T" + assert c[3].name == "TDG" def test_loads_cnot(self): """Parse CNOT gate and verify control/target.""" @@ -99,7 +99,7 @@ def test_loads_cnot(self): assert len(c) == 1 op = c[0] - assert op.instruction.name == "CX" + assert op.name == "CX" assert op.num_qubits == 2 assert op.qubits[0].index == 0 # control assert op.qubits[1].index == 1 # target @@ -117,9 +117,9 @@ def test_loads_two_qubit_gates(self): c = loads(qasm) assert len(c) == 3 - assert c[0].instruction.name == "CY" - assert c[1].instruction.name == "CZ" - assert c[2].instruction.name == "SWAP" + assert c[0].name == "CY" + assert c[1].name == "CZ" + assert c[2].name == "SWAP" def test_loads_toffoli(self): """Parse CCX (Toffoli) gate and verify qubits.""" @@ -134,7 +134,7 @@ def test_loads_toffoli(self): assert len(c) == 1 op = c[0] - assert op.instruction.name == "CCX" + assert op.name == "CCX" assert op.num_qubits == 3 assert op.qubits[0].index == 0 assert op.qubits[1].index == 1 @@ -153,9 +153,9 @@ def test_loads_parametric_gates(self): c = loads(qasm) assert len(c) == 3 - assert c[0].instruction.name == "RX" - assert c[1].instruction.name == "RY" - assert c[2].instruction.name == "RZ" + assert c[0].name == "RX" + assert c[1].name == "RY" + assert c[2].name == "RZ" # Verify parameters (fixed values) assert c[0].num_params == 1 @@ -175,13 +175,13 @@ def test_loads_u_gates(self): c = loads(qasm) assert len(c) == 3 - assert c[0].instruction.name == "Phase" # U1 maps to Phase + assert c[0].name == "Phase" # U1 maps to Phase assert c[0].num_params == 1 - assert c[1].instruction.name == "U" # U2 maps to U + assert c[1].name == "U" # U2 maps to U assert c[1].num_params == 3 - assert c[2].instruction.name == "U" # U3 maps to U + assert c[2].name == "U" # U3 maps to U assert c[2].num_params == 3 def test_loads_pi_expressions(self): @@ -203,7 +203,7 @@ def test_loads_pi_expressions(self): assert c[2].num_params == 1 def test_loads_measurement(self): - """Parse measurement operation and verify directive type.""" + """Parse measurement operation and verify classical data operations.""" qasm = """ OPENQASM 2.0; include "qelib1.inc"; @@ -213,12 +213,14 @@ def test_loads_measurement(self): measure q[0] -> c[0]; """ c = loads(qasm) - assert len(c) == 2 + assert len(c) == 4 - assert c[0].instruction.name == "H" - assert c[1].instruction.is_directive - assert c[1].instruction.name == "Measure" - assert c[1].qubits[0].index == 0 + assert c[0].name == "store" + assert c[1].name == "H" + assert c[2].name == "measure_bit" + assert c[2].qubits[0].index == 0 + assert c[3].name == "store" + assert "measure q[0] -> c0[0];" in dumps(c) def test_loads_measurement_register(self): """Parse measurement on entire register.""" @@ -231,12 +233,16 @@ def test_loads_measurement_register(self): measure q -> c; """ c = loads(qasm) - # h + 2 measurements - assert len(c) == 3 + assert len(c) == 4 - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "Measure" - assert c[2].instruction.name == "Measure" + assert c[0].name == "store" + assert c[1].name == "H" + assert c[2].name == "measure_bits" + assert [q.index for q in c[2].qubits] == [0, 1] + assert c[3].name == "store" + qasm_roundtrip = dumps(c) + assert "measure q[0] -> c0[0];" in qasm_roundtrip + assert "measure q[1] -> c0[1];" in qasm_roundtrip def test_loads_barrier_single(self): """Parse barrier on single qubit.""" @@ -251,10 +257,10 @@ def test_loads_barrier_single(self): c = loads(qasm) assert len(c) == 3 - assert c[0].instruction.name == "H" - assert c[1].instruction.is_directive - assert c[1].instruction.name == "Barrier" - assert c[2].instruction.name == "X" + assert c[0].name == "H" + assert c[1].is_directive + assert c[1].name == "Barrier" + assert c[2].name == "X" def test_loads_barrier_multiple(self): """Parse barrier on multiple qubits.""" @@ -270,7 +276,7 @@ def test_loads_barrier_multiple(self): assert len(c) == 3 barrier_op = c[1] - assert barrier_op.instruction.name == "Barrier" + assert barrier_op.name == "Barrier" assert barrier_op.num_qubits == 3 def test_loads_barrier_register(self): @@ -287,7 +293,7 @@ def test_loads_barrier_register(self): assert len(c) == 3 barrier_op = c[1] - assert barrier_op.instruction.name == "Barrier" + assert barrier_op.name == "Barrier" assert barrier_op.num_qubits == 3 def test_loads_reset_single(self): @@ -302,9 +308,9 @@ def test_loads_reset_single(self): c = loads(qasm) assert len(c) == 2 - assert c[0].instruction.name == "X" - assert c[1].instruction.is_directive - assert c[1].instruction.name == "Reset" + assert c[0].name == "X" + assert c[1].is_directive + assert c[1].name == "Reset" assert c[1].qubits[0].index == 0 def test_loads_reset_register(self): @@ -320,8 +326,8 @@ def test_loads_reset_register(self): # x + 2 resets assert len(c) == 3 - assert c[1].instruction.name == "Reset" - assert c[2].instruction.name == "Reset" + assert c[1].name == "Reset" + assert c[2].name == "Reset" def test_loads_bell_state(self): """Parse Bell state circuit and verify structure.""" @@ -336,8 +342,8 @@ def test_loads_bell_state(self): assert c.num_qubits == 2 assert len(c) == 2 - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "CX" + assert c[0].name == "H" + assert c[1].name == "CX" def test_loads_ghz_state(self): """Parse GHZ state circuit and verify structure.""" @@ -353,9 +359,9 @@ def test_loads_ghz_state(self): assert c.num_qubits == 3 assert len(c) == 3 - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "CX" - assert c[2].instruction.name == "CX" + assert c[0].name == "H" + assert c[1].name == "CX" + assert c[2].name == "CX" def test_loads_multiple_qregs(self): """Parse circuit with multiple quantum registers.""" @@ -386,7 +392,7 @@ def test_loads_identity_gate(self): """ c = loads(qasm) assert len(c) == 1 - assert c[0].instruction.name == "I" + assert c[0].name == "I" def test_loads_sx_sxdg(self): """Parse SX and SXDG gates.""" @@ -400,8 +406,8 @@ def test_loads_sx_sxdg(self): c = loads(qasm) assert len(c) == 2 - assert c[0].instruction.name == "X2P" - assert c[1].instruction.name == "X2M" + assert c[0].name == "X2P" + assert c[1].name == "X2M" def test_loads_p_gate(self): """Parse P gate (alias for Phase/RZ).""" @@ -413,7 +419,7 @@ def test_loads_p_gate(self): """ c = loads(qasm) assert len(c) == 1 - assert c[0].instruction.name == "Phase" + assert c[0].name == "Phase" class TestQasm2LoadsFileIO: @@ -436,8 +442,8 @@ def test_load_from_file(self): c = load(temp_path) assert c.num_qubits == 2 assert len(c) == 2 - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "CX" + assert c[0].name == "H" + assert c[1].name == "CX" finally: os.unlink(temp_path) @@ -458,9 +464,9 @@ def test_file_roundtrip(self): c2 = load(temp_path) assert c2.num_qubits == 3 assert len(c2) == 3 - assert c2[0].instruction.name == "H" - assert c2[1].instruction.name == "CX" - assert c2[2].instruction.name == "CX" + assert c2[0].name == "H" + assert c2[1].name == "CX" + assert c2[2].name == "CX" finally: os.unlink(temp_path) @@ -480,7 +486,7 @@ def test_comments_ignored(self): """ c = loads(qasm) assert len(c) == 1 - assert c[0].instruction.name == "H" + assert c[0].name == "H" def test_whitespace_handling(self): """Verify extra whitespace is handled correctly.""" @@ -495,7 +501,7 @@ def test_whitespace_handling(self): """ c = loads(qasm) assert len(c) == 1 - assert c[0].instruction.name == "H" + assert c[0].name == "H" def test_gate_case_variations(self): """Verify gate names are case-insensitive. @@ -514,10 +520,10 @@ def test_gate_case_variations(self): """ c = loads(qasm) assert len(c) == 4 - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "X" - assert c[2].instruction.name == "Y" - assert c[3].instruction.name == "Z" + assert c[0].name == "H" + assert c[1].name == "X" + assert c[2].name == "Y" + assert c[3].name == "Z" def test_empty_circuit_with_qreg(self): """Parse circuit with only register declarations.""" @@ -548,9 +554,9 @@ def test_ghz_roundtrip(self): assert c2.num_qubits == 3 assert len(c2) == 3 - assert c2[0].instruction.name == "H" - assert c2[1].instruction.name == "CX" - assert c2[2].instruction.name == "CX" + assert c2[0].name == "H" + assert c2[1].name == "CX" + assert c2[2].name == "CX" def test_qft_roundtrip(self): """Test QFT-like circuit round-trip.""" @@ -584,9 +590,9 @@ def test_parametric_roundtrip(self): assert c2.num_qubits == 2 assert len(c2) == 3 - assert c2[0].instruction.name == "RX" - assert c2[1].instruction.name == "RY" - assert c2[2].instruction.name == "CZ" + assert c2[0].name == "RX" + assert c2[1].name == "RY" + assert c2[2].name == "CZ" def test_complex_circuit_roundtrip(self): """Test complex circuit round-trip.""" @@ -599,13 +605,8 @@ def test_complex_circuit_roundtrip(self): c1.cx(i, i + 1) c1.rx(0, 0.5) c1.barrier([0, 1, 2, 3]) - # Note: measure without conditional is commented out in dumps - # so we don't include it in round-trip comparison - qasm = dumps(c1) c2 = loads(qasm) assert c2.num_qubits == 4 - # Count only non-measure operations (measure is commented out in dumps) - expected_len = sum(1 for op in c1 if op.instruction.name != "Measure") - assert len(c2) == expected_len + assert len(c2) == len(c1) diff --git a/tests/python/ir/test_qcis_dump.py b/tests/python/ir/test_qcis_dump.py index 0e2a912..abbeb67 100644 --- a/tests/python/ir/test_qcis_dump.py +++ b/tests/python/ir/test_qcis_dump.py @@ -25,7 +25,6 @@ - Unsupported gates error handling """ -import pytest import tempfile import os import math @@ -370,24 +369,20 @@ def test_dump_overwrites_existing_file(self): os.unlink(temp_path) -class TestQcisDumpsUnsupportedGates: - """Test error handling for unsupported gates.""" +class TestQcisDumpsAdditionalStandardGates: + """Test dumping additional standard gates supported by QCIS.""" - def test_dumps_cx_raises_error(self): - """Verify CX gate raises unsupported gate error.""" + def test_dumps_cx_gate(self): + """Verify CX gate is emitted as a QCIS operation.""" c = Circuit(2) c.cx(0, 1) - with pytest.raises(Exception) as exc_info: - dumps(c) - assert "CX" in str(exc_info.value) or "compile" in str(exc_info.value) + assert dumps(c) == "CX Q0 Q1\n" def test_dumps_cnot_alias(self): - """Verify CNOT (alias for CX) raises error.""" + """Verify CNOT-style CX usage is emitted as CX.""" c = Circuit(2) - # CX is CNOT in some contexts c.cx(0, 1) - with pytest.raises(Exception): - dumps(c) + assert dumps(c) == "CX Q0 Q1\n" class TestQcisRoundTrip: @@ -444,10 +439,10 @@ def test_roundtrip_preserves_operations(self): qcis = dumps(c1) c2 = loads(qcis) - assert c2[0].instruction.name == "H" - assert c2[1].instruction.name == "X" - assert c2[2].instruction.name == "Y" - assert c2[3].instruction.name == "Z" + assert c2[0].name == "H" + assert c2[1].name == "X" + assert c2[2].name == "Y" + assert c2[3].name == "Z" def test_roundtrip_with_measurements(self): """Test round-trip with measurements.""" @@ -461,6 +456,6 @@ def test_roundtrip_with_measurements(self): c2 = loads(qcis) assert c2.num_qubits == 2 - # Measurements are expanded - assert c2[-1].instruction.name == "Measure" - assert c2[-2].instruction.name == "Measure" + # Measurements are represented as classical-data operations and dump + # back to QCIS M lines. + assert dumps(c2).endswith("M Q0\nM Q1\n") diff --git a/tests/python/ir/test_qcis_load.py b/tests/python/ir/test_qcis_load.py index 864ef98..2f64b1b 100644 --- a/tests/python/ir/test_qcis_load.py +++ b/tests/python/ir/test_qcis_load.py @@ -28,7 +28,7 @@ import pytest import tempfile import os -from cqlib.ir.qcis import loads, load +from cqlib.ir.qcis import dumps, loads, load class TestQcisLoadsBasic: @@ -60,7 +60,7 @@ def test_loads_x2p_gate(self): c = loads(qcis) assert c.num_qubits == 1 assert len(c) == 1 - assert c[0].instruction.name == "X2P" + assert c[0].name == "X2P" assert c[0].qubits[0].index == 0 def test_loads_x2m_gate(self): @@ -68,21 +68,21 @@ def test_loads_x2m_gate(self): qcis = "X2M Q0" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "X2M" + assert c[0].name == "X2M" def test_loads_y2p_gate(self): """Parse Y2P gate and verify.""" qcis = "Y2P Q0" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "Y2P" + assert c[0].name == "Y2P" def test_loads_y2m_gate(self): """Parse Y2M gate and verify.""" qcis = "Y2M Q0" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "Y2M" + assert c[0].name == "Y2M" def test_loads_xy2p_gate(self): """Parse XY2P gate with parameter and verify.""" @@ -90,7 +90,7 @@ def test_loads_xy2p_gate(self): c = loads(qcis) assert c.num_qubits == 1 assert len(c) == 1 - assert c[0].instruction.name == "XY2P" + assert c[0].name == "XY2P" assert c[0].num_params == 1 def test_loads_xy2m_gate(self): @@ -98,7 +98,7 @@ def test_loads_xy2m_gate(self): qcis = "XY2M Q0 pi/2" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "XY2M" + assert c[0].name == "XY2M" def test_loads_cz_gate(self): """Parse CZ gate and verify control/target.""" @@ -111,10 +111,10 @@ def test_loads_cz_gate(self): assert len(c) == 2 # First is H gate - assert c[0].instruction.name == "H" + assert c[0].name == "H" # Second is CZ gate - assert c[1].instruction.name == "CZ" + assert c[1].name == "CZ" assert c[1].num_qubits == 2 assert c[1].qubits[0].index == 0 assert c[1].qubits[1].index == 1 @@ -124,7 +124,7 @@ def test_loads_rz_gate(self): qcis = "RZ Q0 pi/4" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "RZ" + assert c[0].name == "RZ" assert c[0].num_params == 1 def test_loads_delay_gate(self): @@ -133,7 +133,7 @@ def test_loads_delay_gate(self): c = loads(qcis) assert len(c) == 1 # QCIS "I" gate is parsed as Delay in the internal representation - assert c[0].instruction.name == "Delay" + assert dumps(c) == "I Q0 1\n" assert c[0].num_params == 1 @@ -151,16 +151,16 @@ def test_loads_pauli_gates(self): assert c.num_qubits == 3 assert len(c) == 3 - assert c[0].instruction.name == "X" - assert c[1].instruction.name == "Y" - assert c[2].instruction.name == "Z" + assert c[0].name == "X" + assert c[1].name == "Y" + assert c[2].name == "Z" def test_loads_hadamard(self): """Parse Hadamard gate and verify.""" qcis = "H Q0" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "H" + assert c[0].name == "H" def test_loads_phase_gates(self): """Parse phase gates (S, SD, T, TD) and verify.""" @@ -174,10 +174,10 @@ def test_loads_phase_gates(self): assert c.num_qubits == 4 assert len(c) == 4 - assert c[0].instruction.name == "S" - assert c[1].instruction.name == "SDG" - assert c[2].instruction.name == "T" - assert c[3].instruction.name == "TDG" + assert c[0].name == "S" + assert c[1].name == "SDG" + assert c[2].name == "T" + assert c[3].name == "TDG" class TestQcisLoadsParametricGates: @@ -188,7 +188,7 @@ def test_loads_rx_gate(self): qcis = "RX Q0 0.5" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "RX" + assert c[0].name == "RX" assert c[0].num_params == 1 def test_loads_ry_gate(self): @@ -196,14 +196,14 @@ def test_loads_ry_gate(self): qcis = "RY Q0 pi/2" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "RY" + assert c[0].name == "RY" def test_loads_rxy_gate(self): """Parse RXY gate with two parameters and verify.""" qcis = "RXY Q0 1.0 0.5" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "RXY" + assert c[0].name == "RXY" assert c[0].num_params == 2 def test_loads_parametric_pi_expressions(self): @@ -217,9 +217,9 @@ def test_loads_parametric_pi_expressions(self): assert c.num_qubits == 3 assert len(c) == 3 - assert c[0].instruction.name == "RX" - assert c[1].instruction.name == "RY" - assert c[2].instruction.name == "RZ" + assert c[0].name == "RX" + assert c[1].name == "RY" + assert c[2].name == "RZ" def test_loads_parametric_arithmetic(self): """Parse gates with arithmetic expressions.""" @@ -241,15 +241,15 @@ def test_loads_barrier_single_qubit(self): qcis = "B Q0" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.is_directive - assert c[0].instruction.name == "Barrier" + assert c[0].is_directive + assert c[0].name == "Barrier" def test_loads_barrier_multiple_qubits(self): """Parse barrier on multiple qubits and verify.""" qcis = "B Q0 Q1 Q2 Q3" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.name == "Barrier" + assert c[0].name == "Barrier" assert c[0].num_qubits == 4 def test_loads_measurement_single_qubit(self): @@ -257,8 +257,7 @@ def test_loads_measurement_single_qubit(self): qcis = "M Q0" c = loads(qcis) assert len(c) == 1 - assert c[0].instruction.is_directive - assert c[0].instruction.name == "Measure" + assert dumps(c) == "M Q0\n" assert c[0].qubits[0].index == 0 def test_loads_measurement_multiple_qubits(self): @@ -268,8 +267,8 @@ def test_loads_measurement_multiple_qubits(self): # Each qubit measurement becomes a separate operation assert c.num_qubits == 3 assert len(c) == 3 + assert dumps(c) == "M Q0\nM Q1\nM Q2\n" for i in range(3): - assert c[i].instruction.name == "Measure" assert c[i].qubits[0].index == i @@ -289,11 +288,10 @@ def test_loads_bell_state(self): assert c.num_qubits == 2 assert len(c) == 5 - assert c[0].instruction.name == "X2P" - assert c[1].instruction.name == "X2M" - assert c[2].instruction.name == "CZ" - assert c[3].instruction.name == "Measure" - assert c[4].instruction.name == "Measure" + assert c[0].name == "X2P" + assert c[1].name == "X2M" + assert c[2].name == "CZ" + assert dumps(c).endswith("M Q0\nM Q1\n") def test_loads_ghz_state(self): """Parse GHZ state circuit and verify.""" @@ -360,10 +358,10 @@ def test_loads_gate_order_preserved(self): c = loads(qcis) assert len(c) == 4 - assert c[0].instruction.name == "X" - assert c[1].instruction.name == "Y" - assert c[2].instruction.name == "Z" - assert c[3].instruction.name == "H" + assert c[0].name == "X" + assert c[1].name == "Y" + assert c[2].name == "Z" + assert c[3].name == "H" class TestQcisLoadsFileIO: @@ -385,8 +383,8 @@ def test_load_from_file(self): c = load(temp_path) assert c.num_qubits == 2 assert len(c) == 4 - assert c[0].instruction.name == "H" - assert c[1].instruction.name == "CZ" + assert c[0].name == "H" + assert c[1].name == "CZ" finally: os.unlink(temp_path) diff --git a/tests/python/parameter/test_creation.py b/tests/python/parameter/test_creation.py index c7285d2..4424458 100644 --- a/tests/python/parameter/test_creation.py +++ b/tests/python/parameter/test_creation.py @@ -178,10 +178,8 @@ def test_constant_no_symbols(self): symbols = p.symbols assert len(symbols) == 0 - def test_pi_constant_symbols(self): - """Pi constant contains the π symbol.""" + def test_pi_constant_has_no_free_symbols(self): + """Pi constant is resolved automatically and has no free symbols.""" pi = Parameter.pi() symbols = pi.symbols - # The symbol library treats π as a symbol - assert len(symbols) == 1 - assert "π" in symbols + assert len(symbols) == 0 diff --git a/tests/python/qis/test_entropy.py b/tests/python/qis/test_entropy.py index 9fb34c1..defb247 100644 --- a/tests/python/qis/test_entropy.py +++ b/tests/python/qis/test_entropy.py @@ -60,7 +60,7 @@ def test_entanglement_entropy_pure(): entropy.entanglement_entropy_pure(sv, [0, 1]) # Subsystem equals full system with pytest.raises(ValueError): entropy.entanglement_entropy_pure(sv, [0, 0]) # Duplicates - with pytest.raises(ValueError): + with pytest.raises(IndexError): entropy.entanglement_entropy_pure(sv, [2]) # Out of bounds @@ -79,7 +79,7 @@ def test_negativity(): assert abs(entropy.negativity(dm_sep, [0]) - 0.0) < 1e-10 # Edge cases - with pytest.raises(ValueError): + with pytest.raises(IndexError): entropy.negativity(dm, [2]) # Out of bounds @@ -400,5 +400,5 @@ def test_negativity_invalid_subsystem(self): """Test negativity with invalid subsystem.""" dm = DensityMatrix(2) - with pytest.raises(ValueError): + with pytest.raises(IndexError): entropy.negativity(dm, [0, 1, 2]) # Out of bounds diff --git a/tests/python/qis/test_metrics.py b/tests/python/qis/test_metrics.py index 4f01e1a..43a2382 100644 --- a/tests/python/qis/test_metrics.py +++ b/tests/python/qis/test_metrics.py @@ -156,7 +156,7 @@ def test_partial_transpose(): assert math.isclose(data[3, 3].real, 0.5, abs_tol=1e-10) # Invalid subsystem - with pytest.raises(ValueError): + with pytest.raises(IndexError): metrics.partial_transpose(dm, [2]) @@ -489,18 +489,18 @@ def test_partial_transpose_invalid_qubits(self): """Test partial transpose with out-of-bounds qubit indices.""" dm = DensityMatrix(2) - # Out of bounds should raise ValueError - with pytest.raises(ValueError): + # Out of bounds should raise IndexError + with pytest.raises(IndexError): metrics.partial_transpose(dm, [2]) - with pytest.raises(ValueError): + with pytest.raises(IndexError): metrics.partial_transpose(dm, [0, 3]) def test_logarithmic_negativity_invalid_qubits(self): """Test logarithmic negativity with out-of-bounds qubit indices.""" dm = DensityMatrix(3) - # Out of bounds should raise ValueError - with pytest.raises(ValueError): + # Out of bounds should raise IndexError + with pytest.raises(IndexError): metrics.logarithmic_negativity(dm, [0, 5]) def test_trace_distance_mixed_dimension_mismatch(self):