Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"format": "PHIR/JSON",
"version": "0.1.0",
"metadata": {
"source_program_type": [
"PECOS.QuantumCircuit",
[
"PECOS",
"fixture"
]
],
"num_qubits": 0
},
"ops": []
}
121 changes: 117 additions & 4 deletions crates/pecos-phir-json/tests/quantum_declarations.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use pecos_engines::{ClassicalEngine, byte_message::ByteMessage};
use pecos_phir::ops::Operation as PhirOperation;
use pecos_phir_json::{
phir_json_to_module,
Expand Down Expand Up @@ -61,6 +62,18 @@ fn declaration_verdicts() {
false,
"data_type",
),
(
"boolean size",
json!({"data":"qvar_define","variable":"q","size":true}),
false,
"size",
),
(
"fractional size",
json!({"data":"qvar_define","variable":"q","size":2.0}),
false,
"size",
),
(
"negative size",
json!({"data":"qvar_define","variable":"q","size":-1}),
Expand All @@ -82,6 +95,7 @@ fn declaration_verdicts() {
] {
let input = program(std::slice::from_ref(&declaration));
let mut interpreter = PhirClassicalInterpreter::new();
let engine = PhirJsonEngine::from_json(&input);
let verdicts = [
(
"converter",
Expand All @@ -104,9 +118,7 @@ fn declaration_verdicts() {
),
(
"engine",
PhirJsonEngine::from_json(&input)
.map(|_| ())
.map_err(|e| e.to_string()),
engine.as_ref().map(|_| ()).map_err(ToString::to_string),
),
];
for (entry, result) in verdicts {
Expand All @@ -125,6 +137,12 @@ fn declaration_verdicts() {
if accepted {
let expected = declaration["size"].as_u64().unwrap();
assert_eq!(interpreter.num_qubits(), usize::try_from(expected).unwrap());
let engine = engine.unwrap();
assert_eq!(engine.num_qubits(), usize::try_from(expected).unwrap());
assert_eq!(
engine.processor.environment.resolve_qubit("q", 0).unwrap(),
0
);
let ast: PHIRProgram = serde_json::from_str(&input).unwrap();
assert!(
matches!(&ast.ops[0], Operation::VariableDefinition { data_type, size: Some(size), .. }
Expand All @@ -144,7 +162,7 @@ fn optional_type_preserves_declaration_order_ids() {
let input = program(&[
json!({"data":"qvar_define","variable":"z","size":2}),
q,
json!({"qop":"X","args":[["a",1],["z",1]]}),
json!({"qop":"CX","args":[["a",1],["z",1]]}),
]);
let module = phir_json_to_module(&input).unwrap();
let gate = module.body.blocks[0]
Expand All @@ -156,6 +174,21 @@ fn optional_type_preserves_declaration_order_ids() {
gate.operands.iter().map(|v| v.id).collect::<Vec<_>>(),
[3, 1]
);
let mut engine = PhirJsonEngine::from_json(&input).unwrap();
assert_eq!(
engine.num_qubits(),
4,
"header must register both quantum declarations"
);
assert_eq!(
engine.processor.environment.resolve_qubit("a", 1).unwrap(),
3
);
assert_eq!(
engine.processor.environment.resolve_qubit("z", 1).unwrap(),
1
);
assert_eq!(command_qubits(&engine.generate_commands().unwrap()), [3, 1]);
let ast: PHIRProgram = serde_json::from_str(&input).unwrap();
let mut executor = BlockExecutor::new();
for op in &ast.ops[..2] {
Expand Down Expand Up @@ -252,3 +285,83 @@ fn quantum_size_conversion_checks_platform_overflow() {
("q", usize::MAX)
);
}

fn command_qubits(message: &ByteMessage) -> Vec<usize> {
message
.quantum_ops()
.unwrap()
.iter()
.flat_map(|gate| gate.qubits.iter().map(|qubit| qubit.0))
.collect()
}

#[test]
fn processor_rejects_zero_size() {
let mut processor = OperationProcessor::new();
let result = processor.handle_variable_definition("qvar_define", "qubits", "q", 0);
assert!(
result.is_err(),
"processor must reject zero size, got {result:?}"
);
let message = result.unwrap_err().to_string();
assert!(
message.contains("'q'") && message.contains("positive size"),
"{message}"
);
assert_eq!(processor.environment.count_qubits(), 0);
assert!(!processor.environment.has_variable("q"));
}

#[test]
fn declarations_execute_through_blocks_and_engine_commands() {
let input = program(&[
json!({"data":"qvar_define","variable":"z","size":2}),
json!({"data":"qvar_define","variable":"a","size":2}),
json!({"qop":"CX","args":[["a",1],["z",1]]}),
]);
let ast: PHIRProgram = serde_json::from_str(&input).unwrap();
let mut executor = BlockExecutor::new();
executor.execute_program(&ast.ops).unwrap();
assert_eq!(executor.processor.environment.count_qubits(), 4);
assert_eq!(command_qubits(&executor.get_builder().build()), [3, 1]);

let mut engine = PhirJsonEngine::from_json(&input).unwrap();
// The processor is public; replacing it isolates execution from header registration.
engine.processor = OperationProcessor::new();
let commands = engine
.generate_commands()
.expect("runtime declarations must register qubits before executing gates");
assert_eq!(engine.num_qubits(), 4);
assert_eq!(command_qubits(&commands), [3, 1]);

let mut engine = PhirJsonEngine::from_json(&input).unwrap();
engine.processor = OperationProcessor::new();
engine.processor.add_quantum_variable("z", 3).unwrap();
let error = engine
.generate_commands()
.map(|_| ())
.unwrap_err()
.to_string();
assert!(
error.contains("Conflicting definition for variable 'z'"),
"{error}"
);
}

#[test]
fn empty_quantum_circuit_round_trip() {
// The Python producer test checks its entire output against this same fixture.
let input = include_str!("fixtures/empty_quantum_circuit.phir.json");
let module = phir_json_to_module(input).unwrap();
assert!(module.body.blocks[0].operations.is_empty());
let ast: PHIRProgram = serde_json::from_str(input).unwrap();
assert!(ast.ops.is_empty());
let mut interpreter = PhirClassicalInterpreter::new();
assert_eq!(interpreter.init(input, None).unwrap(), 0);
assert!(interpreter.execute_program().unwrap().is_empty());
let mut engine = PhirJsonEngine::from_json(input).unwrap();
assert_eq!(engine.num_qubits(), 0);
assert!(command_qubits(&engine.generate_commands().unwrap()).is_empty());
let engine = PhirJsonEngine::from_program(ast).unwrap();
assert_eq!(engine.num_qubits(), 0);
}
24 changes: 22 additions & 2 deletions crates/pecos-qasm/src/qasm_to_phir_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ pub fn program_to_phir_json(program: &Program) -> Result<Value, String> {

// 1) Quantum register definitions
for (name, qubit_ids) in &program.quantum_registers {
if qubit_ids.is_empty() {
continue;
}
ops.push(json!({
"data": "qvar_define",
"data_type": "qubits",
Expand All @@ -74,6 +77,9 @@ pub fn program_to_phir_json(program: &Program) -> Result<Value, String> {
// a signed register would need size + 1 <= N and could not represent 64
// bits at all).
for (name, size) in &program.classical_registers {
if *size == 0 {
continue;
}
let dtype = classical_register_dtype(*size)?;
ops.push(json!({
"data": "cvar_define",
Expand All @@ -91,8 +97,9 @@ pub fn program_to_phir_json(program: &Program) -> Result<Value, String> {
// 4) Export all classical variables
let cvar_names: Vec<&str> = program
.classical_registers
.keys()
.map(String::as_str)
.iter()
.filter(|(_, size)| **size > 0)
.map(|(name, _)| name.as_str())
.collect();
if !cvar_names.is_empty() {
ops.push(json!({
Expand Down Expand Up @@ -512,6 +519,19 @@ mod tests {
assert!(phir["ops"].is_array());
}

#[test]
fn empty_registers_are_not_declared_or_exported() {
let mut program = Program::default();
program.quantum_registers.insert("empty_q".into(), vec![]);
program.classical_registers.insert("empty_c".into(), 0);
assert_eq!(program_to_phir_json(&program).unwrap()["ops"], json!([]));
program.quantum_registers.insert("q".into(), vec![0]);
program.classical_registers.insert("c".into(), 1);
let phir = program_to_phir_json(&program).unwrap();
assert_eq!(phir["ops"].as_array().unwrap().len(), 3);
assert_eq!(phir["ops"][2]["variables"], json!(["c"]));
}

#[test]
fn register_definitions() {
let phir = convert(
Expand Down
20 changes: 18 additions & 2 deletions exp/zlup/src/codegen/phir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ impl PhirJsonCodegen {
}

// Add quantum variable definitions
for alloc in self.allocators.values() {
for alloc in self.allocators.values().filter(|alloc| alloc.capacity > 0) {
phir.ops
.push(PhirJsonOp::QvarDefine(PhirJsonQvarDefine::new(
&alloc.name,
Expand Down Expand Up @@ -773,7 +773,7 @@ impl PhirJsonCodegen {
let mut phir = PhirJsonProgram::new().with_name(&fn_decl.name);

// Add definitions
for alloc in self.allocators.values() {
for alloc in self.allocators.values().filter(|alloc| alloc.capacity > 0) {
phir.ops
.push(PhirJsonOp::QvarDefine(PhirJsonQvarDefine::new(
&alloc.name,
Expand Down Expand Up @@ -1549,6 +1549,22 @@ impl PhirJsonCodegen {
mod tests {
use super::*;

#[test]
fn empty_quantum_allocator_is_not_declared() {
let ast = crate::parse("pub fn main() -> unit { q := qalloc(0); return unit; }").unwrap();
assert!(PhirJsonCodegen::new().compile(&ast).unwrap().ops.is_empty());
let TopLevelDecl::Fn(function) = &ast.declarations[0] else {
panic!("expected main function");
};
assert!(
PhirJsonCodegen::new()
.compile_function(function)
.unwrap()
.ops
.is_empty()
);
}

#[test]
fn test_bell_state() {
let source = r#"
Expand Down
6 changes: 5 additions & 1 deletion python/quantum-pecos/src/pecos/circuits/qc2phir.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ def ops_buffer_append(
ops = prog["ops"]

for sym, size in qc.metadata.get("qvar_spec", {}).items():
if size == 0:
continue
ops.append(
{
"data": "qvar_define",
Expand All @@ -251,6 +253,8 @@ def ops_buffer_append(
)

for sym, size in qc.metadata.get("cvar_spec", {}).items():
if size == 0:
continue
ops.append(
{
"data": "cvar_define",
Expand Down Expand Up @@ -386,7 +390,7 @@ def ops_buffer_append(
num_qubits = len(qid2qsym)
prog["metadata"]["num_qubits"] = num_qubits

if "qvar_spec" not in qc.metadata:
if "qvar_spec" not in qc.metadata and num_qubits > 0:
op = {
"data": "qvar_define",
"data_type": "qubits",
Expand Down
9 changes: 7 additions & 2 deletions python/quantum-pecos/src/pecos/reps/pyphir/pyphir.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,20 @@ def from_phir(
msg = f"Do not know handle qvar type: {o['data_type']}"
raise Exception(msg)

size = o.get("size")
if not isinstance(size, int) or isinstance(size, bool) or size <= 0:
msg = f"Quantum register '{o['variable']}' requires a positive integer size; got {size!r}"
raise ValueError(msg)

qubit_ids = []
for _i in range(o["size"]):
for _i in range(size):
qubit_ids.append(next_qvar_int)
next_qvar_int += 1

data = d.QVarDefine(
data_type=data_type,
variable=o["variable"],
size=o["size"],
size=size,
qubit_ids=qubit_ids,
metadata=o.get("metadata"),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
"""Quantum declarations retain upstream schema validation and Python semantics."""

import json
from pathlib import Path

import pecos
import pytest
from pecos.circuits.qc2phir import to_phir_dict
from pecos.classical_interpreters.phir_classical_interpreter import PhirClassicalInterpreter
from pecos.reps.pyphir import PyPHIR
from pecos.typing import PhirModel


@pytest.mark.parametrize("explicit_type", [False, True])
Expand Down Expand Up @@ -36,14 +42,19 @@ def test_wrong_quantum_type(data_type: str | None) -> None:
reader(program)


@pytest.mark.parametrize("fields", [{}, {"size": 0}, {"size": -1}, {"size": "2"}])
@pytest.mark.parametrize(
"fields",
[{}, {"size": 0}, {"size": -1}, {"size": True}, {"size": False}, {"size": 2.0}, {"size": "2"}, {"size": None}],
)
def test_quantum_size_schema_remains_enforced(fields: dict) -> None:
"""The full upstream schema still rejects missing, zero and malformed sizes."""
program = {
"format": "PHIR/JSON",
"version": "0.1.0",
"ops": [{"data": "qvar_define", "variable": "q", **fields}],
}
with pytest.raises(ValueError, match="Quantum register 'q' requires a positive integer size"):
PyPHIR.from_phir(program)
with pytest.raises(ValueError, match="size"):
PhirClassicalInterpreter().init(program)

Expand Down Expand Up @@ -74,3 +85,28 @@ def test_classical_type_remains_required() -> None:
}
with pytest.raises(ValueError, match="data_type"):
PhirClassicalInterpreter().init(program)


@pytest.mark.parametrize(
"metadata",
[{}, {"qvar_spec": {"q": 0}}, {"cvar_spec": {"c": 0}}, {"qvar_spec": {"q": 0}, "cvar_spec": {"c": 0}}],
)
def test_empty_circuit_round_trip(metadata: dict, monkeypatch: pytest.MonkeyPatch) -> None:
"""The exact emitted document is also consumed by all four Rust entry points."""
monkeypatch.setattr(pecos, "__version__", "fixture")
generated = to_phir_dict(pecos.QuantumCircuit(**metadata))
fixture = (
Path(__file__).resolve().parents[5] / "crates/pecos-phir-json/tests/fixtures/empty_quantum_circuit.phir.json"
)
assert generated == json.loads(fixture.read_text())
PhirModel.model_validate(generated)
assert PyPHIR.from_phir(generated).num_qubits == 0
assert PhirClassicalInterpreter().init(generated) == 0


def test_empty_registers_do_not_remove_nonempty_registers() -> None:
"""Omission is limited to empty registers, including explicit classical sizes."""
generated = to_phir_dict(pecos.QuantumCircuit(qvar_spec={"empty_q": 0, "q": 2}, cvar_spec={"empty_c": 0, "c": 2}))
assert [op["variable"] for op in generated["ops"]] == ["q", "c"]
PhirModel.model_validate(generated)
assert PhirClassicalInterpreter().init(generated) == 2
Loading