1use std::collections::BTreeMap;
4use std::fmt;
5
6use crate::{Bloq, BloqEdge, BloqNodeKind, ClassicalNode};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct BloqStats {
14 pub template_count: usize,
16 pub node_count: usize,
18 pub edge_count: usize,
20 pub node_counts: BTreeMap<&'static str, usize>,
22 pub edge_counts: BTreeMap<&'static str, usize>,
24 pub is_static: bool,
29}
30
31impl fmt::Display for BloqStats {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 writeln!(f, "Bloq IR")?;
34 writeln!(f, "├── Compiled Templates: {}", self.template_count)?;
35 for (label, count, types) in [
36 ("Nodes", self.node_count, &self.node_counts),
37 ("Edges", self.edge_count, &self.edge_counts),
38 ] {
39 writeln!(f, "├── {label}: {count}")?;
40 for (index, (kind, count)) in types.iter().enumerate() {
41 let branch = if index + 1 == types.len() {
42 "└──"
43 } else {
44 "├──"
45 };
46 writeln!(f, "│ {branch} {kind}: {count}")?;
47 }
48 }
49 write!(
50 f,
51 "└── Static: {}",
52 if self.is_static { "Yes" } else { "No" }
53 )
54 }
55}
56
57#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum BloqStatsError {
61 #[error("structural IR statistics count overflows usize")]
63 CountOverflow,
64}
65
66fn add(total: &mut usize, count: usize) -> Result<(), BloqStatsError> {
67 *total = total
68 .checked_add(count)
69 .ok_or(BloqStatsError::CountOverflow)?;
70 Ok(())
71}
72
73impl Bloq {
74 pub fn stats(&self) -> Result<BloqStats, BloqStatsError> {
89 let mut stats = BloqStats {
90 template_count: self.templates().len(),
91 node_count: 0,
92 edge_count: 0,
93 node_counts: [
94 "Quantum",
95 "Compute",
96 "Observable",
97 "Discard",
98 "RepeatUntilSuccess",
99 ]
100 .into_iter()
101 .map(|kind| (kind, 0))
102 .collect(),
103 edge_counts: [("Quantum", 0), ("Value", 0), ("Compose", 0), ("Order", 0)].into(),
104 is_static: true,
105 };
106 for (_, level) in self.levels() {
107 add(&mut stats.node_count, level.node_count())?;
108 add(&mut stats.edge_count, level.edge_count())?;
109 for (_, node) in level.nodes() {
110 stats.is_static &= node.activation.is_none();
111 stats.is_static &= match &node.kind {
112 BloqNodeKind::Quantum(quantum) => quantum.guards.is_empty(),
113 BloqNodeKind::Classical(classical) => {
114 !matches!(classical.as_ref(), ClassicalNode::Discard { .. })
115 }
116 BloqNodeKind::Region(_) => false,
117 };
118 add(stats.node_counts.entry(node.kind_name()).or_default(), 1)?;
119 }
120 for edge in level.edges() {
121 let kind = match edge.edge {
122 BloqEdge::Quantum(quantum) => {
123 stats.is_static &= quantum.guard.is_none();
124 "Quantum"
125 }
126 BloqEdge::Value { .. } => "Value",
127 BloqEdge::Compose { .. } => "Compose",
128 BloqEdge::Order => "Order",
129 };
130 add(stats.edge_counts.entry(kind).or_default(), 1)?;
131 }
132 }
133 Ok(stats)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::circuit::{CircuitBody, CoordCircuit, Op, PauliBasis};
141 use crate::lowering::{BloqTemplate, TemplateInstance, TemplateInstanceId};
142 use crate::{
143 BloqNode, ClassicalExpr, ObservableOutput, QuantumGuard, RegionNode, SubGraph, ValueRef,
144 };
145 use glam::ivec2;
146
147 #[test]
148 fn structural_stats_count_nested_kinds_without_inspecting_or_expanding_circuits() {
149 let mut circuit = CoordCircuit::new();
150 circuit.measure(PauliBasis::Z, [ivec2(1, 0)]);
151 let repeated = circuit.add_body(CircuitBody::new());
152 circuit
153 .body_mut(circuit.entry_body())
154 .unwrap()
155 .ops_mut()
156 .push(Op::Repeat {
157 body: repeated,
158 repetitions: u32::MAX,
159 });
160 let mut program = Bloq::new();
161 let template = program.add_template(BloqTemplate::new(circuit));
162 let mut body = SubGraph::new();
163 let mut node = BloqNode::from_members(vec![]);
164 node.expect_quantum_mut()
165 .instances
166 .push(TemplateInstance::new(
167 TemplateInstanceId(0),
168 template,
169 ivec2(i32::MAX, 0),
170 ));
171 let a = body.add_node(node);
172 let b = body.add_node(BloqNode::classical(ClassicalNode::Compute {
173 expr: ClassicalExpr::Const(false),
174 }));
175 body.add_edge(a, b, BloqEdge::Order);
176 body.add_edge(b, a, BloqEdge::value(0));
177 program.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
178 body,
179 restart_condition: ClassicalExpr::In(0),
180 restart_source: Some(ValueRef {
181 node: b,
182 output: ObservableOutput::Corrected,
183 }),
184 }));
185 let before = program.to_binary();
186 assert!(
187 program.qubit_count().is_err(),
188 "physical coordinate overflow does not block structural reads"
189 );
190 let stats = program.stats().unwrap();
191 assert_eq!(stats.node_count, 3);
192 assert_eq!(stats.node_counts.values().sum::<usize>(), stats.node_count);
193 assert_eq!(stats.node_counts["Compute"], 1);
194 assert_eq!(stats.node_counts["Quantum"], 1);
195 assert_eq!(stats.node_counts["RepeatUntilSuccess"], 1);
196 assert_eq!(stats.edge_count, 2);
197 assert_eq!(
198 stats.edge_counts,
199 [("Compose", 0), ("Order", 1), ("Quantum", 0), ("Value", 1)].into()
200 );
201 assert_eq!(stats.template_count, 1);
202 assert!(!stats.is_static);
203 assert_eq!(
204 stats.to_string(),
205 "Bloq IR\n├── Compiled Templates: 1\n├── Nodes: 3\n│ ├── Compute: 1\n│ ├── Discard: 0\n│ ├── Observable: 0\n│ ├── Quantum: 1\n│ └── RepeatUntilSuccess: 1\n├── Edges: 2\n│ ├── Compose: 0\n│ ├── Order: 1\n│ ├── Quantum: 0\n│ └── Value: 1\n└── Static: No"
206 );
207 assert_eq!(program.node_count(), 1, "existing count remains top-level");
208 assert_eq!(program.to_binary(), before, "statistics preserve stored IR");
209 let mut count = usize::MAX;
210 assert_eq!(add(&mut count, 1), Err(BloqStatsError::CountOverflow));
211 }
212
213 #[test]
214 fn static_classification_tracks_execution_structure_not_classical_values() {
215 let mut program = Bloq::new();
216 assert!(program.stats().unwrap().is_static);
217 let readout = program.add_node(BloqNode::classical(ClassicalNode::observable(0)));
218 for classical in [
219 ClassicalNode::Compute {
220 expr: ClassicalExpr::And(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
221 },
222 ClassicalNode::observable_fragment(Vec::new(), Vec::new()),
223 ] {
224 program.add_node(BloqNode::classical(classical));
225 }
226 assert!(
227 program.stats().unwrap().is_static,
228 "classical computation has fixed structure"
229 );
230 program.node_mut(readout).unwrap().activation = Some(0);
231 assert!(!program.stats().unwrap().is_static);
232 program.node_mut(readout).unwrap().activation = None;
233 let discard = program.add_node(BloqNode::classical(ClassicalNode::Discard {
234 condition: ClassicalExpr::Const(false),
235 }));
236 assert!(
237 !program.stats().unwrap().is_static,
238 "stored discard remains dynamic even with constant predicate"
239 );
240 program.remove_node(discard);
241 let a = program.add_node(BloqNode::from_members(vec![]));
242 let b = program.add_node(BloqNode::from_members(vec![]));
243 program.add_edge(a, b, BloqEdge::quantum(vec![]));
244 assert!(program.stats().unwrap().is_static);
245 program
246 .node_mut(a)
247 .unwrap()
248 .expect_quantum_mut()
249 .guards
250 .push(QuantumGuard::default());
251 assert!(!program.stats().unwrap().is_static);
252 program
253 .node_mut(a)
254 .unwrap()
255 .expect_quantum_mut()
256 .guards
257 .clear();
258 let mut guarded = BloqEdge::quantum(vec![]);
259 let BloqEdge::Quantum(edge) = &mut guarded else {
260 unreachable!()
261 };
262 edge.guard = Some(ValueRef {
263 node: readout,
264 output: ObservableOutput::Corrected,
265 });
266 program.add_edge(b, a, guarded);
267 assert!(!program.stats().unwrap().is_static);
268 program.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
269 restart_source: None,
270 restart_condition: ClassicalExpr::Const(false),
271 body: SubGraph::new(),
272 }));
273 assert_eq!(
274 program.stats().unwrap().node_counts["RepeatUntilSuccess"],
275 1
276 );
277 }
278}