From 077db80ff8d65823fb15551b5b2b973244ac9dd6 Mon Sep 17 00:00:00 2001 From: Agustin Borgna Date: Thu, 6 Jul 2023 11:01:41 +0100 Subject: [PATCH 1/5] Update the json decoder to use resources --- Cargo.toml | 4 +- pyrs/Cargo.toml | 2 +- pyrs/src/lib.rs | 6 +- src/json.rs | 226 +++++++++++++++++- src/json/json_convert.rs | 504 --------------------------------------- src/json/op.rs | 228 ++++++++++++++++++ src/lib.rs | 5 +- src/resource.rs | 64 +++++ src/tests.rs | 9 +- src/utils.rs | 6 + 10 files changed, 536 insertions(+), 518 deletions(-) delete mode 100644 src/json/json_convert.rs create mode 100644 src/json/op.rs create mode 100644 src/resource.rs diff --git a/Cargo.toml b/Cargo.toml index 361aded0..2bd28ae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# symengine = { path = "./ext/symengine.rs", features = ["serde"] } lazy_static = "1.4.0" cgmath = "0.18.0" num-rational = "0.4" @@ -25,11 +24,12 @@ serde_json = "1.0" downcast-rs = "1.2.0" portgraph = "0.7.0" priority-queue = "1.3.0" -quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", tag = "v0.0.0-alpha.3" } +quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", branch = "feat/customSigFn-impl" } smol_str = "0.2.0" typetag = "0.2.8" itertools = "0.11.0" petgraph = { version = "0.6.3", default-features = false } +serde_yaml = "0.9.22" [features] pyo3 = ["dep:pyo3", "tket-json-rs/pyo3", "tket-json-rs/tket2ops", "portgraph/pyo3", "quantinuum-hugr/pyo3"] diff --git a/pyrs/Cargo.toml b/pyrs/Cargo.toml index 67a83b8b..349584be 100644 --- a/pyrs/Cargo.toml +++ b/pyrs/Cargo.toml @@ -15,4 +15,4 @@ portgraph = { version = "0.7.0", features = ["pyo3"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tket-json-rs = { git = "https://github.com/CQCL/tket-json-rs", features = ["pyo3"] } -quantinuum-hugr = { version = "0.1.0", git="https://github.com/CQCL-DEV/hugr", tag = "v0.0.0-alpha.3" } \ No newline at end of file +quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", branch = "feat/customSigFn-impl" } \ No newline at end of file diff --git a/pyrs/src/lib.rs b/pyrs/src/lib.rs index 23b1dd11..03df078e 100644 --- a/pyrs/src/lib.rs +++ b/pyrs/src/lib.rs @@ -1,18 +1,16 @@ use hugr::Hugr; -use portgraph::graph::{Direction, NodeIndex}; use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use tket2::json::json_convert::load_serial; +use tket2::json::TKET1Decode; use tket_json_rs::circuit_json::SerialCircuit; -use tket_json_rs::optype::OpType; create_exception!(pyrs, PyValidateError, PyException); #[pyfunction] fn check_soundness(c: Py) -> PyResult<()> { let ser_c = SerialCircuit::_from_tket1(c); - let hugr: Hugr = load_serial(ser_c); + let hugr: Hugr = ser_c.decode().unwrap(); println!("{}", hugr.dot_string()); hugr.validate() .map_err(|e| PyValidateError::new_err(e.to_string())) diff --git a/src/json.rs b/src/json.rs index a65418b8..70557c56 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,3 +1,227 @@ //! Json serialization and deserialization. -pub mod json_convert; +pub mod op; + +use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::mem; + +use hugr::builder::{AppendWire, CircuitBuilder, Container, DFGBuilder, Dataflow, DataflowHugr}; +use hugr::ops::{ConstValue, OpType}; +use hugr::types::Signature; +use hugr::{Hugr, Wire}; + +use serde_json::json; +use thiserror::Error; +use tket_json_rs::circuit_json; +use tket_json_rs::circuit_json::SerialCircuit; +use tket_json_rs::optype::OpType as JsonOpType; + +use self::op::JsonOp; +use crate::utils::{BIT, QB}; + +/// A JSON-serialized TKET1 circuit that can be converted to a [`Hugr`]. +pub trait TKET1Decode { + /// The error type for decoding. + type DecodeError; + /// Convert the serialized circuit to a [`Hugr`]. + fn decode(self) -> Result; +} + +impl TKET1Decode for SerialCircuit { + type DecodeError = OpConvertError; + fn decode(self) -> Result { + let mut decoder = JsonDecoder::new(&self); + + if !self.phase.is_empty() { + // TODO - add a phase gate + // let phase = Param::new(serialcirc.phase); + // decoder.add_phase(phase); + } + + // TODO: Check the implicit permutation in the serialized circuit. + + for com in self.commands { + decoder.add_command(com); + } + Ok(decoder.finish()) + } +} + +/// The state of an in-progress [`DFGBuilder`] being built from a [`SerialCircuit`]. +/// +/// Mostly used to define helper internal methods. +#[derive(Debug, PartialEq)] +struct JsonDecoder { + /// The Hugr being built. + pub hugr: DFGBuilder, + /// The dangling wires of the builder. + /// Used to generate [`CircuitBuilder`]s. + dangling_wires: Vec, + /// A map from the json registers to flat wire indices. + register_wire: HashMap, + /// The number of qubits in the circuit. + num_qubits: usize, + /// The number of bits in the circuit. + num_bits: usize, +} + +impl JsonDecoder { + /// Initialize a new [`JsonDecoder`], using the metadata from a [`SerialCircuit`]. + pub fn new(serialcirc: &SerialCircuit) -> Self { + let num_qubits = serialcirc.qubits.len(); + let num_bits = serialcirc.bits.len(); + // Map each (register name, index) pair to an offset in the signature. + let mut wire_map: HashMap = + HashMap::with_capacity(num_bits + num_qubits); + for (i, register) in serialcirc + .qubits + .iter() + .chain(serialcirc.bits.iter()) + .enumerate() + { + if register.1.len() != 1 { + // TODO: Support multi-index registers? + panic!("Register {} has more than one index", register.0); + } + wire_map.insert((register, 0).into(), i); + } + let sig = Signature::new_linear([vec![QB; num_qubits], vec![BIT; num_bits]].concat()); + + let mut dfg = DFGBuilder::new(sig.input, sig.output).unwrap(); + + dfg.set_metadata(json!({"name": serialcirc.name})); + + let dangling_wires = dfg.input_wires().collect::>(); + JsonDecoder { + hugr: dfg, + dangling_wires, + register_wire: wire_map, + num_qubits, + num_bits, + } + } + + /// Finish building the [`Hugr`]. + pub fn finish(self) -> Hugr { + // TODO: Throw validation error? + self.hugr + .finish_hugr_with_outputs(self.dangling_wires) + .unwrap() + } + + /// Add a [`Command`] from the serial circuit to the [`JsonDecoder`]. + /// + /// - [`Command`]: circuit_json::Command + pub fn add_command(&mut self, command: circuit_json::Command) { + let circuit_json::Command { op, args, .. } = command; + let params = op.params.clone().unwrap_or_default(); + let num_qubits = args + .iter() + .take_while(|&arg| self.reg_wire(arg, 0) < self.num_qubits) + .count(); + let num_bits = args.len() - num_qubits; + let op = JsonOp::new_from_op(op, num_qubits, num_bits); + + let args: Vec<_> = args.into_iter().map(|reg| self.reg_wire(®, 0)).collect(); + + let param_wires: Vec = params.iter().map(|p| self.get_param_wire(p)).collect(); + + let append_wires = args + .into_iter() + .map(AppendWire::I) + .chain(param_wires.into_iter().map(AppendWire::W)); + + self.with_circ_builder(|circ| { + circ.append_and_consume(&op, append_wires).unwrap(); + }); + } + + /// Apply a function to the internal hugr builder viewed as a [`CircuitBuilder`]. + fn with_circ_builder(&mut self, f: impl FnOnce(&mut CircuitBuilder>)) { + let mut circ = self.hugr.as_circuit(mem::take(&mut self.dangling_wires)); + f(&mut circ); + self.dangling_wires = circ.finish(); + } + + /// Returns the wire carrying a parameter. + /// + /// If the parameter is a constant, a constant definition is added to the Hugr. + /// + /// TODO: If the parameter is a variable, returns the corresponding wire from the input. + fn get_param_wire(&mut self, param: &str) -> Wire { + if let Ok(f) = param.parse::() { + self.hugr.add_load_const(ConstValue::F64(f)).unwrap() + } else if param.split('/').count() == 2 { + // TODO: Use the rational types from `Hugr::extensions::rotation` + let (n, d) = param.split_once('/').unwrap(); + let n = n.parse::().unwrap(); + let d = d.parse::().unwrap(); + self.hugr.add_load_const(ConstValue::F64(n / d)).unwrap() + } else { + // TODO: Pre-compute variables and add them to the input signature. + todo!("Variable parameters not yet supported") + } + } + + /// Return the wire index for the `elem`th value of a given register. + /// + /// Relies on TKET1 constraint that all registers have unique names. + fn reg_wire(&self, register: &circuit_json::Register, elem: usize) -> usize { + self.register_wire[&(register, elem).into()] + } +} + +/// The state of an in-progress [`SerialCircuit`] being built from a [`Circuit`]. +#[derive(Debug, PartialEq)] +struct JsonEncoder { + /// The Hugr being built. + pub serial: SerialCircuit, + /// The dangling wires of the builder. + /// Used to generate [`CircuitBuilder`]s. + dangling_wires: Vec, + /// A map from the json registers to flat wire indices. + register_wire: HashMap, + /// The number of qubits in the circuit. + num_qubits: usize, + /// The number of bits in the circuit. + num_bits: usize, +} + +//impl JsonEncoder { +// /// Create a new [`JsonEncoder`] from a [`Circuit`]. +// pub fn new(circ: &impl Circuit) -> Self { +// let num_qubits = circ.qubits().len(); +// let num_bits = circ.bits().len(); +// } +//} + +/// Error type for conversion between `Op` and `OpType`. +#[derive(Debug, Error)] +pub enum OpConvertError { + /// The serialized operation is not supported. + #[error("Unsupported serialized operation: {0:?}")] + UnsupportedSerializedOp(JsonOpType), + /// The serialized operation is not supported. + #[error("Cannot serialize operation: {0:?}")] + UnsupportedOpSerialization(OpType), +} + +/// A hashed register, used to identify registers in the [`JsonDecoder::register_wire`] map, +/// avoiding string clones on lookup. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct RegisterHash { + hash: u64, +} + +impl From<(&circuit_json::Register, usize)> for RegisterHash { + fn from((reg, elem): (&circuit_json::Register, usize)) -> Self { + let mut hasher = DefaultHasher::new(); + reg.0.hash(&mut hasher); + reg.1[elem].hash(&mut hasher); + Self { + hash: hasher.finish(), + } + } +} diff --git a/src/json/json_convert.rs b/src/json/json_convert.rs deleted file mode 100644 index adaff2a6..00000000 --- a/src/json/json_convert.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! Conversion definitions between Hugr operations and the serializable TKET -//! operations defined by `tket-json-rs`. - -use std::collections::HashMap; -use std::str::FromStr; - -use hugr::builder::{AppendWire, DFGBuilder, Dataflow, DataflowHugr}; -use hugr::ops::constant::ConstValue; -use hugr::ops::{self, CustomOp, LeafOp, OpaqueOp}; -use hugr::resource::ResourceSet; -use hugr::types::{ClassicType, LinearType, Signature, SimpleType}; -use hugr::{Hugr as Circuit, Wire}; -// use hugr::hugr: -// use crate::circuit::operation::{ConstValue, Op, WireType}; -use serde::{Deserialize, Serialize}; -use tket_json_rs::circuit_json::{Command, Operation, Register, SerialCircuit}; -use tket_json_rs::optype::OpType; - -const QB: SimpleType = SimpleType::Linear(LinearType::Qubit); -const BIT: SimpleType = SimpleType::Classic(ClassicType::Int(1)); -// fn to_qubit(reg: Register) -> UnitID { -// UnitID::Qubit { -// reg_name: reg.0, -// index: reg.1.into_iter().map(|i| i as u32).collect(), -// } -// } - -// fn to_bit(reg: Register) -> UnitID { -// UnitID::Bit { -// name: reg.0, -// index: reg.1.into_iter().map(|i| i as u32).collect(), -// } -// } -struct HugrOp(ops::OpType); -impl TryFrom for HugrOp { - type Error = OpConvertError; - fn try_from(serial_op: OpType) -> Result { - Ok(HugrOp(match serial_op { - // OpType::Input => ops::Input{}, - // OpType::Output => ops::Output, - // OpType::Create => todo!(), - // OpType::Discard => todo!(), - // OpType::ClInput => todo!(), - // OpType::ClOutput => todo!(), - // OpType::Barrier => todo!(), - // OpType::Label => todo!(), - // OpType::Branch => todo!(), - // OpType::Goto => todo!(), - // OpType::Stop => todo!(), - // OpType::ClassicalTransform => todo!(), - // OpType::SetBits => todo!(), - // OpType::CopyBits => todo!(), - // OpType::RangePredicate => todo!(), - // OpType::ExplicitPredicate => todo!(), - // OpType::ExplicitModifier => todo!(), - // OpType::MultiBit => todo!(), - // OpType::Z => todo!(), - OpType::X => LeafOp::X.into(), - // OpType::Y => todo!(), - // OpType::S => todo!(), - // OpType::Sdg => todo!(), - // OpType::T => todo!(), - // OpType::Tdg => todo!(), - // OpType::V => todo!(), - // OpType::Vdg => todo!(), - // OpType::SX => todo!(), - // OpType::SXdg => todo!(), - OpType::H => LeafOp::H.into(), - OpType::CX => LeafOp::CX.into(), - // OpType::CY => todo!(), - // OpType::CZ => todo!(), - // OpType::CH => todo!(), - // OpType::CV => todo!(), - // OpType::CVdg => todo!(), - // OpType::CSX => todo!(), - // OpType::CSXdg => todo!(), - // OpType::CRz => todo!(), - // OpType::CRx => todo!(), - // OpType::CRy => todo!(), - // OpType::CU1 => todo!(), - // OpType::CU3 => todo!(), - // OpType::PhaseGadget => todo!(), - // OpType::CCX => todo!(), - // OpType::SWAP => todo!(), - // OpType::CSWAP => todo!(), - // OpType::BRIDGE => todo!(), - OpType::noop => LeafOp::Noop { ty: QB }.into(), - // TODO TKET1 measure takes a bit as input, HUGR measure does not - // OpType::Measure => LeafOp::Measure.into(), - // OpType::Collapse => todo!(), - OpType::Reset => LeafOp::Reset.into(), - // OpType::ECR => todo!(), - // OpType::ISWAP => todo!(), - // OpType::PhasedX => todo!(), - // OpType::NPhasedX => todo!(), - OpType::ZZMax => LeafOp::ZZMax.into(), - // OpType::XXPhase => todo!(), - // OpType::YYPhase => todo!(), - // OpType::ZZPhase => todo!(), - // OpType::XXPhase3 => todo!(), - // OpType::ESWAP => todo!(), - // OpType::FSim => todo!(), - // OpType::Sycamore => todo!(), - // OpType::ISWAPMax => todo!(), - // OpType::PhasedISWAP => todo!(), - // OpType::CnRy => todo!(),R - // OpType::CnX => todo!(), - // OpType::CircBox => todo!(), - // OpType::Unitary1qBox => todo!(), - // OpType::Unitary2qBox => todo!(), - // OpType::Unitary3qBox => todo!(), - // OpType::ExpBox => todo!(), - // OpType::PauliExpBox => todo!(), - // OpType::CliffBox => todo!(), - // OpType::CustomGate => todo!(), - // OpType::PhasePolyBox => todo!(), - // OpType::QControlBox => todo!(), - // OpType::ClassicalExpBox => todo!(), - // OpType::Conditional => todo!(), - // OpType::ProjectorAssertionBox => todo!(), - // OpType::StabiliserAssertionBox => todo!(), - // OpType::UnitaryTableauBox => todo!(), - // OpType::Rx => Op::RxF64, - // OpType::Ry => todo!(), - OpType::Rz => LeafOp::RzF64.into(), - // OpType::TK1 => Op::TK1, - // OpType::AngleAdd => Op::AngleAdd, - // OpType::AngleMul => Op::AngleMul, - // OpType::AngleNeg => Op::AngleNeg, - // OpType::QuatMul => Op::QuatMul, - // OpType::RxF64 => LeafOp::RxF64.into(), - OpType::RzF64 => LeafOp::RzF64.into(), - // OpType::Rotation => Op::Rotation, - // OpType::ToRotation => Op::ToRotation, - _ => return Err(OpConvertError), - })) - } - - // } -} - -/// Error type for conversion between `Op` and `OpType`. -#[derive(Debug)] -pub struct OpConvertError; - -// impl TryFrom<&Op> for OpType { -// fn try_from(op: &Op) -> Result { -// // let (op_type, params) = match op { -// // Op::H => (OpType::H, vec![]), -// // Op::CX => (OpType::CX, vec![]), -// // Op::ZZMax => (OpType::ZZMax, vec![]), -// // Op::Reset => (OpType::Reset, vec![]), -// // Op::Input => (OpType::Input, vec![]), -// // Op::Output => (OpType::Output, vec![]), -// // Op::Rx(p) => (OpType::Rx, vec![p]), -// // Op::Ry(p) => (OpType::Ry, vec![p]), -// // Op::Rz(p) => (OpType::Rz, vec![p]), -// // Op::TK1(a, b, c) => (OpType::TK1, vec![a, b, c]), -// // Op::ZZPhase(p) => (OpType::ZZPhase, vec![p]), -// // Op::PhasedX(p1, p2) => (OpType::PhasedX, vec![p1, p2]), -// // Op::Measure => (OpType::Measure, vec![]), -// // Op::Barrier => (OpType::Barrier, vec![]), -// // Op::Noop(WireType::Qubit) => (OpType::noop, vec![]), -// // _ => panic!("Not supported by Serialized TKET-1: {:?}", op), -// // }; - -// // let signature = match self.signature() { -// // Signature::Linear(sig) => sig.iter().map(|wt| match wt { -// // WireType::Quantum => todo!(), -// // WireType::Classical => todo!(), -// // WireType::Bool => todo!(), -// // }), -// // Signature::NonLinear(_, _) => panic!(), -// // } - -// // let params = (!params.is_empty()) -// // .then(|| params.into_iter().map(|p| p.to_string().into()).collect()); -// // Operation { -// // op_type, -// // // params: params.map(|ps| ps.iter().map(|e| e.as_str().to_string()).collect()), -// // params, -// // signature: None, -// // op_box: None, -// // n_qb: None, -// // conditional: None, -// // } - -// Ok(match op { -// Op::H => OpType::H, -// Op::CX => OpType::CX, -// Op::ZZMax => OpType::ZZMax, -// Op::Reset => OpType::Reset, -// Op::Input => OpType::Input, -// Op::Output => OpType::Output, -// Op::Measure => OpType::Measure, -// Op::Barrier => OpType::Barrier, -// Op::RxF64 => OpType::Rx, -// Op::RzF64 => OpType::Rz, -// Op::TK1 => OpType::TK1, -// Op::Noop(WireType::Qubit) => OpType::noop, -// Op::AngleAdd => OpType::AngleAdd, -// Op::AngleMul => OpType::AngleMul, -// Op::AngleNeg => OpType::AngleNeg, -// Op::QuatMul => OpType::QuatMul, -// Op::Rotation => OpType::Rotation, -// Op::ToRotation => OpType::ToRotation, -// Op::Copy { .. } => OpType::Copy, -// Op::Const(_) => OpType::Const, -// Op::Custom(cbox) => { -// if let Some(tk1op) = cbox.downcast_ref::() { -// tk1op.op_type.clone() -// } else { -// return Err(OpConvertError); -// } -// } -// _ => return Err(OpConvertError), -// }) -// } - -// type Error = OpConvertError; -// } - -// impl + Clone + std::fmt::Display> From> for Circuit { - -/// Load a serializable circuit into a `Circuit`. -pub fn load_serial(serialcirc: SerialCircuit) -> Circuit { - let n_qbs = serialcirc.qubits.len(); - let n_bits = serialcirc.bits.len(); - let wire_map: HashMap<(String, i64), usize> = serialcirc - .qubits - .into_iter() - .chain(serialcirc.bits.into_iter()) - .enumerate() - .map(|(i, x)| ((x.0, x.1[0]), i)) - .collect(); - let sig = Signature::new_linear([vec![QB; n_qbs], vec![BIT; n_bits]].concat()); - - // let uids: Vec<_> = serialcirc - // .qubits - // .into_iter() - // .map(to_qubit) - // .chain(serialcirc.bits.into_iter().map(to_bit)) - // .collect(); - - // let mut circ = Circuit::with_uids(uids); - - let mut dfg = DFGBuilder::new(sig.input, sig.output).unwrap(); - - // circ.name = serialcirc.name; - // circ.phase = Param::new(serialcirc.phase); - - // TODO use phase gates instead - // circ.phase = f64::from_str(&serialcirc.phase.clone().into()[..]).unwrap(); - - // let frontier: HashMap = circ - // .unitids() - // .enumerate() - // .map(|(i, uid)| (uid.clone(), i)) - // .collect(); - let param_wires: HashMap = serialcirc - .commands - .iter() - .flat_map(|com| com.op.params.clone().unwrap_or_default().into_iter()) - .map(|p| { - if let Ok(f) = f64::from_str(&p[..]) { - (p, dfg.add_load_const(ConstValue::F64(f)).unwrap()) - } else if p.split('/').count() == 2 { - let (n, d) = p.split_once('/').unwrap(); - let n = f64::from_str(n).unwrap(); - let d = f64::from_str(d).unwrap(); - (p, dfg.add_load_const(ConstValue::F64(n / d)).unwrap()) - } else { - // need to be able to add floating point inputs to the - // signature ahead of time - todo!() - // circ.add_unitid(UnitID::Angle(p_str)) - } - }) - .collect(); - - let wires = dfg.input_wires().collect(); - let mut circ = dfg.as_circuit(wires); - for com in serialcirc.commands { - let Command { op, args, .. } = com; - let params = op.params.clone(); - let HugrOp(op) = op.op_type.clone().try_into().unwrap_or(HugrOp( - LeafOp::CustomOp { - custom: map_op(op, args.clone()), - } - .into(), - )); - let args: Vec<_> = args - .into_iter() - .map(|reg| { - // relies on TKET1 constraint that all registers have - // unique names - *wire_map.get(&(reg.0, reg.1[0])).unwrap() - }) - .collect(); - - let param_wires: Vec = params - .unwrap_or_default() - .iter() - .map(|p| *param_wires.get(p).unwrap()) - .collect(); - - // if let Some(params) = ps { - // let mut prev = circ.dag.node_edges(v, Direction::Incoming).last(); - // for p in params.into_iter() { - // let p_str = p.to_string(); - // let param_source = if let Ok(f) = f64::from_str(&p_str[..]) { - // circ.append_with_outputs(ops::Const(ConstValue::), inputs) - // let e = circ.add_edge(WireType::Angle); - // circ.add_vertex_with_edges( - // Op::Const(ConstValue::f64_angle(f)), - // vec![], - // vec![e], - // ); - // e - // } else { - // // need to be able to add floating point inputs to the - // // signature ahead of time - // todo!() - // // circ.add_unitid(UnitID::Angle(p_str)) - // }; - // circ.dag - // .connect(v, param_source, Direction::Incoming, prev) - // .unwrap(); - // prev = Some(param_source); - // // circ.tup_add_edge( - // // param_source, - // // (v, (args.len() + i) as u8).into(), - // // WireType::Angle, - // // ); - // } - // }; - let append_wires = args - .into_iter() - .map(AppendWire::I) - .chain(param_wires.into_iter().map(AppendWire::W)); - circ.append_and_consume(op, append_wires).unwrap(); - } - // TODO implicit perm - - let wires = circ.finish(); - dfg.finish_hugr_with_outputs(wires).unwrap() -} -// } - -// impl From for Register { -// fn from(uid: UnitID) -> Self { -// match uid { -// UnitID::Qubit { -// reg_name: name, -// index, -// } -// | UnitID::Bit { name, index } => { -// Register(name, index.into_iter().map(i64::from).collect()) -// } -// _ => panic!("Not supported: {uid:?}"), -// } -// } -// } - -// impl + std::fmt::Debug + Clone + Send + Sync> From for SerialCircuit

{ -// fn from(circ: Circuit) -> Self { -// let commands = circ -// .to_commands() -// .filter_map(|com| { -// let params = match com.op { -// Op::Input | Op::Output | Op::Const(_) => return None, -// Op::RzF64 | Op::RxF64 => param_strings(&circ, &com, 1), -// Op::TK1 => param_strings(&circ, &com, 3), -// _ => None, -// }; -// let tk1op = if let Op::Custom(cbox) = com.op { -// cbox.downcast_ref::().cloned() -// } else { -// None -// }; - -// Some(Command { -// op: map_op(tk1op.unwrap_or_else(|| Operation { -// op_type: com.op.try_into().unwrap(), -// n_qb: None, -// params, -// op_box: None, -// signature: None, -// conditional: None, -// })), -// args: com.args.into_iter().map(Into::into).collect(), -// opgroup: None, -// }) -// }) -// .collect(); - -// let qubits: Vec = circ.qubits().map(Into::into).collect(); -// let implicit_permutation = qubits -// .iter() -// .map(|q| Permutation(q.clone(), q.clone())) -// .collect(); -// let bits = circ.bits().map(Into::into).collect(); - -// SerialCircuit { -// commands, -// name: circ.name, -// phase: circ.phase.to_string().into(), -// qubits, -// bits, -// implicit_permutation, -// } -// } -// } -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TK1Op { - serialized_op: Operation, - resources: ResourceSet, -} - -#[typetag::serde] -impl CustomOp for TK1Op { - fn name(&self) -> smol_str::SmolStr { - format!("{:?}", self.serialized_op.op_type).into() - } - - fn signature(&self) -> Signature { - // TODO: Is there a way to make this static? The Opaque simple type requires initializing a Box... - // dbg!(&self.serialized_op); - Signature::new_linear( - self.serialized_op - .signature - .as_ref() - .expect("custom op needs a signature") - .iter() - .map(|s| match &s[..] { - "Q" => QB, - "B" => BIT, - _ => panic!("unknown type."), - }) - .collect::>(), - ) - } - - fn resources(&self) -> &ResourceSet { - &self.resources - } -} -fn map_op(mut op: Operation, args: Vec) -> OpaqueOp { - // TODO try and infer signature from arguments, right now just assumes all qubit - let sig: Vec = vec!["Q".into(); args.len()]; - op.signature = Some(sig); - let op = TK1Op { - serialized_op: op, - resources: ResourceSet::new(), - }; - let id = op.name(); - OpaqueOp::new(id, op) - // Operation { - // op_type: op.op_type, - // n_qb: op.n_qb, - // params: op - // .params - // .map(|params| params.into_iter().map(|p| p.into()).collect()), - // op_box: op.op_box, - // signature: op.signature, - // conditional: op.conditional, - // } -} - -// fn param_strings( -// circ: &Circuit, -// com: &crate::circuit::circuit::Command, -// num: u32, -// ) -> Option> { -// Some( -// (0..num) -// .map(|i| { -// let angle_edge = circ -// .edge_at_port(com.vertex, 1 + i as usize, Direction::Incoming) -// .expect("Expected an angle wire."); -// let pred_n = circ -// .dag -// .edge_endpoint(angle_edge, Direction::Outgoing) -// .unwrap(); -// let pred = &circ -// .dag -// .node_weight(pred_n) -// .expect("Expected predecessor node.") -// .op; -// match pred { -// Op::Const(ConstValue::Angle(p)) => p.to_f64().to_string(), -// Op::Input => match &circ.uids[circ -// .port_of_edge(pred_n, angle_edge, Direction::Outgoing) -// .unwrap()] -// { -// UnitID::Angle(s) => s.clone(), -// _ => panic!("Must be an Angle input"), -// }, -// _ => panic!("Only constant or simple string param inputs supported."), -// } -// }) -// .collect(), -// ) -// } diff --git a/src/json/op.rs b/src/json/op.rs new file mode 100644 index 00000000..eea8719c --- /dev/null +++ b/src/json/op.rs @@ -0,0 +1,228 @@ +//! Conversion definitions between Hugr operations and the serializable TKET +//! operations defined by `tket-json-rs`. + +use hugr::ops::custom::ExternalOp; +use hugr::ops::{LeafOp, OpTrait, OpType}; +use hugr::types::Signature; + +use itertools::Itertools; +use tket_json_rs::circuit_json; +use tket_json_rs::optype::OpType as JsonOpType; + +use super::OpConvertError; +use crate::utils::{BIT, F64, QB}; + +/// A serialized operation, containing the operation type and all its attributes. +/// +/// Wrapper around [`circuit_json::Operation`] with cached number of qubits and bits. +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct JsonOp { + op: circuit_json::Operation, + num_qubits: usize, + num_bits: usize, + /// Number of input parameters + num_params: usize, +} + +impl JsonOp { + /// Create a new `JsonOp` from a `circuit_json::Operation`, computing its + /// number of qubits from the signature + /// + /// Fails if the operation does not define a signature. See + /// [`JsonOp::new_from_op`] for a version that generates a signature if none + /// is defined. + #[allow(unused)] + pub fn new(op: circuit_json::Operation) -> Option { + let Some(sig) = &op.signature else { return None; }; + let input_counts = sig.iter().map(String::as_ref).counts(); + let num_qubits = input_counts.get("Q").copied().unwrap_or(0); + let num_bits = input_counts.get("B").copied().unwrap_or(0); + let num_params = op.params.as_ref().map(Vec::len).unwrap_or_default(); + Some(Self { + op, + num_qubits, + num_bits, + num_params, + }) + } + + /// Create a new `JsonOp` from a `circuit_json::Operation`, with the number + /// of qubits and bits explicitly specified. + /// + /// If the operation does not define a signature, one is generated with the + /// given amounts. + pub fn new_from_op( + mut op: circuit_json::Operation, + num_qubits: usize, + num_bits: usize, + ) -> Self { + if op.signature.is_none() { + op.signature = + Some([vec!["Q".into(); num_qubits], vec!["B".into(); num_bits]].concat()); + } + let num_params = op.params.as_ref().map_or(0, Vec::len); + Self { + op, + num_qubits, + num_bits, + num_params, + } + } + + /// Create a new `JsonOp` from the optype and the number of parameters. + pub fn new_with_counts( + json_optype: JsonOpType, + num_qubits: usize, + num_bits: usize, + num_params: usize, + ) -> Self { + let op = circuit_json::Operation { + op_type: json_optype, + n_qb: Some(num_qubits as u32), + params: None, + op_box: None, + signature: Some([vec!["Q".into(); num_qubits], vec!["B".into(); num_qubits]].concat()), + conditional: None, + }; + Self { + op, + num_qubits, + num_bits, + num_params, + } + } + + /// Compute the signature of the operation. + #[inline] + #[allow(unused)] + pub fn signature(&self) -> Signature { + let linear = [vec![QB; self.num_qubits], vec![BIT; self.num_bits]].concat(); + let params = vec![F64; self.num_params]; + Signature::new_df([linear.clone(), params].concat(), linear) + } + + /// Wraps the op into a Hugr opaque operation + fn as_opaque_op(&self) -> ExternalOp { + crate::resource::wrap_json_op(self) + } +} + +impl From<&JsonOp> for OpType { + /// Convert the operation into a HUGR operation. + /// + /// We only translate operations that have a 1:1 mapping between TKET and HUGR. + /// Any other operation is wrapped in an `OpaqueOp`. + fn from(json_op: &JsonOp) -> Self { + match json_op.op.op_type { + JsonOpType::X => LeafOp::X.into(), + JsonOpType::H => LeafOp::H.into(), + JsonOpType::CX => LeafOp::CX.into(), + JsonOpType::noop => LeafOp::Noop { ty: QB }.into(), + // TODO TKET1 measure takes a bit as input, HUGR measure does not + //JsonOpType::Measure => LeafOp::Measure.into(), + JsonOpType::Reset => LeafOp::Reset.into(), + JsonOpType::ZZMax => LeafOp::ZZMax.into(), + JsonOpType::Rz => LeafOp::RzF64.into(), + JsonOpType::RzF64 => LeafOp::RzF64.into(), + // TODO TKET1 I/O needs some special handling + //JsonOpType::Input => hugr::ops::Input { + // types: json_op.signature().output, + // resources: Default::default(), + //} + //.into(), + //JsonOpType::Output => hugr::ops::Output { + // types: json_op.signature().input, + // resources: Default::default(), + //} + //.into(), + JsonOpType::Z => LeafOp::Z.into(), + JsonOpType::Y => LeafOp::Y.into(), + JsonOpType::S => LeafOp::S.into(), + JsonOpType::Sdg => LeafOp::Sadj.into(), + JsonOpType::T => LeafOp::T.into(), + JsonOpType::Tdg => LeafOp::Tadj.into(), + _ => LeafOp::CustomOp(json_op.as_opaque_op()).into(), + } + } +} + +impl TryFrom<&OpType> for JsonOp { + type Error = OpConvertError; + + fn try_from(op: &OpType) -> Result { + // We only translate operations that have a 1:1 mapping between TKET and HUGR + // + // Other TKET1 operations are wrapped in an `OpaqueOp`. + // + // Non-supported Hugr operations throw an error. + let err = || OpConvertError::UnsupportedOpSerialization(op.clone()); + + if let OpType::LeafOp(LeafOp::CustomOp(_ext)) = op { + todo!("Try to extract the tket1 op from ext"); + //let Some(tk1op) = custom.downcast_ref::() else { + // return Err(err()); + //}; + //let json_op = tk1op.serialized_op.clone(); + //return JsonOp::new(json_op).ok_or(err()); + } + + let json_optype: JsonOpType = match op { + OpType::LeafOp(leaf) => match leaf { + LeafOp::H => JsonOpType::H, + LeafOp::CX => JsonOpType::CX, + LeafOp::ZZMax => JsonOpType::ZZMax, + LeafOp::Reset => JsonOpType::Reset, + LeafOp::Measure => JsonOpType::Measure, + LeafOp::T => JsonOpType::T, + LeafOp::S => JsonOpType::S, + LeafOp::X => JsonOpType::X, + LeafOp::Y => JsonOpType::Y, + LeafOp::Z => JsonOpType::Z, + LeafOp::Tadj => JsonOpType::Tdg, + LeafOp::Sadj => JsonOpType::Sdg, + LeafOp::Noop { .. } => JsonOpType::noop, + //LeafOp::RzF64 => JsonOpType::Rz, // The angle in RzF64 comes from a constant input + //LeafOp::Xor => todo!(), + //LeafOp::MakeTuple { .. } => todo!(), + //LeafOp::UnpackTuple { .. } => todo!(), + //LeafOp::Tag { .. } => todo!(), + //LeafOp::Lift { .. } => todo!(), + // CustomOp is handled above + _ => return Err(err()), + }, + OpType::Input(_) => JsonOpType::Input, + OpType::Output(_) => JsonOpType::Output, + //hugr::ops::OpType::FuncDefn(_) => todo!(), + //hugr::ops::OpType::FuncDecl(_) => todo!(), + //hugr::ops::OpType::Const(_) => todo!(), + //hugr::ops::OpType::Call(_) => todo!(), + //hugr::ops::OpType::CallIndirect(_) => todo!(), + //hugr::ops::OpType::LoadConstant(_) => todo!(), + //hugr::ops::OpType::DFG(_) => JsonOpType::CircBox, // TODO: Requires generating the Operation::op_box + _ => return Err(err()), + }; + + let (num_qubits, num_bits, params) = + op.signature() + .input + .iter() + .fold((0, 0, 0), |(qs, bs, params), x| { + if *x == QB { + (qs + 1, bs, params) + } else if *x == BIT { + (qs, bs + 1, params) + } else if *x == F64 { + (qs, bs, params + 1) + } else { + (qs, bs, params) + } + }); + + Ok(JsonOp::new_with_counts( + json_optype, + num_qubits, + num_bits, + params, + )) + } +} diff --git a/src/lib.rs b/src/lib.rs index f84c00b3..b21fe7db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,10 +8,11 @@ //! compilation for many different quantum architectures. pub mod circuit; -//pub mod json; +pub mod json; pub mod passes; +pub mod resource; -//mod utils; +mod utils; //#[cfg(test)] //mod tests; diff --git a/src/resource.rs b/src/resource.rs new file mode 100644 index 00000000..67572ef2 --- /dev/null +++ b/src/resource.rs @@ -0,0 +1,64 @@ +//! This module defines the Hugr resources used to represent circuits. +//! +//! This includes a resource for the opaque TKET1 operations. + +use std::collections::HashMap; + +use hugr::ops::custom::{ExternalOp, OpaqueOp}; +use hugr::resource::{OpDef, ResourceId, ResourceSet, SignatureError}; +use hugr::types::type_param::{TypeArg, TypeParam}; +use hugr::types::TypeRow; +use hugr::Resource; +use lazy_static::lazy_static; +use smol_str::SmolStr; + +use super::json::op::JsonOp; + +/// The ID of the TKET1 resource. +pub const TKET1_RESOURCE_ID: ResourceId = SmolStr::new_inline("TKET1"); + +/// The name for opaque TKET1 operations. +pub const JSON_OP_NAME: SmolStr = SmolStr::new_inline("TKET1 Json Op"); + +lazy_static! { + /// The TKET1 resource, containing the opaque TKET1 operations. + pub static ref TKET1_RESOURCE: Resource = { + let json_op = OpDef::new_with_custom_sig( + JSON_OP_NAME, + "An opaque TKET1 operation.".into(), + vec![TypeParam::Value], + HashMap::new(), + json_op_signature, + ); + + let mut res = Resource::new(TKET1_RESOURCE_ID); + res.add_op(json_op).unwrap(); + res + }; +} + +/// Create a new opaque operation +pub(crate) fn wrap_json_op(op: &JsonOp) -> ExternalOp { + let sig = op.signature(); + let op = serde_yaml::to_value(op).unwrap(); + OpaqueOp::new( + TKET1_RESOURCE_ID, + JSON_OP_NAME, + "".into(), + vec![TypeArg::Value(op)], + Some(sig), + ) + .into() +} + +/// Compute the signature of a json-encoded TKET1 operation. +fn json_op_signature(args: &[TypeArg]) -> Result<(TypeRow, TypeRow, ResourceSet), SignatureError> { + let [TypeArg::Value(arg)] = args else { + panic!("Wrong number of arguments"); + // TODO: Add more Signature Errors + //return Err(SignatureError::WrongNumArgs(1, args.len())); + }; + let op: JsonOp = serde_yaml::from_value(arg.clone()).unwrap(); // TODO Errors! + let sig = op.signature(); + Ok((sig.input, sig.output, sig.output_resources)) +} diff --git a/src/tests.rs b/src/tests.rs index e048fc96..244c1690 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,6 +4,7 @@ use hugr::hugr::region::FlatRegionView; use hugr::{Hugr, HugrView}; use tket_json_rs::circuit_json; +use crate::circuit::Circuit; use crate::json::json_convert::TKET1Decode; #[test] @@ -14,9 +15,9 @@ fn read_json() { assert_eq!(ser.commands.len(), 4); let hugr: Hugr = ser.decode().unwrap(); - let _circ = FlatRegionView::new(&hugr, hugr.root()); + let circ = FlatRegionView::new(&hugr, hugr.root()); - //assert_eq!(circ.qubits().len(), 2); + assert_eq!(circ.qubits().len(), 2); //check_soundness(&circ).unwrap(); @@ -37,9 +38,9 @@ fn read_json_unknown_op() { assert_eq!(ser.commands.len(), 1); let hugr: Hugr = ser.decode().unwrap(); - let _circ = FlatRegionView::new(&hugr, hugr.root()); + let circ = FlatRegionView::new(&hugr, hugr.root()); - //assert_eq!(circ.qubits().len(), 3); + assert_eq!(circ.qubits().len(), 3); //let mut coms = circ.to_commands(); //coms.next(); // skip input diff --git a/src/utils.rs b/src/utils.rs index f99bb147..ac509404 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,11 @@ //! Utility functions for the library. +use hugr::types::{ClassicType, LinearType, SimpleType}; + +pub(crate) const QB: SimpleType = SimpleType::Linear(LinearType::Qubit); +pub(crate) const BIT: SimpleType = SimpleType::Classic(ClassicType::Int(1)); +pub(crate) const F64: SimpleType = SimpleType::Classic(ClassicType::F64); + #[allow(dead_code)] // Test only utils #[cfg(test)] From 23275408020cfe68855bbb1205ca03d4d13eca34 Mon Sep 17 00:00:00 2001 From: Agustin Borgna Date: Wed, 12 Jul 2023 14:19:26 +0100 Subject: [PATCH 2/5] Re-enable json tests --- src/json.rs | 3 +++ src/{ => json}/tests.rs | 15 ++------------- src/lib.rs | 3 --- 3 files changed, 5 insertions(+), 16 deletions(-) rename src/{ => json}/tests.rs (78%) diff --git a/src/json.rs b/src/json.rs index 70557c56..9d3211db 100644 --- a/src/json.rs +++ b/src/json.rs @@ -2,6 +2,9 @@ pub mod op; +#[cfg(test)] +mod tests; + use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::hash::{Hash, Hasher}; diff --git a/src/tests.rs b/src/json/tests.rs similarity index 78% rename from src/tests.rs rename to src/json/tests.rs index 244c1690..17ae445c 100644 --- a/src/tests.rs +++ b/src/json/tests.rs @@ -1,15 +1,14 @@ //! General tests. -use hugr::hugr::region::FlatRegionView; +use hugr::hugr::region::{FlatRegionView, Region}; use hugr::{Hugr, HugrView}; use tket_json_rs::circuit_json; use crate::circuit::Circuit; -use crate::json::json_convert::TKET1Decode; +use crate::json::TKET1Decode; #[test] fn read_json() { - // let expr = symengine::Expression::new("a + b + 3"); let circ_s = r#"{"bits": [["c", [0]], ["c", [1]]], "commands": [{"args": [["q", [0]]], "op": {"type": "H"}}, {"args": [["q", [0]], ["q", [1]]], "op": {"type": "CX"}}, {"args": [["q", [0]], ["c", [0]]], "op": {"type": "Measure"}}, {"args": [["q", [1]], ["c", [1]]], "op": {"type": "Measure"}}], "implicit_permutation": [[["q", [0]], ["q", [0]]], [["q", [1]], ["q", [1]]]], "phase": "0", "qubits": [["q", [0]], ["q", [1]]]}"#; let ser: circuit_json::SerialCircuit = serde_json::from_str(circ_s).unwrap(); assert_eq!(ser.commands.len(), 4); @@ -19,13 +18,8 @@ fn read_json() { assert_eq!(circ.qubits().len(), 2); - //check_soundness(&circ).unwrap(); - //let _reser: SerialCircuit = circ.into(); //assert_eq!(&ser, &_reser); - - // ser and reser cannot be compared because they will be different up to - // topsort ordering of parallel commands } #[test] @@ -42,11 +36,6 @@ fn read_json_unknown_op() { assert_eq!(circ.qubits().len(), 3); - //let mut coms = circ.to_commands(); - //coms.next(); // skip input - //let com = coms.next().unwrap(); - //assert!(matches!(com.op, &Op::Custom(_))); - //let _reser: SerialCircuit = circ.into(); //assert_eq!(&ser, &_reser); } diff --git a/src/lib.rs b/src/lib.rs index b21fe7db..7f1dd436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,3 @@ pub mod passes; pub mod resource; mod utils; - -//#[cfg(test)] -//mod tests; From 0c68ea96e81d2dfbd970e152c75ab1c788eff81e Mon Sep 17 00:00:00 2001 From: Agustin Borgna Date: Wed, 12 Jul 2023 14:21:49 +0100 Subject: [PATCH 3/5] Update hugr commit tag --- Cargo.toml | 2 +- pyrs/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2bd28ae1..1cd21f57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ serde_json = "1.0" downcast-rs = "1.2.0" portgraph = "0.7.0" priority-queue = "1.3.0" -quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", branch = "feat/customSigFn-impl" } +quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", tag = "v0.0.0-alpha.4" } smol_str = "0.2.0" typetag = "0.2.8" itertools = "0.11.0" diff --git a/pyrs/Cargo.toml b/pyrs/Cargo.toml index 349584be..14a199c2 100644 --- a/pyrs/Cargo.toml +++ b/pyrs/Cargo.toml @@ -15,4 +15,4 @@ portgraph = { version = "0.7.0", features = ["pyo3"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tket-json-rs = { git = "https://github.com/CQCL/tket-json-rs", features = ["pyo3"] } -quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", branch = "feat/customSigFn-impl" } \ No newline at end of file +quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", tag = "v0.0.0-alpha.4" } \ No newline at end of file From 69ef7da41bb03d290e637927e80d0afabd4aa8d9 Mon Sep 17 00:00:00 2001 From: Agustin Borgna Date: Wed, 12 Jul 2023 14:31:33 +0100 Subject: [PATCH 4/5] Expand doc --- src/json/op.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/json/op.rs b/src/json/op.rs index eea8719c..652e7b2b 100644 --- a/src/json/op.rs +++ b/src/json/op.rs @@ -1,5 +1,10 @@ -//! Conversion definitions between Hugr operations and the serializable TKET -//! operations defined by `tket-json-rs`. +//! This module defines the internal `JsonOp` struct wrapping the logic for +//! going between `tket_json_rs::optype::OpType` and `hugr::ops::OpType`. +//! +//! The `JsonOp` tries to homogenize the +//! `tket_json_rs::circuit_json::Operation`s coming from the encoded TKET1 +//! circuits by ensuring they always define a signature, and computing the +//! explicit count of qubits and linear bits. use hugr::ops::custom::ExternalOp; use hugr::ops::{LeafOp, OpTrait, OpType}; @@ -15,6 +20,8 @@ use crate::utils::{BIT, F64, QB}; /// A serialized operation, containing the operation type and all its attributes. /// /// Wrapper around [`circuit_json::Operation`] with cached number of qubits and bits. +/// +/// The `Operation` contained by this struct is guaranteed to have a signature. #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub(crate) struct JsonOp { op: circuit_json::Operation, From f47b3e2fe7cde45e1b4c8b7c5912b52c3c57a82b Mon Sep 17 00:00:00 2001 From: Agustin Borgna Date: Wed, 12 Jul 2023 15:46:53 +0100 Subject: [PATCH 5/5] Move the decoder to a submodule --- src/circuit.rs | 4 +- src/json.rs | 187 ++------------------------------------------ src/json/decoder.rs | 160 +++++++++++++++++++++++++++++++++++++ src/json/op.rs | 3 + 4 files changed, 173 insertions(+), 181 deletions(-) create mode 100644 src/json/decoder.rs diff --git a/src/circuit.rs b/src/circuit.rs index 62fe0760..74737860 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -11,6 +11,8 @@ pub mod command; // TODO: Move TKET1's custom op definition to tket-rs (or hugr?) //mod tk1ops; +use crate::utils::QB; + use self::command::{Command, CommandIterator, Unit}; use hugr::ops::OpTrait; @@ -46,7 +48,7 @@ pub trait Circuit<'circ> { fn qubits(&self) -> Vec { self.units() .iter() - .filter(|(_, typ)| typ == &LinearType::Qubit.into()) + .filter(|(_, typ)| typ == &QB) .map(|(unit, _)| *unit) .collect() } diff --git a/src/json.rs b/src/json.rs index 9d3211db..fedc3f19 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,31 +1,23 @@ //! Json serialization and deserialization. +mod decoder; +//mod encoder; pub mod op; #[cfg(test)] mod tests; -use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; -use std::mem; +use hugr::ops::OpType; +use hugr::Hugr; -use hugr::builder::{AppendWire, CircuitBuilder, Container, DFGBuilder, Dataflow, DataflowHugr}; -use hugr::ops::{ConstValue, OpType}; -use hugr::types::Signature; -use hugr::{Hugr, Wire}; - -use serde_json::json; use thiserror::Error; -use tket_json_rs::circuit_json; use tket_json_rs::circuit_json::SerialCircuit; use tket_json_rs::optype::OpType as JsonOpType; -use self::op::JsonOp; -use crate::utils::{BIT, QB}; +use self::decoder::JsonDecoder; /// A JSON-serialized TKET1 circuit that can be converted to a [`Hugr`]. -pub trait TKET1Decode { +pub trait TKET1Decode: Sized { /// The error type for decoding. type DecodeError; /// Convert the serialized circuit to a [`Hugr`]. @@ -34,6 +26,7 @@ pub trait TKET1Decode { impl TKET1Decode for SerialCircuit { type DecodeError = OpConvertError; + fn decode(self) -> Result { let mut decoder = JsonDecoder::new(&self); @@ -52,154 +45,6 @@ impl TKET1Decode for SerialCircuit { } } -/// The state of an in-progress [`DFGBuilder`] being built from a [`SerialCircuit`]. -/// -/// Mostly used to define helper internal methods. -#[derive(Debug, PartialEq)] -struct JsonDecoder { - /// The Hugr being built. - pub hugr: DFGBuilder, - /// The dangling wires of the builder. - /// Used to generate [`CircuitBuilder`]s. - dangling_wires: Vec, - /// A map from the json registers to flat wire indices. - register_wire: HashMap, - /// The number of qubits in the circuit. - num_qubits: usize, - /// The number of bits in the circuit. - num_bits: usize, -} - -impl JsonDecoder { - /// Initialize a new [`JsonDecoder`], using the metadata from a [`SerialCircuit`]. - pub fn new(serialcirc: &SerialCircuit) -> Self { - let num_qubits = serialcirc.qubits.len(); - let num_bits = serialcirc.bits.len(); - // Map each (register name, index) pair to an offset in the signature. - let mut wire_map: HashMap = - HashMap::with_capacity(num_bits + num_qubits); - for (i, register) in serialcirc - .qubits - .iter() - .chain(serialcirc.bits.iter()) - .enumerate() - { - if register.1.len() != 1 { - // TODO: Support multi-index registers? - panic!("Register {} has more than one index", register.0); - } - wire_map.insert((register, 0).into(), i); - } - let sig = Signature::new_linear([vec![QB; num_qubits], vec![BIT; num_bits]].concat()); - - let mut dfg = DFGBuilder::new(sig.input, sig.output).unwrap(); - - dfg.set_metadata(json!({"name": serialcirc.name})); - - let dangling_wires = dfg.input_wires().collect::>(); - JsonDecoder { - hugr: dfg, - dangling_wires, - register_wire: wire_map, - num_qubits, - num_bits, - } - } - - /// Finish building the [`Hugr`]. - pub fn finish(self) -> Hugr { - // TODO: Throw validation error? - self.hugr - .finish_hugr_with_outputs(self.dangling_wires) - .unwrap() - } - - /// Add a [`Command`] from the serial circuit to the [`JsonDecoder`]. - /// - /// - [`Command`]: circuit_json::Command - pub fn add_command(&mut self, command: circuit_json::Command) { - let circuit_json::Command { op, args, .. } = command; - let params = op.params.clone().unwrap_or_default(); - let num_qubits = args - .iter() - .take_while(|&arg| self.reg_wire(arg, 0) < self.num_qubits) - .count(); - let num_bits = args.len() - num_qubits; - let op = JsonOp::new_from_op(op, num_qubits, num_bits); - - let args: Vec<_> = args.into_iter().map(|reg| self.reg_wire(®, 0)).collect(); - - let param_wires: Vec = params.iter().map(|p| self.get_param_wire(p)).collect(); - - let append_wires = args - .into_iter() - .map(AppendWire::I) - .chain(param_wires.into_iter().map(AppendWire::W)); - - self.with_circ_builder(|circ| { - circ.append_and_consume(&op, append_wires).unwrap(); - }); - } - - /// Apply a function to the internal hugr builder viewed as a [`CircuitBuilder`]. - fn with_circ_builder(&mut self, f: impl FnOnce(&mut CircuitBuilder>)) { - let mut circ = self.hugr.as_circuit(mem::take(&mut self.dangling_wires)); - f(&mut circ); - self.dangling_wires = circ.finish(); - } - - /// Returns the wire carrying a parameter. - /// - /// If the parameter is a constant, a constant definition is added to the Hugr. - /// - /// TODO: If the parameter is a variable, returns the corresponding wire from the input. - fn get_param_wire(&mut self, param: &str) -> Wire { - if let Ok(f) = param.parse::() { - self.hugr.add_load_const(ConstValue::F64(f)).unwrap() - } else if param.split('/').count() == 2 { - // TODO: Use the rational types from `Hugr::extensions::rotation` - let (n, d) = param.split_once('/').unwrap(); - let n = n.parse::().unwrap(); - let d = d.parse::().unwrap(); - self.hugr.add_load_const(ConstValue::F64(n / d)).unwrap() - } else { - // TODO: Pre-compute variables and add them to the input signature. - todo!("Variable parameters not yet supported") - } - } - - /// Return the wire index for the `elem`th value of a given register. - /// - /// Relies on TKET1 constraint that all registers have unique names. - fn reg_wire(&self, register: &circuit_json::Register, elem: usize) -> usize { - self.register_wire[&(register, elem).into()] - } -} - -/// The state of an in-progress [`SerialCircuit`] being built from a [`Circuit`]. -#[derive(Debug, PartialEq)] -struct JsonEncoder { - /// The Hugr being built. - pub serial: SerialCircuit, - /// The dangling wires of the builder. - /// Used to generate [`CircuitBuilder`]s. - dangling_wires: Vec, - /// A map from the json registers to flat wire indices. - register_wire: HashMap, - /// The number of qubits in the circuit. - num_qubits: usize, - /// The number of bits in the circuit. - num_bits: usize, -} - -//impl JsonEncoder { -// /// Create a new [`JsonEncoder`] from a [`Circuit`]. -// pub fn new(circ: &impl Circuit) -> Self { -// let num_qubits = circ.qubits().len(); -// let num_bits = circ.bits().len(); -// } -//} - /// Error type for conversion between `Op` and `OpType`. #[derive(Debug, Error)] pub enum OpConvertError { @@ -210,21 +55,3 @@ pub enum OpConvertError { #[error("Cannot serialize operation: {0:?}")] UnsupportedOpSerialization(OpType), } - -/// A hashed register, used to identify registers in the [`JsonDecoder::register_wire`] map, -/// avoiding string clones on lookup. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct RegisterHash { - hash: u64, -} - -impl From<(&circuit_json::Register, usize)> for RegisterHash { - fn from((reg, elem): (&circuit_json::Register, usize)) -> Self { - let mut hasher = DefaultHasher::new(); - reg.0.hash(&mut hasher); - reg.1[elem].hash(&mut hasher); - Self { - hash: hasher.finish(), - } - } -} diff --git a/src/json/decoder.rs b/src/json/decoder.rs new file mode 100644 index 00000000..4609afeb --- /dev/null +++ b/src/json/decoder.rs @@ -0,0 +1,160 @@ +//! Intermediate structure for converting decoding [`SerialCircuit`]s into [`Hugr`]s. + +use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::mem; + +use hugr::builder::{AppendWire, CircuitBuilder, Container, DFGBuilder, Dataflow, DataflowHugr}; +use hugr::ops::ConstValue; +use hugr::types::Signature; +use hugr::{Hugr, Wire}; + +use serde_json::json; +use tket_json_rs::circuit_json; +use tket_json_rs::circuit_json::SerialCircuit; + +use super::op::JsonOp; +use crate::utils::{BIT, QB}; + +/// The state of an in-progress [`DFGBuilder`] being built from a [`SerialCircuit`]. +/// +/// Mostly used to define helper internal methods. +#[derive(Debug, PartialEq)] +pub(super) struct JsonDecoder { + /// The Hugr being built. + pub hugr: DFGBuilder, + /// The dangling wires of the builder. + /// Used to generate [`CircuitBuilder`]s. + dangling_wires: Vec, + /// A map from the json registers to flat wire indices. + register_wire: HashMap, + /// The number of qubits in the circuit. + num_qubits: usize, + /// The number of bits in the circuit. + num_bits: usize, +} + +impl JsonDecoder { + /// Initialize a new [`JsonDecoder`], using the metadata from a [`SerialCircuit`]. + pub fn new(serialcirc: &SerialCircuit) -> Self { + let num_qubits = serialcirc.qubits.len(); + let num_bits = serialcirc.bits.len(); + // Map each (register name, index) pair to an offset in the signature. + let mut wire_map: HashMap = + HashMap::with_capacity(num_bits + num_qubits); + for (i, register) in serialcirc + .qubits + .iter() + .chain(serialcirc.bits.iter()) + .enumerate() + { + if register.1.len() != 1 { + // TODO: Support multi-index registers? + panic!("Register {} has more than one index", register.0); + } + wire_map.insert((register, 0).into(), i); + } + let sig = Signature::new_linear([vec![QB; num_qubits], vec![BIT; num_bits]].concat()); + + let mut dfg = DFGBuilder::new(sig.input, sig.output).unwrap(); + + dfg.set_metadata(json!({"name": serialcirc.name})); + + let dangling_wires = dfg.input_wires().collect::>(); + JsonDecoder { + hugr: dfg, + dangling_wires, + register_wire: wire_map, + num_qubits, + num_bits, + } + } + + /// Finish building the [`Hugr`]. + pub fn finish(self) -> Hugr { + // TODO: Throw validation error? + self.hugr + .finish_hugr_with_outputs(self.dangling_wires) + .unwrap() + } + + /// Add a [`Command`] from the serial circuit to the [`JsonDecoder`]. + /// + /// - [`Command`]: circuit_json::Command + pub fn add_command(&mut self, command: circuit_json::Command) { + let circuit_json::Command { op, args, .. } = command; + let params = op.params.clone().unwrap_or_default(); + let num_qubits = args + .iter() + .take_while(|&arg| self.reg_wire(arg, 0) < self.num_qubits) + .count(); + let num_bits = args.len() - num_qubits; + let op = JsonOp::new_from_op(op, num_qubits, num_bits); + + let args: Vec<_> = args.into_iter().map(|reg| self.reg_wire(®, 0)).collect(); + + let param_wires: Vec = params.iter().map(|p| self.get_param_wire(p)).collect(); + + let append_wires = args + .into_iter() + .map(AppendWire::I) + .chain(param_wires.into_iter().map(AppendWire::W)); + + self.with_circ_builder(|circ| { + circ.append_and_consume(&op, append_wires).unwrap(); + }); + } + + /// Apply a function to the internal hugr builder viewed as a [`CircuitBuilder`]. + fn with_circ_builder(&mut self, f: impl FnOnce(&mut CircuitBuilder>)) { + let mut circ = self.hugr.as_circuit(mem::take(&mut self.dangling_wires)); + f(&mut circ); + self.dangling_wires = circ.finish(); + } + + /// Returns the wire carrying a parameter. + /// + /// If the parameter is a constant, a constant definition is added to the Hugr. + /// + /// TODO: If the parameter is a variable, returns the corresponding wire from the input. + fn get_param_wire(&mut self, param: &str) -> Wire { + if let Ok(f) = param.parse::() { + self.hugr.add_load_const(ConstValue::F64(f)).unwrap() + } else if param.split('/').count() == 2 { + // TODO: Use the rational types from `Hugr::extensions::rotation` + let (n, d) = param.split_once('/').unwrap(); + let n = n.parse::().unwrap(); + let d = d.parse::().unwrap(); + self.hugr.add_load_const(ConstValue::F64(n / d)).unwrap() + } else { + // TODO: Pre-compute variables and add them to the input signature. + todo!("Variable parameters not yet supported") + } + } + + /// Return the wire index for the `elem`th value of a given register. + /// + /// Relies on TKET1 constraint that all registers have unique names. + fn reg_wire(&self, register: &circuit_json::Register, elem: usize) -> usize { + self.register_wire[&(register, elem).into()] + } +} + +/// A hashed register, used to identify registers in the [`JsonDecoder::register_wire`] map, +/// avoiding string clones on lookup. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct RegisterHash { + hash: u64, +} + +impl From<(&circuit_json::Register, usize)> for RegisterHash { + fn from((reg, elem): (&circuit_json::Register, usize)) -> Self { + let mut hasher = DefaultHasher::new(); + reg.0.hash(&mut hasher); + reg.1[elem].hash(&mut hasher); + Self { + hash: hasher.finish(), + } + } +} diff --git a/src/json/op.rs b/src/json/op.rs index 652e7b2b..52520cae 100644 --- a/src/json/op.rs +++ b/src/json/op.rs @@ -100,6 +100,9 @@ impl JsonOp { } /// Compute the signature of the operation. + // + // TODO: We are using Hugr's non-liner bits. We should have a custom linear + // bit type instead. #[inline] #[allow(unused)] pub fn signature(&self) -> Signature {