Skip to main content

bloq_ir/visualization/
mod.rs

1//! Standalone SVG views of the physical program DAG, using the editor's layout.
2//!
3//! Rendering preserves region bodies and does not instantiate templates, select
4//! guarded alternatives, execute the program, or perform a full validation audit.
5
6pub 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    /// Render the program dependency graph as a self-contained SVG.
15    ///
16    /// `include_classical` includes classical nodes and their incident edges.
17    /// When false, quantum nodes, regions and their direct dependencies remain.
18    /// Nested region bodies are drawn inside their owning container. Quantum, composition,
19    /// value and order edges retain distinct colors; this is a structural view
20    /// of all alternatives, not one selected runtime execution.
21    ///
22    /// ```
23    /// let program = bloq_ir::Bloq::new();
24    /// let svg = program.to_svg(true);
25    /// assert!(svg.starts_with("<svg"));
26    /// ```
27    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('&', "&amp;")
199        .replace('<', "&lt;")
200        .replace('>', "&gt;")
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 &lt; b &amp; 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}