1pub mod layout;
7
8use std::collections::HashMap;
9use std::fmt::Write;
10
11use crate::{Bloq, BloqEdge, BloqNode, BloqNodeId, ClassicalNode, NodeProvenance, SubGraph};
12
13impl Bloq {
14 pub fn to_svg(&self, include_classical: bool) -> String {
28 let mut rendered = render_level(self.top(), include_classical);
29 rendered.width = rendered.width.max(430.0);
30 write!(rendered.svg, "<g transform=\"translate(0 {})\"><text style=\"fill:#4a65b8\">Quantum</text><text x=\"100\" style=\"fill:#b788ee\">Compose</text><text x=\"210\" style=\"fill:#4b8c67\">Value</text><text x=\"280\" style=\"fill:#758198\">Order (dashed)</text></g>", rendered.height + 28.0).expect("write SVG legend");
31 rendered.height += 40.0;
32 format!(
33 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\" role=\"img\" aria-label=\"Bloq IR dependency graph\"><style>text{{font-family:system-ui,sans-serif;font-size:13px;fill:#202536}}.region{{fill:#f7f8fc;stroke:#758198}}.quantum{{fill:#e6edff;stroke:#4a65b8}}.classical{{fill:#e8f6ed;stroke:#4b8c67}}</style><defs>{}</defs><rect width=\"100%\" height=\"100%\" fill=\"white\"/><g transform=\"translate(24 24)\">{}</g></svg>",
34 rendered.width + 48.0,
35 rendered.height + 48.0,
36 rendered.width + 48.0,
37 rendered.height + 48.0,
38 ["quantum", "compose", "value", "order"].into_iter().zip(["#4a65b8", "#b788ee", "#4b8c67", "#758198"]).map(|(kind, color)| format!("<marker id=\"{kind}\" markerWidth=\"7\" markerHeight=\"7\" refX=\"6\" refY=\"3\" orient=\"auto\"><path d=\"M0 0 L6 3 L0 6 Z\" fill=\"{color}\"/></marker>")).collect::<String>(),
39 rendered.svg,
40 )
41 }
42}
43
44struct RenderedLevel {
45 width: f32,
46 height: f32,
47 svg: String,
48}
49
50fn render_level(level: &SubGraph, include_classical: bool) -> RenderedLevel {
51 let nodes = level
52 .nodes()
53 .filter(|(_, node)| include_classical || node.try_classical().is_none())
54 .collect::<Vec<_>>();
55 let mut bodies = HashMap::new();
56 let mut sizes = Vec::new();
57 for (id, node) in &nodes {
58 let mut width = 190.0_f32.max(node_label(node).len() as f32 * 8.0 + 60.0);
59 if let Some(detail) = node_detail(level, *id, node) {
60 width = width.max(detail.len() as f32 * 8.0 + 24.0);
61 }
62 let mut height = 60.0;
63 if let Some(region) = node.try_region() {
64 let mut children = Vec::new();
65 for (_, body) in region.bodies() {
66 let child = render_level(body, include_classical);
67 width = width.max(child.width + 32.0);
68 height += child.height + 24.0;
69 children.push(child);
70 }
71 bodies.insert(id.0, children);
72 }
73 sizes.push((id.0, (width, height)));
74 }
75 let size_of = sizes.iter().copied().collect::<HashMap<_, _>>();
76 let edges = level
77 .edges()
78 .filter(|edge| size_of.contains_key(&edge.source.0) && size_of.contains_key(&edge.target.0))
79 .collect::<Vec<_>>();
80 let pairs = edges
81 .iter()
82 .map(|edge| (edge.source.0, edge.target.0))
83 .collect::<Vec<_>>();
84 let placed = layout::compute_relative(&sizes, &pairs, 40.0);
85 let mut svg = String::new();
86 for (index, edge) in edges.into_iter().enumerate() {
87 let (kind, color, dash) = match edge.edge {
88 BloqEdge::Quantum(_) => ("quantum", "#4a65b8", ""),
89 BloqEdge::Compose { .. } => ("compose", "#b788ee", ""),
90 BloqEdge::Value { .. } => ("value", "#4b8c67", ""),
91 BloqEdge::Order => ("order", "#758198", " stroke-dasharray=\"4 3\""),
92 };
93 let mut path = String::new();
94 for (segment, &[start, first, second, end]) in placed.routes[index].iter().enumerate() {
95 if segment == 0 {
96 write!(path, "M{} {} ", start.0, start.1).expect("write path start");
97 }
98 write!(
99 path,
100 "C{} {} {} {} {} {} ",
101 first.0, first.1, second.0, second.1, end.0, end.1
102 )
103 .expect("write routed curve");
104 }
105 write!(svg, "<path data-edge=\"{kind}\" d=\"{path}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"1.5\"{dash} marker-end=\"url(#{kind})\"/>").expect("write SVG string");
106 }
107 for (id, node) in nodes {
108 let (x, y) = placed.positions[&id.0];
109 let (w, h) = size_of[&id.0];
110 let kind = if node.try_region().is_some() {
111 "region"
112 } else if node.try_quantum().is_some() {
113 "quantum"
114 } else {
115 "classical"
116 };
117 write!(svg, "<g data-node=\"{}\"><rect class=\"{kind}\" x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" rx=\"7\"/><text x=\"{}\" y=\"{}\">N{} · {}</text>", id.0, x + 12.0, y + 23.0, id.0, escape_text(&node_label(node))).expect("write SVG string");
118 if let Some(quantum) = node.try_quantum() {
119 write!(
120 svg,
121 "<text x=\"{}\" y=\"{}\">{} instances · {} guards</text>",
122 x + 12.0,
123 y + 44.0,
124 quantum.instances.len(),
125 quantum.guards.len()
126 )
127 .expect("write SVG string");
128 }
129 if let Some(detail) = node_detail(level, id, node) {
130 write!(
131 svg,
132 "<text x=\"{}\" y=\"{}\">{}</text>",
133 x + 12.0,
134 y + 44.0,
135 escape_text(&detail)
136 )
137 .expect("write SVG provenance");
138 }
139 if let Some(children) = bodies.remove(&id.0) {
140 let mut top = y + 44.0;
141 for child in children {
142 write!(
143 svg,
144 "<g transform=\"translate({} {top})\">{}</g>",
145 x + 16.0,
146 child.svg
147 )
148 .expect("write SVG string");
149 top += child.height + 24.0;
150 }
151 }
152 svg.push_str("</g>");
153 }
154 RenderedLevel {
155 width: placed.content.0.max(190.0),
156 height: placed.content.1.max(60.0),
157 svg,
158 }
159}
160
161fn node_label(node: &BloqNode) -> String {
162 match &node.provenance {
163 NodeProvenance::OutputFrame { basis, .. } => {
164 format!("{} · Output frame {basis:?}", node.kind_name())
165 }
166 NodeProvenance::BranchSelector { .. } => format!("{} · Branch selector", node.kind_name()),
167 _ => match node.try_classical() {
168 Some(ClassicalNode::Observable { index: None, .. }) => "Observable fragment".into(),
169 Some(ClassicalNode::Observable {
170 index: Some(index), ..
171 }) => format!("Observable {index}"),
172 _ => node.kind_name().into(),
173 },
174 }
175}
176
177fn node_detail(level: &SubGraph, id: BloqNodeId, node: &BloqNode) -> Option<String> {
178 node.try_classical()?;
179 let mut parts = Vec::new();
180 if let Some(slot) = node.activation {
181 parts.push(
182 level
183 .value_inputs(id)
184 .find(|input| input.slot == slot)
185 .map_or_else(
186 || format!("when input {slot}"),
187 |input| format!("when N{}", input.producer.0),
188 ),
189 );
190 }
191 if node.provenance != NodeProvenance::None {
192 parts.push(node.provenance.to_string());
193 }
194 (!parts.is_empty()).then(|| parts.join(" · "))
195}
196
197fn escape_text(text: &str) -> String {
198 text.replace('&', "&")
199 .replace('<', "<")
200 .replace('>', ">")
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::{ClassicalExpr, QuantumNode, RegionNode};
207
208 #[test]
209 fn semantic_roles_preserve_node_kinds_and_escape_source_names() {
210 let mut program = Bloq::new();
211 for basis in [crate::Basis::X, crate::Basis::Z] {
212 let node = BloqNode::classical(ClassicalNode::Compute {
213 expr: ClassicalExpr::Const(false),
214 })
215 .with_provenance(NodeProvenance::OutputFrame {
216 port: glam::ivec3(1, 2, 3),
217 basis,
218 });
219 program.add_node(node);
220 }
221 program.add_node(
222 BloqNode::classical(ClassicalNode::Compute {
223 expr: ClassicalExpr::Const(false),
224 })
225 .with_provenance(NodeProvenance::BranchSelector {
226 name: "a < b & c".into(),
227 }),
228 );
229 let svg = program.to_svg(true);
230 assert!(svg.contains("Compute · Output frame X"));
231 assert!(svg.contains("Compute · Output frame Z"));
232 assert!(svg.contains("frame x (1,2,3)"));
233 assert!(svg.contains("Compute · Branch selector"));
234 assert!(svg.contains("selector a < b & c"));
235 assert!(!svg.contains("selector a < b & c"));
236 assert!(!program.to_svg(false).contains("Output frame"));
237 }
238
239 #[test]
240 fn classical_flag_filters_nodes_and_edges_without_dropping_regions() {
241 let mut body = SubGraph::new();
242 let q0 = body.add_node(BloqNode::quantum(QuantumNode::default()));
243 let q1 = body.add_node(BloqNode::quantum(QuantumNode::default()));
244 body.add_edge(q0, q1, BloqEdge::quantum(Vec::new()));
245 let first = body.add_node(BloqNode::classical(ClassicalNode::Compute {
246 expr: ClassicalExpr::Const(true),
247 }));
248 let second = body.add_node(BloqNode::classical(ClassicalNode::Compute {
249 expr: ClassicalExpr::In(0),
250 }));
251 body.add_edge(first, second, BloqEdge::value(0));
252 let fragment = body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
253 Vec::new(),
254 Vec::new(),
255 )));
256 let observable = body.add_node(BloqNode::classical(ClassicalNode::observable(0)));
257 body.add_edge(fragment, observable, BloqEdge::compose(0));
258 let mut program = Bloq::new();
259 program.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
260 restart_source: None,
261 restart_condition: ClassicalExpr::Const(false),
262 body,
263 }));
264 let full = program.to_svg(true);
265 assert!(full.contains("RepeatUntilSuccess"));
266 assert!(full.contains("Compute"));
267 assert!(full.contains("data-edge=\"value\""));
268 assert!(full.contains("data-edge=\"compose\""));
269 assert!(full.contains("marker id=\"compose\""));
270 let quantum = program.to_svg(false);
271 assert!(quantum.contains("RepeatUntilSuccess"));
272 assert!(quantum.contains("Quantum"));
273 assert!(quantum.contains("data-edge=\"quantum\""));
274 assert!(!quantum.contains("Compute"));
275 assert!(!quantum.contains("data-edge=\"value\""));
276 assert!(!quantum.contains("data-edge=\"compose\""));
277 assert_eq!(full, program.to_svg(true));
278 }
279}