diff --git a/Cargo.toml b/Cargo.toml index 94ac9131..361aded0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,24 +25,21 @@ 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.1" } +quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", tag = "v0.0.0-alpha.3" } smol_str = "0.2.0" typetag = "0.2.8" - +itertools = "0.11.0" +petgraph = { version = "0.6.3", default-features = false } [features] -pyo3 = [ - "dep:pyo3", - "tket-json-rs/pyo3", - "tket-json-rs/tket2ops", - "portgraph/pyo3", -] +pyo3 = ["dep:pyo3", "tket-json-rs/pyo3", "tket-json-rs/tket2ops", "portgraph/pyo3", "quantinuum-hugr/pyo3"] tkcxx = ["dep:tket-rs", "dep:num-complex"] [dev-dependencies] -rstest = "0.17.0" +rstest = "0.18.1" criterion = { version = "0.5.1", features = ["html_reports"] } - +webbrowser = "0.8.10" +urlencoding = "2.1.2" #[[bench]] #name = "tket2_bench" diff --git a/pyrs/Cargo.toml b/pyrs/Cargo.toml index e5c3201b..67a83b8b 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.1" } \ No newline at end of file +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 diff --git a/src/circuit.rs b/src/circuit.rs index 0cbd7ac4..62fe0760 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -1,7 +1,6 @@ -#[allow(clippy::module_inception)] -pub mod circuit; -//pub mod dag; -//pub mod operation; +//! Quantum circuit representation and operations. + +pub mod command; //#[cfg(feature = "pyo3")] //pub mod py_circuit; @@ -9,4 +8,107 @@ pub mod circuit; //#[cfg(feature = "tkcxx")] //pub mod unitarybox; +// TODO: Move TKET1's custom op definition to tket-rs (or hugr?) //mod tk1ops; + +use self::command::{Command, CommandIterator, Unit}; + +use hugr::ops::OpTrait; + +pub use hugr::hugr::region::Region; +pub use hugr::ops::OpType; +pub use hugr::types::{ClassicType, EdgeKind, LinearType, Signature, SimpleType, TypeRow}; +pub use hugr::{Node, Port, Wire}; +use petgraph::visit::{GraphBase, IntoNeighborsDirected, IntoNodeIdentifiers}; + +/// An object behaving like a quantum circuit. +// +// TODO: More methods: +// - other_{in,out}puts (for non-linear i/o + const inputs)? +// - Vertical slice iterator +// - Gate count map +// - Depth +pub trait Circuit<'circ> { + /// An iterator over the commands in the circuit. + type Commands: Iterator>; + + /// An iterator over the commands applied to an unit. + type UnitCommands: Iterator>; + + /// Return the name of the circuit + fn name(&self) -> Option<&str>; + + /// Get the linear inputs of the circuit and their types. + fn units(&self) -> Vec<(Unit, SimpleType)>; + + /// Returns the ports corresponding to qubits inputs to the circuit. + #[inline] + fn qubits(&self) -> Vec { + self.units() + .iter() + .filter(|(_, typ)| typ == &LinearType::Qubit.into()) + .map(|(unit, _)| *unit) + .collect() + } + + /// Given a linear port in a node, returns the corresponding port on the other side of the node (if any). + fn follow_linear_port(&self, node: Node, port: Port) -> Option; + + /// Returns all the commands in the circuit, in some topological order. + fn commands<'a: 'circ>(&'a self) -> Self::Commands; + + /// Returns all the commands applied to the given unit, in order. + fn unit_commands<'a: 'circ>(&'a self) -> Self::UnitCommands; +} + +impl<'circ, T> Circuit<'circ> for T +where + T: 'circ + Region<'circ>, + for<'a> &'a T: GraphBase + IntoNeighborsDirected + IntoNodeIdentifiers, +{ + type Commands = CommandIterator<'circ, T>; + type UnitCommands = std::iter::Empty>; + + #[inline] + fn name(&self) -> Option<&str> { + let meta = self.get_metadata(self.root()).as_object()?; + meta.get("name")?.as_str() + } + + #[inline] + fn units(&self) -> Vec<(Unit, SimpleType)> { + let root = self.root(); + let optype = self.get_optype(root); + optype + .signature() + .input_df_types() + .iter() + .filter(|typ| typ.is_linear()) + .enumerate() + .map(|(i, typ)| (i.into(), typ.clone())) + .collect() + } + + fn follow_linear_port(&self, node: Node, port: Port) -> Option { + let optype = self.get_optype(node); + if !optype.port_kind(port)?.is_linear() { + return None; + } + // TODO: We assume the linear data uses the same port offsets on both sides of the node. + // In the future we may want to have a more general mechanism to handle this. + let other_port = Port::new(port.direction().reverse(), port.index()); + debug_assert_eq!(optype.port_kind(other_port), optype.port_kind(port)); + Some(other_port) + } + + fn commands<'a: 'circ>(&'a self) -> Self::Commands { + // Traverse the circuit in topological order. + CommandIterator::new(self) + } + + fn unit_commands<'a: 'circ>(&'a self) -> Self::UnitCommands { + // TODO Can we associate linear i/o with the corresponding unit without + // doing the full toposort? + todo!() + } +} diff --git a/src/circuit/circuit.rs b/src/circuit/circuit.rs deleted file mode 100644 index 1889bb8b..00000000 --- a/src/circuit/circuit.rs +++ /dev/null @@ -1,667 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{Debug, Display}; -use std::hash::Hash; - -use portgraph::graph::{ConnectError, Direction, DIRECTIONS}; -use portgraph::substitute::{BoundedSubgraph, OpenGraph, RewriteError}; - -use super::dag::{Dag, Edge, EdgeProperties, TopSorter, Vertex, VertexProperties}; -use super::operation::{ConstValue, Op, Param, WireType}; -#[cfg(feature = "pyo3")] -use pyo3::prelude::*; - -#[cfg_attr(feature = "pyo3", derive(FromPyObject))] -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub enum UnitID { - Qubit { reg_name: String, index: Vec }, - Bit { name: String, index: Vec }, - F64(String), - Angle(String), -} - -impl UnitID { - pub fn get_type(&self) -> WireType { - match self { - Self::Qubit { .. } => WireType::Qubit, - Self::Bit { .. } => WireType::LinearBit, - Self::F64(_) => WireType::F64, - Self::Angle(_) => WireType::Angle, - } - } -} - -#[derive(Clone, PartialEq)] -struct Boundary { - pub input: Vertex, - pub output: Vertex, -} - -pub struct CycleInGraph(); -impl Debug for CycleInGraph { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("CycleInGraph: Cycle detected or created in graph. Not a DAG.") - .finish() - } -} - -impl From for String { - fn from(c: CycleInGraph) -> Self { - format!("{c:?}") - } -} - -#[cfg_attr(feature = "pyo3", pyclass(name = "RsCircuit"))] -#[derive(Clone, PartialEq)] -pub struct Circuit { - pub(crate) dag: Dag, - pub name: Option, - pub phase: Param, - boundary: Boundary, - pub(crate) uids: Vec, -} - -impl Default for Circuit { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug)] -pub struct CircuitError(pub String); - -impl Display for CircuitError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("CircuitError {}", self.0)) - } -} -impl Error for CircuitError {} - -impl From for CircuitError { - fn from(s: String) -> Self { - Self(s) - } -} - -impl From<&str> for CircuitError { - fn from(s: &str) -> Self { - s.to_string().into() - } -} - -#[cfg_attr(feature = "pyo3", pymethods)] -impl Circuit { - pub fn add_vertex(&mut self, op: Op) -> Vertex { - // let capacity = op.signature().map_or(0, |sig| sig.len()); - let weight = VertexProperties::new(op); - self.dag.add_node(weight) - } - - pub fn add_insert_edge( - &mut self, - source: (Vertex, usize), - target: (Vertex, usize), - edge_type: WireType, - ) -> Result { - let e = self.add_edge(edge_type); - self.dag - .insert_edge(source.0, e, Direction::Outgoing, source.1)?; - self.dag - .insert_edge(target.0, e, Direction::Incoming, target.1)?; - Ok(e) - } - - pub fn add_vertex_with_edges( - &mut self, - op: Op, - incoming: Vec, - outgoing: Vec, - ) -> Vertex { - let weight = VertexProperties::new(op); - self.dag - .add_node_with_edges(weight, incoming, outgoing) - .unwrap() - } - pub fn add_edge(&mut self, edge_type: WireType) -> Edge { - self.dag.add_edge(EdgeProperties { edge_type }) - } - - pub fn add_unitid(&mut self, uid: UnitID) -> Edge { - let e = self.add_edge(uid.get_type()); - self.uids.push(uid); - - self.dag - .connect_last(self.boundary.input, e, Direction::Outgoing) - .unwrap(); - e - } - - pub fn add_linear_unitid(&mut self, uid: UnitID) { - let ie = self.add_edge(uid.get_type()); - - self.dag - .connect_last(self.boundary.input, ie, Direction::Outgoing) - .unwrap(); - self.dag - .connect_last(self.boundary.output, ie, Direction::Incoming) - .unwrap(); - // let [_, inlen] = self.dag.node_boundary_size(self.boundary.input); - // let [outlen, _] = self.dag.node_boundary_size(self.boundary.output); - // self.tup_add_edge( - // (self.boundary.input, inlen as u8), - // (self.boundary.output, outlen as u8), - // uid.get_type(), - // ); - self.uids.push(uid); - } - - pub fn node_op(&self, n: Vertex) -> Option<&Op> { - self.dag.node_weight(n).map(|vp| &vp.op) - } - - pub fn edge_type(&self, e: Edge) -> Option { - self.dag.edge_weight(e).map(|ep| ep.edge_type) - } - pub fn dot_string(&self) -> String { - portgraph::dot::dot_string(self.dag_ref()) - } - - pub fn apply_rewrite(&mut self, rewrite: CircuitRewrite) -> Result<(), RewriteError> { - self.dag.apply_rewrite(rewrite.graph_rewrite)?; - self.phase += rewrite.phase; - Ok(()) - } - - // pub fn insert_at_edge( - // &mut self, - // vert: Vertex, - // edge: Edge, - // ports: [PortIndex; 2], - // ) -> Result<(), CircuitError> { - // let [s, t] = self - // .dag - // .edge_endpoints(edge) - // .ok_or_else(|| "Edge not found.".to_string())?; - // self.dag.update_edge(edge, s, NodePort::new(vert, ports[0])); - - // self.tup_add_edge( - // NodePort::new(vert, ports[1]), - // t, - // self.dag.edge_weight(edge).unwrap().edge_type, - // ); - // Ok(()) - // } - - pub fn remove_node(&mut self, n: Vertex) -> Option { - self.dag.remove_node(n).map(|v| v.op) - } - - pub fn remove_edge(&mut self, e: Edge) -> Option { - self.dag.remove_edge(e).map(|ep| ep.edge_type) - } - - pub fn edge_endpoints(&self, e: Edge) -> Option<(Vertex, Vertex)> { - let s = self.dag.edge_endpoint(e, Direction::Outgoing)?; - let t = self.dag.edge_endpoint(e, Direction::Incoming)?; - // self.dag.edge_endpoints(e).map(|[e1, e2]| (e1, e2)) - Some((s, t)) - } - - pub fn edge_at_port(&self, n: Vertex, port: usize, direction: Direction) -> Option { - self.dag.node_edges(n, direction).nth(port) - } - - pub fn port_of_edge(&self, n: Vertex, edge: Edge, direction: Direction) -> Option { - self.dag - .node_edges(n, direction) - .enumerate() - .find_map(|(i, oe)| (oe == edge).then_some(i)) - } - - pub fn node_edges(&self, n: Vertex, direction: Direction) -> Vec { - self.dag.node_edges(n, direction).collect() - } - - pub fn node_boundary_size(&self, n: Vertex) -> [usize; 2] { - DIRECTIONS.map(|direction| self.dag.node_edges(n, direction).count()) - } - - pub fn neighbours(&self, n: Vertex, direction: Direction) -> Vec { - self.dag - .node_edges(n, direction) - .map(|e| self.dag.edge_endpoint(e, direction.reverse()).unwrap()) - .collect() - } - - pub fn add_const(&mut self, c: ConstValue) -> Vertex { - self.add_vertex(Op::Const(c)) - } - - pub fn get_const(&self, n: Vertex) -> Option<&ConstValue> { - self.node_op(n).and_then(|op| match op { - Op::Const(c) => Some(c), - _ => None, - }) - } - - pub fn node_count(&self) -> usize { - self.dag.node_count() - } - - pub fn edge_count(&self) -> usize { - self.dag.edge_count() - } - - pub fn new_input(&mut self, edge_type: WireType) -> Edge { - let e = self.add_edge(edge_type); - self.dag - .connect_first(self.boundary.input, e, Direction::Outgoing) - .unwrap(); - e - } - - pub fn new_output(&mut self, edge_type: WireType) -> Edge { - let e = self.add_edge(edge_type); - self.dag - .connect_first(self.boundary.output, e, Direction::Incoming) - .unwrap(); - e - } -} -impl Circuit { - pub fn new() -> Self { - Self::with_uids(vec![]) - } - - pub fn with_uids(uids: Vec) -> Self { - let n_uids = uids.len(); - let mut dag = Dag::with_capacity(2, n_uids); - let input = dag.add_node(VertexProperties::new(Op::Input)); - let output = dag.add_node(VertexProperties::new(Op::Output)); - let mut slf = Self { - dag, - name: None, - phase: 0.0, - boundary: Boundary { input, output }, - uids: Vec::with_capacity(n_uids), - }; - for uid in uids { - slf.add_linear_unitid(uid); - } - slf - } - - pub fn bind_input(&mut self, in_port: usize, val: ConstValue) -> Result { - let e = self - .edge_at_port(self.boundary.input, in_port, Direction::Outgoing) - .ok_or_else(|| "No such input".to_string())?; - // let target = self.dag.edge_endpoint(e, Direction::Incoming); - // let [_, target] = self.dag.edge_endpoints(e).unwrap(); - // let existing_typ = self.dag.remove_edge(e).unwrap().edge_type; - if val.get_type() != self.edge_type(e).unwrap() { - return Err("Edge type of input does not match type of provided value.".into()); - } - self.dag.disconnect(e, Direction::Outgoing); - // let oes = vec![self.add_edge(existing_typ)]; - Ok(self.add_vertex_with_edges(Op::Const(val), vec![], vec![e])) - // self.tup_add_edge((cons, 0).into(), target, existing_typ); - // Ok(cons) - } - - pub fn append_op(&mut self, op: Op, args: &[usize]) -> Result { - // akin to add-op in TKET-1 - - // let new_vert = self.add_vertex(op); - let out_edges: Vec<_> = self - .dag - .node_edges(self.boundary.output, Direction::Incoming) - .collect(); - let insertion_edges = args - .iter() - .map(|port| out_edges.get(*port).copied()) - .collect::>>() - .ok_or(ConnectError::UnknownEdge)?; - - let mut incoming = vec![]; - // let mut outgoing = vec![]; - for e in insertion_edges.iter() { - let e_type = self - .dag - .edge_weight(*e) - .expect("Edge should be there.") - .edge_type; - let in_e = self.add_edge(e_type); - self.dag.replace_connection(*e, in_e, Direction::Outgoing)?; - // let prev = self.dag.edge_endpoint(*e, Direction::Outgoing).unwrap(); - // self.dag.connect_after(prev, in_e, Direction::Outgoing, *e); - // self.dag.disconnect(*e, Direction::Outgoing); - incoming.push(in_e); - // outgoing.push(self.add_edge(e_type)); - // let p = PortIndex::new(p); - // self.insert_at_edge(new_vert, e, [p; 2])?; - } - self.dag - .add_node_with_edges(VertexProperties { op }, incoming, insertion_edges) - - // Ok(new_vert) - } - - pub fn to_commands(&self) -> CommandIter { - CommandIter::new(self) - } - - pub fn commands_with_unitid(&self, unitid: UnitID) -> impl Iterator { - self.to_commands() - .filter(move |cmd| cmd.args.contains(&unitid)) - } - - pub fn qubits(&self) -> impl Iterator + '_ { - self.uids.iter().filter_map(|uid| match uid { - UnitID::Qubit { .. } => Some(uid.clone()), - UnitID::Bit { .. } | UnitID::F64(_) | UnitID::Angle(_) => None, - }) - } - - pub fn bits(&self) -> impl Iterator + '_ { - self.uids.iter().filter_map(|uid| match uid { - UnitID::Bit { .. } => Some(uid.clone()), - UnitID::Qubit { .. } | UnitID::F64(_) | UnitID::Angle(_) => None, - }) - } - - pub fn unitids(&self) -> impl Iterator + '_ { - self.uids.iter() - } - - pub fn boundary(&self) -> [Vertex; 2] { - [self.boundary.input, self.boundary.output] - } - - /// send an edge in to a copy vertex and return - /// the N-1 new edges (with the first being connected to the existing target) - pub fn copy_edge(&mut self, e: Edge, copies: u32) -> Result, String> { - let edge_type = match self.dag.edge_weight(e) { - Some(EdgeProperties { edge_type, .. }) => *edge_type, - _ => return Err("Edge not found".into()), - }; - - let copy_op = match edge_type { - WireType::Qubit | WireType::LinearBit => { - return Err("Cannot copy qubit or LinearBit wires.".into()) - } - _ => Op::Copy { - n_copies: copies, - typ: edge_type, - }, - }; - - let mut copy_es: Vec<_> = (0..copies).map(|_| self.add_edge(edge_type)).collect(); - // let copy_node = self.add_vertex(copy_op); - self.dag - .replace_connection(e, copy_es[0], Direction::Incoming) - .unwrap(); - // self.dag.disconnect(e, Direction::Incoming); - // let edge_type = self.dag.remove_edge(e).unwrap().edge_type; - // self.tup_add_edge(s, (copy_node, 0).into(), edge_type); - // self.tup_add_edge((copy_node, 0).into(), t, edge_type); - - // Ok(copy_node) - self.add_vertex_with_edges(copy_op, vec![e], copy_es.clone()); - - copy_es.remove(0); - Ok(copy_es) - } - - // pub fn apply_rewrite(&mut self, rewrite: CircuitRewrite) -> Result<(), String> { - // self.dag.apply_rewrite(rewrite.graph_rewrite)?; - // self.phase += rewrite.phase; - // Ok(()) - // } - pub fn remove_invalid(mut self) -> Self { - let (node_map, _) = self.dag.compact(); - self.boundary = Boundary { - input: node_map[&self.boundary.input], - output: node_map[&self.boundary.output], - }; - self - } - - pub fn dag_ref(&self) -> &Dag { - &self.dag - } -} - -#[derive(Debug, PartialEq)] -pub struct Command<'c> { - pub vertex: Vertex, - pub op: &'c Op, - pub args: Vec, -} - -pub struct CommandIter<'circ> { - nodes: TopSorter<'circ>, - circ: &'circ Circuit, - frontier: HashMap, -} - -impl<'circ> CommandIter<'circ> { - fn new(circ: &'circ Circuit) -> Self { - Self { - nodes: TopSorter::new( - &circ.dag, - circ.dag - .node_indices() - .filter(|n| circ.dag.node_edges(*n, Direction::Incoming).count() == 0) - .collect(), - ) - .with_cyclicity_check(), - frontier: circ - .dag - .node_edges(circ.boundary.input, Direction::Outgoing) - .enumerate() - .map(|(i, e)| (e, circ.uids.get(i).unwrap())) - .collect(), - circ, - } - } -} - -impl<'circ> Iterator for CommandIter<'circ> { - type Item = Command<'circ>; - - fn next(&mut self) -> Option { - self.nodes.next().map(|node| { - let VertexProperties { op } = self.circ.dag.node_weight(node).expect("Node not found"); - // assumes linarity - let args = self - .circ - .dag - .node_edges(node, Direction::Incoming) - .zip(self.circ.dag.node_edges(node, Direction::Outgoing)) - .map(|(in_e, out_e)| { - let uid = self.frontier.remove(&in_e).expect("edge not in frontier"); - self.frontier.insert(out_e, uid); - uid.clone() - }) - .collect(); - - Command { - vertex: node, - op, - args, - } - }) - } -} - -pub(crate) type CircDagRewrite = portgraph::substitute::Rewrite; - -#[cfg_attr(feature = "pyo3", pyclass)] -#[derive(Debug, Clone)] -pub struct CircuitRewrite { - pub graph_rewrite: CircDagRewrite, - pub phase: Param, -} - -impl CircuitRewrite { - pub fn new( - subg: BoundedSubgraph, - replacement: OpenGraph, - phase: Param, - ) -> Self { - Self { - graph_rewrite: CircDagRewrite { subg, replacement }, - phase, - } - } -} - -impl From for OpenGraph { - fn from(mut c: Circuit) -> Self { - let [entry, exit] = c.boundary(); - let in_ports = c.dag.node_edges(entry, Direction::Outgoing).collect(); - let out_ports = c.dag.node_edges(exit, Direction::Incoming).collect(); - - c.dag.remove_node(entry); - c.dag.remove_node(exit); - Self { - dangling: [in_ports, out_ports], - graph: c.dag, - } - } -} - -#[cfg(test)] -mod tests { - use crate::circuit::{ - circuit::CircuitRewrite, - operation::{ConstValue, Op, WireType}, - }; - use portgraph::{ - graph::Direction, - substitute::{BoundedSubgraph, Rewrite, SubgraphRef}, - }; - - use super::{Circuit, UnitID}; - - #[test] - fn test_add_identity() { - let mut circ = Circuit::new(); - // let [i, o] = circ.boundary(); - for _ in 0..2 { - let ie = circ.new_input(WireType::Qubit); - let oe = circ.new_output(WireType::Qubit); - let _noop = circ.add_vertex_with_edges(Op::Noop(WireType::Qubit), vec![ie], vec![oe]); - // circ.dag.connect_first(i, ie, Direction::Outgoing); - // circ.dag.connect_first(o, oe, Direction::Incoming); - // circ.tup_add_edge((i, p), (noop, 0), WireType::Qubit); - // circ.tup_add_edge((noop, 0), (o, p), WireType::Qubit); - } - } - - #[test] - fn test_bind_value() { - let mut circ = Circuit::new(); - // let [i, o] = circ.boundary(); - - let i1 = circ.new_input(WireType::F64); - let i0 = circ.new_input(WireType::F64); - - let o = circ.new_output(WireType::F64); - let add = circ.add_vertex_with_edges(Op::AngleAdd, vec![i0, i1], vec![o]); - - // circ.tup_add_edge((i, 0), (add, 0), WireType::F64); - // circ.tup_add_edge((i, 1), (add, 1), WireType::F64); - // circ.tup_add_edge((add, 0), (o, 0), WireType::F64); - - assert_eq!(circ.dag.edge_count(), 3); - assert_eq!(circ.dag.node_count(), 3); - assert_eq!( - circ.dag - .node_edges(circ.boundary.input, Direction::Outgoing) - .count(), - 2 - ); - - // println!("{}", circ.dag.node_edges(circ.boundary.output, Direction::Incoming).count()); - // println!("{:?}", circ.edge_at_port(circ.boundary.input, 1, Direction::Outgoing)); - - circ.bind_input(0, ConstValue::F64(1.0)).unwrap(); - circ.bind_input(0, ConstValue::F64(2.0)).unwrap(); - println!("{}", circ.dot_string()); - - assert_eq!(circ.dag.edge_count(), 3); - assert_eq!(circ.dag.node_count(), 5); - assert_eq!( - circ.dag - .node_edges(circ.boundary.input, Direction::Outgoing) - .count(), - 0 - ); - - let neis = circ.neighbours(add, Direction::Incoming); - - assert_eq!( - circ.dag.node_weight(neis[0]).unwrap().op, - Op::Const(ConstValue::F64(1.0)) - ); - assert_eq!( - circ.dag.node_weight(neis[1]).unwrap().op, - Op::Const(ConstValue::F64(2.0)) - ); - } - - #[test] - fn test_add_uid() { - let q0 = UnitID::Qubit { - reg_name: "q".into(), - index: vec![0], - }; - let a = UnitID::Angle("a".into()); - - // An empty circuit with UnitIDs [q0, a] - let mut c = Circuit::new(); - c.add_linear_unitid(q0.clone()); - c.add_unitid(a.clone()); - - // Make sure UnitIDs and edges are stored in right order - assert_eq!(c.uids, vec![q0, a]); - assert_eq!( - c.node_edges(c.boundary.input, Direction::Outgoing) - .into_iter() - .map(|e| c.edge_type(e).unwrap()) - .collect::>(), - vec![WireType::Qubit, WireType::Angle] - ) - } - - #[test] - fn test_sub_empty() { - let q0 = UnitID::Qubit { - reg_name: "q".into(), - index: vec![0], - }; - let q1 = UnitID::Qubit { - reg_name: "q".into(), - index: vec![1], - }; - let qbs = vec![q0, q1]; - let mut circ = Circuit::with_uids(qbs.clone()); - let cx = circ.append_op(Op::CX, &[0, 1]).unwrap(); - let replacement = Circuit::with_uids(qbs).into(); - let rewrite = CircuitRewrite { - graph_rewrite: Rewrite::new( - BoundedSubgraph { - subgraph: SubgraphRef::new([cx].iter().cloned().collect()), - edges: [ - circ.node_edges(cx, Direction::Incoming), - circ.node_edges(cx, Direction::Outgoing), - ], - }, - replacement, - ), - phase: 0., - }; - assert!(circ.apply_rewrite(rewrite).is_ok()); - } -} diff --git a/src/circuit/command.rs b/src/circuit/command.rs new file mode 100644 index 00000000..fa06d284 --- /dev/null +++ b/src/circuit/command.rs @@ -0,0 +1,173 @@ +//! Circuit commands. +//! +//! A [`Command`] is an operation applied to an specific wires, possibly identified by their index in the circuit's input vector. + +use std::collections::HashMap; +use std::iter::FusedIterator; + +use hugr::hugr::region::Region; +use hugr::ops::OpTrait; +pub use hugr::ops::OpType; +pub use hugr::types::{ClassicType, EdgeKind, LinearType, Signature, SimpleType, TypeRow}; +pub use hugr::{Node, Port, Wire}; +use petgraph::visit::{GraphBase, IntoNeighborsDirected, IntoNodeIdentifiers}; + +use super::Circuit; + +/// Descriptor of a wire in a [`Circuit`]. If it is a qubit or linear bit +/// originating from the circuit's input, it is described by an index. +/// Otherwise, it is described by an internal [`Wire`]. +// +// TODO Better name? +// TODO Merge this with CircuitBuilder::AppendWire? +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Unit { + /// Arbitrary wire. + W(Wire), + /// Index of a linear element in the [`Circuit`]'s input vector. + Linear(usize), +} + +impl From for Unit { + fn from(value: usize) -> Self { + Unit::Linear(value) + } +} + +impl From for Unit { + fn from(value: Wire) -> Self { + Unit::W(value) + } +} + +/// An operation applied to specific wires. +pub struct Command<'circ> { + /// The operation. + pub op: &'circ OpType, + /// The operation node. + pub node: Node, + /// The input units to the operation. + pub inputs: Vec, + /// The output units to the operation. + pub outputs: Vec, +} + +/// An iterator over the commands of a circuit. +#[derive(Clone, Debug)] +pub struct CommandIterator<'circ, Circ> { + /// The circuit + circ: &'circ Circ, + /// Toposorted nodes + nodes: Vec, + /// Current element in `nodes` + current: usize, + /// Last wires for each linear `Unit` + wire_unit: HashMap, +} + +impl<'circ, Circ> CommandIterator<'circ, Circ> +where + Circ: Region<'circ>, + for<'a> &'a Circ: GraphBase + IntoNeighborsDirected + IntoNodeIdentifiers, +{ + /// Create a new iterator over the commands of a circuit. + pub(super) fn new(circ: &'circ Circ) -> Self { + let nodes = petgraph::algo::toposort(circ, None).unwrap(); + Self { + circ, + nodes, + current: 0, + wire_unit: HashMap::new(), + } + } + + /// Process a new node, updating wires in `unit_wires` and returns the + /// command for the node. + fn process_node(&mut self, node: Node) -> Command<'circ> { + let optype = self.circ.get_optype(node); + let sig = optype.signature(); + + // Get the wire corresponding to each input unit. + // TODO: Add this to HugrView? + let inputs = sig + .input_ports() + .filter_map(|port| { + let (from, from_port) = self.circ.linked_ports(node, port).next()?; + let wire = Wire::new(from, from_port); + // Get the unit corresponding to a wire, or return a wire Unit. + match self.wire_unit.remove(&wire) { + Some(unit) => { + if let Some(new_port) = self.circ.follow_linear_port(node, port) { + self.wire_unit.insert(Wire::new(node, new_port), unit); + } + Some(Unit::Linear(unit)) + } + None => Some(Unit::W(wire)), + } + }) + .collect(); + + let outputs = sig + .output_ports() + .map(|port| { + let wire = Wire::new(node, port); + match self.wire_unit.get(&wire) { + Some(&unit) => Unit::Linear(unit), + None => Unit::W(wire), + } + }) + .collect(); + + Command { + op: optype, + node, + inputs, + outputs, + } + } +} + +impl<'circ, Circ> Iterator for CommandIterator<'circ, Circ> +where + Circ: Region<'circ>, + for<'a> &'a Circ: GraphBase + IntoNeighborsDirected + IntoNodeIdentifiers, +{ + type Item = Command<'circ>; + + fn next(&mut self) -> Option { + if self.current == self.nodes.len() { + return None; + } + let node = self.nodes[self.current]; + self.current += 1; + Some(self.process_node(node)) + } + + #[inline] + fn count(self) -> usize { + self.len() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (self.len(), Some(self.len())) + } +} + +impl<'circ, Circ> ExactSizeIterator for CommandIterator<'circ, Circ> +where + Circ: Region<'circ>, + for<'a> &'a Circ: GraphBase + IntoNeighborsDirected + IntoNodeIdentifiers, +{ + #[inline] + fn len(&self) -> usize { + self.nodes.len() - self.current + } +} + +impl<'circ, Circ> FusedIterator for CommandIterator<'circ, Circ> +where + Circ: Region<'circ>, + for<'a> &'a Circ: GraphBase + IntoNeighborsDirected + IntoNodeIdentifiers, +{ +} diff --git a/src/circuit/dag.rs b/src/circuit/dag.rs deleted file mode 100644 index 170b0d3d..00000000 --- a/src/circuit/dag.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::fmt::Display; - -use super::operation::{Op, WireType}; - -#[derive(Clone, Default, Debug, PartialEq)] -pub struct VertexProperties { - pub op: Op, -} - -impl Display for VertexProperties { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.op) - } -} - -impl VertexProperties { - pub fn new(op: Op) -> Self { - Self { op } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct EdgeProperties { - pub edge_type: WireType, -} - -impl Display for EdgeProperties { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.edge_type) - } -} - -// pub(crate) type DAG = StableDag; -pub(crate) type Dag = portgraph::graph::Graph; -pub(crate) type TopSorter<'a> = - portgraph::toposort::TopSortWalker<'a, VertexProperties, EdgeProperties>; -pub(crate) type Vertex = portgraph::graph::NodeIndex; -pub(crate) type Edge = portgraph::graph::EdgeIndex; diff --git a/src/circuit/tk1ops.rs b/src/circuit/tk1ops.rs index 1046abf4..f7e164ed 100644 --- a/src/circuit/tk1ops.rs +++ b/src/circuit/tk1ops.rs @@ -1,14 +1,22 @@ -use super::operation::CustomOp; -use super::operation::Signature; -use super::operation::ToCircuitFail; +//! Implementation of Hugr's CustomOp trait for the TKET1 bindings in `tket-rs`. +//! +//! TODO: This cannot be defined here. Should we do it in `hugr` or in `tket-rs`? + +use hugr::ops::CustomOp; +use hugr::types::Signature; use tket_json_rs::circuit_json::Operation; +#[typetag::serde] impl CustomOp for Operation { - fn signature(&self) -> Option { - None + fn resources(&self) -> &ResourceSet { + todo!() + } + +fn name(&self) -> SmolStr { + todo!() } - fn to_circuit(&self) -> Result { - Err(ToCircuitFail) +fn signature(&self) -> Signature { + todo!() } } diff --git a/src/lib.rs b/src/lib.rs index 74315b19..f84c00b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,11 +7,11 @@ //! quantum software developers to take advantage of its state of the art //! compilation for many different quantum architectures. -//pub mod circuit; - -pub mod json; +pub mod circuit; +//pub mod json; pub mod passes; -// pub mod validate; -// #[cfg(test)] -// mod tests; +//mod utils; + +//#[cfg(test)] +//mod tests; diff --git a/src/tests.rs b/src/tests.rs index 07ad86d0..e048fc96 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,23 +1,10 @@ -use std::error::Error; +//! General tests. -use crate::{ - circuit::{ - circuit::{Circuit, UnitID}, - operation::WireType, - operation::{ConstValue, Op}, - }, - passes::{ - apply_exhaustive, apply_greedy, - classical::{constant_fold_strat, find_const_ops}, - squash::{ - cx_cancel_pass, find_singleq_rotations, find_singleq_rotations_pattern, - squash_pattern, - }, - }, - validate::check_soundness, -}; -use portgraph::Direction; -use tket_json_rs::circuit_json::{self, SerialCircuit}; +use hugr::hugr::region::FlatRegionView; +use hugr::{Hugr, HugrView}; +use tket_json_rs::circuit_json; + +use crate::json::json_convert::TKET1Decode; #[test] fn read_json() { @@ -26,19 +13,22 @@ fn read_json() { let ser: circuit_json::SerialCircuit = serde_json::from_str(circ_s).unwrap(); assert_eq!(ser.commands.len(), 4); - let circ: Circuit = ser.clone().into(); + let hugr: Hugr = ser.decode().unwrap(); + let _circ = FlatRegionView::new(&hugr, hugr.root()); + + //assert_eq!(circ.qubits().len(), 2); - check_soundness(&circ).unwrap(); + //check_soundness(&circ).unwrap(); - let _reser: SerialCircuit = circ.into(); - assert_eq!(&ser, &_reser); + //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] -fn read_json_unkwown_op() { +fn read_json_unknown_op() { // test ops that are not native to tket-2 are correctly captured as // custom and output @@ -46,358 +36,16 @@ fn read_json_unkwown_op() { let ser: circuit_json::SerialCircuit = serde_json::from_str(circ_s).unwrap(); assert_eq!(ser.commands.len(), 1); - let circ: Circuit = ser.clone().into(); - 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); -} -// #[test] -// fn test_param() { -// assert_eq!(Param::new("3") + Param::new("x"), Param::new("3 + x")); -// assert_eq!(Param::new("0") - Param::new("0.1"), Param::new("-0.1")); -// assert_eq!(Param::new("0.1").neg(), Param::new("-0.1")); - -// assert!(Param::new("x").eval().is_none()); -// assert_eq!(Param::new("2.0 + 2.0/4").eval(), Some(2.5)); -// assert!(equiv_0(&Param::new("0"), 4)); -// assert!(equiv_0(&Param::new("4.0"), 4)); -// assert!(equiv_0(&Param::new("8.0"), 4)); -// assert!(!equiv_0(&Param::new("2.0"), 4)); -// assert!(equiv_0(&Param::new("2.0"), 2)); -// assert!(!equiv_0(&Param::new("0.5"), 2)); -// } - -// #[test] -// fn test_dagger() { -// assert_eq!(Op::H.dagger().unwrap(), Op::H); -// assert_eq!(Op::CX.dagger().unwrap(), Op::CX); -// assert_eq!(Op::Rx(0.1.into()).dagger().unwrap(), Op::Rx((-0.1).into())); -// assert_eq!( -// Op::Rz(Param::new("x")).dagger().unwrap(), -// Op::Rz(Param::new("-x")) -// ); -// } - -#[test] -fn test_fadd() { - let mut circ = Circuit::new(); - - let i1 = circ.new_input(WireType::F64); - let i0 = circ.new_input(WireType::F64); - let o = circ.new_output(WireType::F64); - let _fadd = circ.add_vertex_with_edges(Op::AngleAdd, vec![i0, i1], vec![o]); -} - -#[test] -fn test_copy() -> Result<(), Box> { - let mut circ = Circuit::new(); - - let i = circ.add_unitid(UnitID::F64("a".into())); - let o = circ.new_output(WireType::F64); - - let fadd = circ.add_vertex_with_edges(Op::AngleAdd, vec![i], vec![o]); - - let copy_e = circ.copy_edge(i, 2).unwrap()[0]; - - circ.dag.insert_edge(fadd, copy_e, Direction::Incoming, 1)?; - // println!("{}", circ.dot_string()); - - Ok(()) -} - -#[test] -fn test_const_fold_simple() -> Result<(), Box> { - let mut circ = Circuit::new(); - - let [_, output] = circ.boundary(); - - let fadd = circ.add_vertex(Op::AngleAdd); - let one = circ.add_vertex(Op::Const(ConstValue::f64_angle(0.5))); - let two = circ.add_vertex(Op::Const(ConstValue::f64_angle(1.5))); - let _e1 = circ.add_insert_edge((one, 0), (fadd, 0), WireType::Angle)?; - let _e2 = circ.add_insert_edge((two, 0), (fadd, 1), WireType::Angle)?; - - let _out = circ.add_insert_edge((fadd, 0), (output, 0), WireType::Angle)?; - check_soundness(&circ).unwrap(); - - let rewrite = find_const_ops(&circ).next().unwrap(); - // println!("{:#?}", rewrite); - - circ.apply_rewrite(rewrite).unwrap(); - // println!("{}", circ.dot_string()); - - // println!("{}", dot_string(&circ.dag)); - assert_eq!(circ.dag.node_count(), 3); - assert_eq!(circ.dag.edge_count(), 1); - let mut nodeit = circ.dag.node_weights(); - // skip input and output - nodeit.next(); - nodeit.next(); - - assert_eq!( - &nodeit.next().unwrap().op, - &Op::Const(ConstValue::f64_angle(2.0)) - ); - - check_soundness(&circ).unwrap(); - Ok(()) -} - -#[test] -fn test_const_fold_less_simple() { - let mut circ = Circuit::new(); - - let qi = circ.new_input(WireType::Qubit); - let qo = circ.new_output(WireType::Qubit); - - let angle_edges = [0; 9].map(|_| circ.add_edge(WireType::Angle)); - - circ.add_vertex_with_edges(Op::RxF64, vec![qi, angle_edges[0]], vec![qo]); - - circ.add_vertex_with_edges( - Op::Copy { - n_copies: 2, - typ: WireType::Angle, - }, - vec![angle_edges[1]], - vec![angle_edges[2], angle_edges[3]], - ); - - circ.add_vertex_with_edges( - Op::Const(ConstValue::f64_angle(0.5)), - vec![], - vec![angle_edges[1]], - ); - circ.add_vertex_with_edges( - Op::Const(ConstValue::f64_angle(2.0)), - vec![], - vec![angle_edges[4]], - ); - circ.add_vertex_with_edges( - Op::Const(ConstValue::f64_angle(8.0)), - vec![], - vec![angle_edges[8]], - ); - circ.add_vertex_with_edges(Op::AngleNeg, vec![angle_edges[4]], vec![angle_edges[5]]); - circ.add_vertex_with_edges( - Op::AngleAdd, - vec![angle_edges[2], angle_edges[3]], - vec![angle_edges[6]], - ); - circ.add_vertex_with_edges( - Op::AngleAdd, - vec![angle_edges[5], angle_edges[6]], - vec![angle_edges[7]], - ); - circ.add_vertex_with_edges( - Op::AngleAdd, - vec![angle_edges[7], angle_edges[8]], - vec![angle_edges[0]], - ); - - assert_eq!(circ.dag.node_count(), 11); - assert_eq!(circ.dag.edge_count(), 11); - check_soundness(&circ).unwrap(); - - let orig_circ = circ.clone(); - let mut orig_circ2 = circ.clone(); - let rewrites: Vec<_> = find_const_ops(&circ).collect(); - - assert_eq!(rewrites.len(), 2); - - for rewrite in rewrites { - circ.apply_rewrite(rewrite).unwrap(); - } - check_soundness(&circ).unwrap(); - - assert_eq!(circ.dag.node_count(), 10); - assert_eq!(circ.dag.edge_count(), 9); - - assert_eq!( - circ.dag - .node_weights() - .filter(|v| matches!(v.op, Op::Const(_))) - .count(), - 4 - ); - - assert_eq!( - circ.dag - .node_weights() - .filter(|op| matches!(op.op, Op::AngleNeg)) - .count(), - 0 - ); - - assert_eq!( - circ.dag - .node_weights() - .filter(|op| matches!(op.op, Op::AngleAdd)) - .count(), - 3 - ); - - // evaluate all the additions - for _ in 0..3 { - let rewrites: Vec<_> = find_const_ops(&circ).collect(); - - assert_eq!(rewrites.len(), 1); - - circ.apply_rewrite(rewrites.into_iter().next().unwrap()) - .unwrap(); - } - check_soundness(&circ).unwrap(); - - let constant_folder = - |circuit| apply_exhaustive(circuit, |c| find_const_ops(c).collect()).unwrap(); - - // the above should replicate doing it all in one go - let (circ2, success) = constant_folder(orig_circ); - check_soundness(&circ2).unwrap(); - - assert!(success); - - let (circ, success) = constant_folder(circ); - check_soundness(&circ).unwrap(); - - assert!(!success); - - // println!("{}", orig_circ2.dot_string()); - - assert!(constant_fold_strat(&mut orig_circ2).unwrap()); - - for c in [circ, circ2, orig_circ2] { - let c = c.remove_invalid(); - assert_eq!(c.dag.node_count(), 4); - assert_eq!(c.dag.edge_count(), 3); - let const_val = c - .dag - .node_weights() - .find_map(|v| { - if let Op::Const(x) = &v.op { - Some(x) - } else { - None - } - }) - .unwrap(); - - assert_eq!(const_val, &ConstValue::f64_angle(7.0)); - } -} - -#[test] -fn test_rotation_replace() { - let mut circ = Circuit::new(); - let [input, output] = circ.boundary(); - - let point5 = circ.add_vertex(Op::Const(ConstValue::f64_angle(0.5))); - let rx = circ.add_vertex(Op::RxF64); - circ.add_insert_edge((input, 0), (rx, 0), WireType::Qubit) - .unwrap(); - circ.add_insert_edge((point5, 0), (rx, 1), WireType::Angle) - .unwrap(); - circ.add_insert_edge((rx, 0), (output, 0), WireType::Qubit) - .unwrap(); - - check_soundness(&circ).unwrap(); - - // let rot_replacer = - // |circuit| apply_exhaustive(circuit, |c| find_singleq_rotations(c).collect()).unwrap(); - - let rot_replacer = |circuit| { - apply_exhaustive(circuit, |c| find_singleq_rotations_pattern(c).collect()).unwrap() - }; - let (circ2, success) = rot_replacer(circ); - // println!("{}", circ2.dot_string()); - check_soundness(&circ2).unwrap(); - - assert!(success); - - let constant_folder = - |circuit| apply_exhaustive(circuit, |c| find_const_ops(c).collect()).unwrap(); - let (circ2, success) = constant_folder(circ2); - check_soundness(&circ2).unwrap(); - assert!(success); - // use portgraph::dot::dot_string; - // println!("{}", dot_string(&_circ2.dag)); -} - -#[test] -fn test_squash() { - let mut circ = Circuit::new(); - - let angle2 = circ.new_input(WireType::Angle); - let angle1 = circ.new_input(WireType::Angle); - let qi = circ.new_input(WireType::Qubit); - - let qint = circ.add_edge(WireType::Qubit); - - let qo = circ.new_output(WireType::Qubit); - - circ.add_vertex_with_edges(Op::RxF64, vec![qi, angle1], vec![qint]); - circ.add_vertex_with_edges(Op::RzF64, vec![qint, angle2], vec![qo]); - check_soundness(&circ).unwrap(); - - let rot_replacer = - |circuit| apply_exhaustive(circuit, |c| find_singleq_rotations(c).collect()).unwrap(); - let (circ2, success) = rot_replacer(circ); - - check_soundness(&circ2).unwrap(); - assert!(success); - // let squasher = - // |circuit| apply_exhaustive(circuit, |c| SquashFindIter::new(c).collect()).unwrap(); - let squasher = |circuit| apply_greedy(circuit, |c| squash_pattern(c).next()).unwrap(); - let (mut circ2, success) = squasher(circ2); - // println!("{}", circ2.dot_string()); - - assert!(success); - check_soundness(&circ2).unwrap(); - - circ2.bind_input(1, ConstValue::f64_angle(0.5)).unwrap(); - circ2.bind_input(1, ConstValue::f64_angle(0.2)).unwrap(); - - // use portgraph::dot::dot_string; - // println!("{}", dot_string(&circ2.dag)); - let constant_folder = - |circuit| apply_exhaustive(circuit, |c| find_const_ops(c).collect()).unwrap(); - let (circ2, success) = constant_folder(circ2); - - assert!(success); - check_soundness(&circ2).unwrap(); - - // let _circ2 = _circ2.remove_invalid(); - // use portgraph::dot::dot_string; - // println!("{}", dot_string(&_circ2.dag)); - // TODO verify behaviour at each step -} - -#[test] -fn test_cx_cancel() { - let qubits = vec![ - UnitID::Qubit { - reg_name: "q".into(), - index: vec![0], - }, - UnitID::Qubit { - reg_name: "q".into(), - index: vec![1], - }, - ]; - let mut circ = Circuit::with_uids(qubits); - circ.append_op(Op::CX, &[0, 1]).unwrap(); - circ.append_op(Op::CX, &[0, 1]).unwrap(); - check_soundness(&circ).unwrap(); + let hugr: Hugr = ser.decode().unwrap(); + let _circ = FlatRegionView::new(&hugr, hugr.root()); - let (circ, success) = cx_cancel_pass(circ); - assert!(success); + //assert_eq!(circ.qubits().len(), 3); - check_soundness(&circ).unwrap(); + //let mut coms = circ.to_commands(); + //coms.next(); // skip input + //let com = coms.next().unwrap(); + //assert!(matches!(com.op, &Op::Custom(_))); - // println!("{}", circ.dot_string()); - assert_eq!(circ.dag.node_count(), 2); + //let _reser: SerialCircuit = circ.into(); + //assert_eq!(&ser, &_reser); } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..f99bb147 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,16 @@ +//! Utility functions for the library. + +#[allow(dead_code)] +// Test only utils +#[cfg(test)] +pub(crate) mod test { + /// Open a browser page to render a dot string graph. + /// + /// This can be used directly on the output of `Hugr::dot_string` + #[cfg(not(ci_run))] + pub(crate) fn viz_dotstr(dotstr: &str) { + let mut base: String = "https://dreampuf.github.io/GraphvizOnline/#".into(); + base.push_str(&urlencoding::encode(dotstr)); + webbrowser::open(&base).unwrap(); + } +}