use super::*;
use crate::circuit::gate::{ClassicalDataOp, Instruction};
use crate::circuit::{
Circuit, ClassicalExpr, ClassicalType, Parameter, ParameterValue, Qubit, StandardGate,
};
use crate::ir::qasm3::load::Qasm3ParseError;
use crate::ir::{qasm3_load, qasm3_loads, qcis_loads};
use std::error::Error;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn unique_temp_path(test_name: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!(
"cqlib_qasm3_dump_{}_{}_{}.qasm",
std::process::id(),
test_name,
nonce
))
}
fn assert_round_trip_standard_gates(source: &str, expected: &[StandardGate]) {
let circuit = qasm3_loads(source).unwrap();
let actual = circuit
.operations()
.iter()
.filter_map(|op| match op.instruction {
Instruction::Standard(gate) => Some(gate),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
#[test]
fn dumps_stdgate_only_circuit_without_extra_definitions() {
let q0 = Qubit::new(0);
let q1 = Qubit::new(1);
let mut circuit = Circuit::new(2);
circuit.h(q0).unwrap();
circuit.cx(q0, q1).unwrap();
circuit.phase(q1, std::f64::consts::PI / 2.0).unwrap();
circuit.u(q0, 0.1, 0.2, 0.3).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
h q[0];
cx q[0],q[1];
p(1.5707963267948966) q[1];
u3(0.1,0.2,0.3) q[0];
"#
);
assert_round_trip_standard_gates(
&qasm,
&[
StandardGate::H,
StandardGate::CX,
StandardGate::Phase,
StandardGate::U,
],
);
}
#[test]
fn to_string_alias_matches_dumps() {
let mut circuit = Circuit::new(1);
circuit.h(Qubit::new(0)).unwrap();
assert_eq!(to_string(&circuit).unwrap(), dumps(&circuit).unwrap());
}
#[test]
fn to_path_alias_writes_file() {
let mut circuit = Circuit::new(1);
circuit.x(Qubit::new(0)).unwrap();
let path = unique_temp_path("to_path");
to_path(&circuit, path.as_path()).unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), dumps(&circuit).unwrap());
fs::remove_file(path).unwrap();
}
#[test]
fn dumps_extension_gate_definitions_and_keeps_call_names() {
let q0 = Qubit::new(0);
let q1 = Qubit::new(1);
let mut circuit = Circuit::new(2);
circuit.x2p(q0).unwrap();
circuit.x2m(q1).unwrap();
circuit.y2p(q0).unwrap();
circuit.y2m(q1).unwrap();
circuit.xy2p(q0, 0.25).unwrap();
circuit.xy2m(q1, 0.5).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
gate x2p q { rx(pi/2) q; }
gate x2m q { rx(-pi/2) q; }
gate y2p q { ry(pi/2) q; }
gate y2m q { ry(-pi/2) q; }
gate xy2p(phi) q { rz(-phi) q; x2p q; rz(phi) q; }
gate xy2m(phi) q { rz(-phi) q; x2m q; rz(phi) q; }
qubit[2] q;
x2p q[0];
x2m q[1];
y2p q[0];
y2m q[1];
xy2p(0.25) q[0];
xy2m(0.5) q[1];
"#
);
assert_round_trip_standard_gates(
&qasm,
&[
StandardGate::X2P,
StandardGate::X2M,
StandardGate::Y2P,
StandardGate::Y2M,
StandardGate::XY2P,
StandardGate::XY2M,
],
);
}
#[test]
fn dumps_ising_gate_definitions_and_keeps_call_names() {
let q0 = Qubit::new(0);
let q1 = Qubit::new(1);
let mut circuit = Circuit::new(2);
circuit.rxx(q0, q1, 0.1).unwrap();
circuit.ryy(q0, q1, 0.2).unwrap();
circuit.rzz(q0, q1, 0.3).unwrap();
circuit.rzx(q0, q1, 0.4).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
gate rxx(theta) a,b { h a; h b; cx a,b; rz(theta) b; cx a,b; h a; h b; }
gate ryy(theta) a,b { rx(pi/2) a; rx(pi/2) b; cx a,b; rz(theta) b; cx a,b; rx(-pi/2) a; rx(-pi/2) b; }
gate rzz(theta) a,b { cx a,b; rz(theta) b; cx a,b; }
gate rzx(theta) a,b { h b; cx a,b; rz(theta) b; cx a,b; h b; }
qubit[2] q;
rxx(0.1) q[0],q[1];
ryy(0.2) q[0],q[1];
rzz(0.3) q[0],q[1];
rzx(0.4) q[0],q[1];
"#
);
assert_round_trip_standard_gates(
&qasm,
&[
StandardGate::RXX,
StandardGate::RYY,
StandardGate::RZZ,
StandardGate::RZX,
],
);
}
#[test]
fn dumps_gphase_as_statement() {
let mut circuit = Circuit::new(0);
circuit
.append(
Instruction::Standard(StandardGate::GPhase),
std::iter::empty::<Qubit>(),
[ParameterValue::Fixed(0.25)],
None,
)
.unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[0] q;
gphase(0.25);
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert_eq!(loaded.operations().len(), 0);
assert!((loaded.global_phase().evaluate(&None).unwrap() - 0.25).abs() < 1e-10);
}
#[test]
fn dumps_circuit_global_phase_as_gphase_statement() {
let mut circuit = Circuit::new(1);
circuit.set_global_phase(Parameter::from(0.25));
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
gphase(0.25);
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert!((loaded.global_phase().evaluate(&None).unwrap() - 0.25).abs() < 1e-10);
}
#[test]
fn dumps_symbolic_circuit_global_phase_as_gphase_statement() {
let mut circuit = Circuit::new(0);
circuit.set_global_phase(Parameter::symbol("theta"));
let qasm = dumps(&circuit).unwrap();
assert!(qasm.contains("gphase(theta);"), "got:\n{qasm}");
}
#[test]
fn round_trips_loaded_top_level_gphase() {
let circuit = qasm3_loads(
r#"
OPENQASM 3;
include "stdgates.inc";
qubit q;
gphase(0.25);
x q;
"#,
)
.unwrap();
let qasm = dumps(&circuit).unwrap();
let loaded = qasm3_loads(&qasm).unwrap();
assert!(qasm.contains("gphase(0.25);"), "got:\n{qasm}");
assert!((loaded.global_phase().evaluate(&None).unwrap() - 0.25).abs() < 1e-10);
}
#[test]
fn dumps_measurement_reset_and_barrier() {
let q0 = Qubit::new(0);
let q1 = Qubit::new(1);
let mut circuit = Circuit::new(2);
let bits = circuit.var(ClassicalType::bit_vec(2).unwrap());
circuit.measure_bits_into([q0, q1], bits).unwrap();
circuit.barrier(vec![q0, q1]).unwrap();
circuit.reset(q1).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c0;
c0 = measure q;
barrier q[0],q[1];
reset q[1];
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert!(matches!(
loaded.operations()[0].instruction,
Instruction::ClassicalData(ClassicalDataOp::MeasureBits { .. })
));
}
#[test]
fn dumps_partial_bitvec_measurement_as_indexed_assignments() {
let q0 = Qubit::new(0);
let q2 = Qubit::new(2);
let mut circuit = Circuit::new(3);
let bits = circuit.var(ClassicalType::bit_vec(2).unwrap());
circuit.measure_bits_into([q2, q0], bits).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[2] c0;
c0[0] = measure q[2];
c0[1] = measure q[0];
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert_eq!(loaded.num_qubits(), 3);
assert_eq!(loaded.operations().len(), 4);
}
#[test]
fn dumps_unused_partial_bitvec_measurement_to_generated_register() {
let q0 = Qubit::new(0);
let q2 = Qubit::new(2);
let mut circuit = Circuit::new(3);
circuit.measure_bits([q2, q0]).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[2] meas;
meas[0] = measure q[2];
meas[1] = measure q[0];
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert_eq!(loaded.num_qubits(), 3);
assert_eq!(loaded.operations().len(), 4);
}
#[test]
fn dumps_single_bit_measurement_round_trip() {
let q0 = Qubit::new(0);
let mut circuit = Circuit::new(1);
let bit = circuit.var(ClassicalType::Bit);
circuit.measure_into(q0, bit).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit c0;
c0 = measure q[0];
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert!(matches!(
loaded.operations()[0].instruction,
Instruction::ClassicalData(ClassicalDataOp::MeasureBit { .. })
));
}
#[test]
fn dumps_unused_measurement_values_to_generated_register() {
let q0 = Qubit::new(0);
let q1 = Qubit::new(1);
let mut circuit = Circuit::new(2);
circuit.measure(q0).unwrap();
circuit.measure(q1).unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] meas;
meas[0] = measure q[0];
meas[1] = measure q[1];
"#
);
let loaded = qasm3_loads(&qasm).unwrap();
assert_eq!(loaded.operations().len(), 4);
}
#[test]
fn qcis_measurements_dump_to_reloadable_qasm3() {
let circuit = qcis_loads("M Q0 Q1\n").unwrap();
let qasm = dumps(&circuit).unwrap();
assert!(!qasm.contains("bit v"), "got:\n{qasm}");
assert!(qasm.contains("bit[2] meas;"), "got:\n{qasm}");
assert!(qasm.contains("meas[0] = measure q[0];"), "got:\n{qasm}");
assert!(qasm.contains("meas[1] = measure q[1];"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn qcis_single_measurement_dumps_with_explicit_registers() {
let circuit = qcis_loads("H Q0\nM Q0\n").unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] meas;
h q[0];
meas[0] = measure q[0];
"#
);
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn qcis_sparse_qubit_measurement_declares_enough_qubits() {
let circuit = qcis_loads("H Q1\nM Q1\n").unwrap();
let qasm = dumps(&circuit).unwrap();
assert_eq!(
qasm,
r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
// q[0] -> Q1
bit[1] meas;
h q[0];
meas[0] = measure q[0];
"#
);
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn dumps_general_store_assignment() {
let mut circuit = Circuit::new(0);
let bit = circuit.var(ClassicalType::Bool);
circuit
.store(bit, ClassicalExpr::bool_literal(true))
.unwrap();
let qasm = dumps(&circuit).unwrap();
assert!(qasm.contains("bool c0;"), "got:\n{qasm}");
assert!(qasm.contains("c0 = true;"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn rejects_scalar_bit_store_assignment() {
let mut circuit = Circuit::new(0);
let bit = circuit.var(ClassicalType::Bit);
circuit
.store(bit, ClassicalExpr::bit_literal(true))
.unwrap();
let err = dumps(&circuit).unwrap_err();
assert!(matches!(
err,
Qasm3DumpError::UnsupportedClassicalData(message)
if message.contains("scalar bit store")
));
}
#[test]
fn round_trips_if_branch_measurement_with_initializer() {
let source = r#"OPENQASM 3.0;
include "stdgates.inc";
bool flag = true;
qubit[2] q;
if (flag) {
h q[0];
measure q[0];
}
measure q[1];
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("bool c0;"), "got:\n{qasm}");
assert!(qasm.contains("c0 = true;"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn round_trips_switch_branch_measurements_with_initializer() {
let source = r#"OPENQASM 3.0;
include "stdgates.inc";
uint[2] selector = 1;
qubit[3] q;
switch (selector) {
case 0 {
x q[0];
measure q[0];
}
case 1 {
h q[1];
measure q[1];
}
default {
z q[2];
measure q[2];
}
}
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("uint[2] c0;"), "got:\n{qasm}");
assert!(qasm.contains("c0 = 1;"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn round_trips_extension_gates_then_measurement() {
let source = r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
rxx(pi/7) q[0], q[1];
ryy(-pi/5) q[0], q[1];
rzz(0.125) q[0], q[1];
measure q[1];
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("gate rxx(theta)"), "got:\n{qasm}");
assert!(qasm.contains("gate ryy(theta)"), "got:\n{qasm}");
assert!(qasm.contains("gate rzz(theta)"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn round_trips_nested_if_measurements_with_initializers() {
let source = r#"OPENQASM 3.0;
bool outer = true;
bool inner = false;
qubit[2] q;
if (outer) {
if (inner) {
measure q[0];
} else {
measure q[1];
}
}
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("bool c0;"), "got:\n{qasm}");
assert!(qasm.contains("bool c1;"), "got:\n{qasm}");
assert!(qasm.contains("c0 = true;"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn round_trips_symbolic_parameter_input() {
let source = r#"OPENQASM 3.0;
include "stdgates.inc";
input angle[64] theta;
qubit q;
rx(theta) q;
measure q;
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("input angle[64] theta;"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn parameter_renaming_preserves_overlapping_names_through_round_trip() {
let q0 = Qubit::new(0);
let mut gate = Circuit::new(1);
gate.x(q0).unwrap();
let mut circuit = Circuit::new(1);
circuit
.append(gate.to_gate("a").unwrap(), [q0], [], None)
.unwrap();
circuit.rx(q0, Parameter::symbol("a")).unwrap();
circuit.rz(q0, Parameter::symbol("a0")).unwrap();
circuit.set_global_phase(Parameter::symbol("a") + Parameter::symbol("a0") * 2.0);
let qasm = dumps(&circuit).unwrap();
assert!(qasm.contains("input angle[64] a0;"), "got:\n{qasm}");
assert!(qasm.contains("input angle[64] a00;"), "got:\n{qasm}");
assert!(qasm.contains("rx(a0) q[0];"), "got:\n{qasm}");
assert!(qasm.contains("rz(a00) q[0];"), "got:\n{qasm}");
let loaded = qasm3_loads(&qasm).unwrap();
let values = Some(HashMap::from([("a0", 0.125), ("a00", 0.5)]));
assert_eq!(loaded.global_phase().evaluate(&values).unwrap(), 1.125);
let angles: Vec<_> = loaded
.operations()
.iter()
.filter(|op| {
matches!(
op.instruction,
Instruction::Standard(StandardGate::RX | StandardGate::RZ)
)
})
.map(|op| {
loaded
.resolve_parameter(&op.params[0])
.unwrap()
.evaluate(&values)
.unwrap()
})
.collect();
assert_eq!(angles, [0.125, 0.5]);
}
#[test]
fn parameter_renaming_resolves_swapped_symbols_simultaneously() {
let mut circuit = Circuit::new(1);
circuit
.rx(
Qubit::new(0),
Parameter::symbol("a") + Parameter::symbol("b") * 2.0,
)
.unwrap();
let param_map = HashMap::from([
("a".to_string(), Parameter::symbol("b")),
("b".to_string(), Parameter::symbol("a")),
]);
let renamed = resolve_param(&circuit.operations()[0], 0, &circuit, ¶m_map).unwrap();
assert_eq!(
renamed
.evaluate(&Some(HashMap::from([("a", 2.0), ("b", 3.0)])))
.unwrap(),
7.0
);
}
#[test]
fn parameter_renaming_swaps_global_phase_symbols_simultaneously() {
let mut circuit = Circuit::new(1);
circuit.set_global_phase(Parameter::symbol("a") + Parameter::symbol("b") * 2.0);
let param_map = HashMap::from([
("a".to_string(), Parameter::symbol("b")),
("b".to_string(), Parameter::symbol("a")),
]);
let mut output = String::new();
dump_global_phase(&circuit, &mut output, ¶m_map).unwrap();
let phase = Parameter::try_from(
output
.strip_prefix("gphase(")
.unwrap()
.strip_suffix(");\n")
.unwrap(),
)
.unwrap();
assert_eq!(
phase
.evaluate(&Some(HashMap::from([("a", 2.0), ("b", 3.0)])))
.unwrap(),
7.0
);
}
#[test]
fn avoids_auto_measurement_name_collision_with_gate() {
let source = r#"OPENQASM 3.0;
include "stdgates.inc";
gate meas a {
x a;
}
qubit target;
meas target;
measure target;
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("gate meas"), "got:\n{qasm}");
assert!(qasm.contains("bit[1] meas0;"), "got:\n{qasm}");
assert!(qasm.contains("meas0[0] = measure q[0];"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn avoids_qubit_register_name_collision_with_gate() {
let source = r#"OPENQASM 3.0;
include "stdgates.inc";
gate q a {
h a;
}
qubit target;
q target;
measure target;
"#;
let qasm = dumps(&qasm3_loads(source).unwrap()).unwrap();
assert!(qasm.contains("gate q"), "got:\n{qasm}");
assert!(qasm.contains("qubit[1] q0;"), "got:\n{qasm}");
assert!(qasm.contains("q q0[0];"), "got:\n{qasm}");
assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}");
}
#[test]
fn rejects_delay() {
let q0 = Qubit::new(0);
let mut circuit = Circuit::new(1);
circuit
.append(Instruction::Delay, [q0], [ParameterValue::Fixed(1.0)], None)
.unwrap();
let err = dumps(&circuit).unwrap_err();
assert!(matches!(
err,
Qasm3DumpError::UnsupportedInstruction(message) if message == "delay"
));
}
#[test]
fn load_preserves_io_error_source() {
let path = std::env::temp_dir()
.join(format!("cqlib_qasm3_missing_{}", std::process::id()))
.join("missing.qasm");
let err = qasm3_load(&path).unwrap_err();
assert!(err.source().is_some());
assert!(matches!(
err,
Qasm3ParseError::IoError(error) if error.kind() == std::io::ErrorKind::NotFound
));
}
#[test]
fn dump_preserves_io_error_source() {
let circuit = Circuit::new(0);
let path = std::env::temp_dir()
.join(format!("cqlib_qasm3_missing_{}", std::process::id()))
.join("out.qasm");
let err = dump(&circuit, &path).unwrap_err();
assert!(err.source().is_some());
assert!(matches!(
err,
Qasm3DumpError::IoError(error) if error.kind() == std::io::ErrorKind::NotFound
));
}
#[test]
fn parameter_errors_propagate_from_gate_export_with_context() {
use crate::circuit::{CircuitParam, ParameterValue};
use std::collections::HashMap;
let mut circuit = Circuit::new(1);
let qubit = Qubit::new(0);
let qubit_map = HashMap::from([(qubit, "q[0]".to_string())]);
for (gate, params) in [
(StandardGate::RX, vec![ParameterValue::Fixed(0.5)]),
(
StandardGate::U,
vec![
ParameterValue::Fixed(0.5),
ParameterValue::Fixed(0.25),
ParameterValue::Fixed(0.75),
],
),
] {
circuit
.append(Instruction::Standard(gate), [qubit], params, None)
.unwrap();
let valid = circuit.operations().last().unwrap();
let param_index = valid.params.len() - 1;
for param in [
CircuitParam::Index(99),
CircuitParam::Fixed(f64::NAN),
CircuitParam::Fixed(f64::INFINITY),
CircuitParam::Fixed(f64::NEG_INFINITY),
] {
let mut operation = valid.clone();
operation.params[param_index] = param;
let mut output = String::new();
let error = super::dump_standard_gate(
&gate,
&operation,
&circuit,
&mut output,
&qubit_map,
&HashMap::new(),
)
.unwrap_err();
assert!(
matches!(error, Qasm3DumpError::FormatError(ref message)
if message.contains(&format!("parameter {param_index}"))
&& message.contains(&format!("{gate:?}"))),
"{error}"
);
}
}
}
#[test]
fn export_preserves_indexed_constants_and_symbol_substitution() {
use crate::circuit::ParameterValue;
use std::collections::HashMap;
let mut circuit = Circuit::new(1);
circuit
.rx(Qubit::new(0), ParameterValue::Param(Parameter::pi()))
.unwrap();
circuit
.rx(
Qubit::new(0),
ParameterValue::Param(Parameter::symbol("theta")),
)
.unwrap();
let qasm = dumps(&circuit).unwrap();
assert!(qasm.contains("rx(pi)"), "{qasm}");
let replacements = HashMap::from([("theta".to_string(), Parameter::symbol("angle0"))]);
assert_eq!(
super::format_params(&circuit.operations()[1], &circuit, &replacements).unwrap(),
"(angle0)"
);
}