microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
compiler/qsc_circuit/src/circuit.rs
346lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | #[cfg(test)] |
| 5 | mod tests; |
| 6 | |
| 7 | use rustc_hash::FxHashMap; |
| 8 | use serde::Serialize; |
| 9 | use std::{fmt::Display, fmt::Write, ops::Not, vec}; |
| 10 | |
| 11 | /// Representation of a quantum circuit. |
| 12 | /// Implementation of <https://github.com/microsoft/quantum-viz.js/wiki/API-schema-reference> |
| 13 | #[derive(Clone, Serialize, Default, Debug, PartialEq)] |
| 14 | pub struct Circuit { |
| 15 | pub operations: Vec<Operation>, |
| 16 | pub qubits: Vec<Qubit>, |
| 17 | } |
| 18 | |
| 19 | #[derive(Clone, Serialize, Debug, PartialEq)] |
| 20 | pub struct Operation { |
| 21 | #[allow(clippy::struct_field_names)] |
| 22 | pub gate: String, |
| 23 | #[serde(rename = "displayArgs")] |
| 24 | #[serde(skip_serializing_if = "Option::is_none")] |
| 25 | pub display_args: Option<String>, |
| 26 | #[serde(rename = "isControlled")] |
| 27 | #[serde(skip_serializing_if = "Not::not")] |
| 28 | pub is_controlled: bool, |
| 29 | #[serde(rename = "isAdjoint")] |
| 30 | #[serde(skip_serializing_if = "Not::not")] |
| 31 | pub is_adjoint: bool, |
| 32 | #[serde(rename = "isMeasurement")] |
| 33 | #[serde(skip_serializing_if = "Not::not")] |
| 34 | pub is_measurement: bool, |
| 35 | #[serde(skip_serializing_if = "Vec::is_empty")] |
| 36 | pub controls: Vec<Register>, |
| 37 | pub targets: Vec<Register>, |
| 38 | #[serde(skip_serializing_if = "Vec::is_empty")] |
| 39 | pub children: Vec<Operation>, |
| 40 | } |
| 41 | |
| 42 | const QUANTUM_REGISTER: usize = 0; |
| 43 | const CLASSICAL_REGISTER: usize = 1; |
| 44 | |
| 45 | #[derive(Serialize, Debug, Eq, Hash, PartialEq, Clone)] |
| 46 | pub struct Register { |
| 47 | #[serde(rename = "qId")] |
| 48 | pub q_id: usize, |
| 49 | pub r#type: usize, |
| 50 | #[serde(rename = "cId")] |
| 51 | #[serde(skip_serializing_if = "Option::is_none")] |
| 52 | pub c_id: Option<usize>, |
| 53 | } |
| 54 | |
| 55 | impl Register { |
| 56 | pub fn quantum(q_id: usize) -> Self { |
| 57 | Self { |
| 58 | q_id, |
| 59 | r#type: QUANTUM_REGISTER, |
| 60 | c_id: None, |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | pub fn classical(q_id: usize, c_id: usize) -> Self { |
| 65 | Self { |
| 66 | q_id, |
| 67 | r#type: CLASSICAL_REGISTER, |
| 68 | c_id: Some(c_id), |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | #[derive(PartialEq, Clone, Serialize, Debug)] |
| 74 | pub struct Qubit { |
| 75 | pub id: usize, |
| 76 | #[serde(rename = "numChildren")] |
| 77 | pub num_children: usize, |
| 78 | } |
| 79 | |
| 80 | #[derive(Clone, Debug, Copy, Default)] |
| 81 | pub struct Config { |
| 82 | /// Perform Base Profile decompositions |
| 83 | pub base_profile: bool, |
| 84 | } |
| 85 | |
| 86 | type ObjectsByColumn = FxHashMap<usize, String>; |
| 87 | |
| 88 | struct Row { |
| 89 | wire: Wire, |
| 90 | objects: ObjectsByColumn, |
| 91 | next_column: usize, |
| 92 | } |
| 93 | |
| 94 | enum Wire { |
| 95 | Qubit { q_id: usize }, |
| 96 | Classical { start_column: Option<usize> }, |
| 97 | } |
| 98 | |
| 99 | impl Row { |
| 100 | fn add_object(&mut self, column: usize, object: &str) { |
| 101 | match &mut self.wire { |
| 102 | Wire::Qubit { .. } => { |
| 103 | self.add(column, fmt_on_qubit_wire(object)); |
| 104 | } |
| 105 | Wire::Classical { .. } => { |
| 106 | self.add(column, fmt_on_classical_wire(object)); |
| 107 | } |
| 108 | }; |
| 109 | } |
| 110 | |
| 111 | fn add_gate(&mut self, column: usize, gate: &str, args: Option<&str>, is_adjoint: bool) { |
| 112 | let mut gate_label = String::new(); |
| 113 | gate_label.push_str(gate); |
| 114 | if is_adjoint { |
| 115 | gate_label.push('\''); |
| 116 | } |
| 117 | if let Some(args) = args { |
| 118 | let _ = write!(&mut gate_label, "({args})"); |
| 119 | } |
| 120 | self.add_object(column, &gate_label); |
| 121 | } |
| 122 | |
| 123 | fn add_vertical(&mut self, column: usize) { |
| 124 | if !self.objects.contains_key(&column) { |
| 125 | match self.wire { |
| 126 | Wire::Qubit { .. } => self.add(column, QUBIT_WIRE_CROSS), |
| 127 | Wire::Classical { start_column } => { |
| 128 | if start_column.is_some() { |
| 129 | self.add(column, CLASSICAL_WIRE_CROSS); |
| 130 | } else { |
| 131 | self.add(column, VERTICAL); |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | fn add_dashed_vertical(&mut self, column: usize) { |
| 139 | if !self.objects.contains_key(&column) { |
| 140 | match self.wire { |
| 141 | Wire::Qubit { .. } => self.add(column, QUBIT_WIRE_DASHED_CROSS), |
| 142 | Wire::Classical { start_column } => { |
| 143 | if start_column.is_some() { |
| 144 | self.add(column, CLASSICAL_WIRE_DASHED_CROSS); |
| 145 | } else { |
| 146 | self.add(column, VERTICAL_DASHED); |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | fn start_classical(&mut self, column: usize) { |
| 154 | self.add(column, CLASSICAL_WIRE_START); |
| 155 | if let Wire::Classical { start_column } = &mut self.wire { |
| 156 | start_column.replace(column); |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | fn add(&mut self, column: usize, v: impl Into<String>) { |
| 161 | self.objects.insert(column, v.into()); |
| 162 | self.next_column = column + 1; |
| 163 | } |
| 164 | |
| 165 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>, end_column: usize) -> std::fmt::Result { |
| 166 | // Temporary string so we can trim whitespace at the end |
| 167 | let mut s = String::new(); |
| 168 | match &self.wire { |
| 169 | Wire::Qubit { q_id: label } => { |
| 170 | s.write_str(&fmt_qubit_label(*label))?; |
| 171 | for column in 1..end_column { |
| 172 | let val = self.objects.get(&column); |
| 173 | if let Some(v) = val { |
| 174 | s.write_str(v)?; |
| 175 | } else { |
| 176 | s.write_str(QUBIT_WIRE)?; |
| 177 | } |
| 178 | } |
| 179 | } |
| 180 | Wire::Classical { start_column } => { |
| 181 | for column in 0..end_column { |
| 182 | let val = self.objects.get(&column); |
| 183 | if let Some(v) = val { |
| 184 | s.write_str(v)?; |
| 185 | } else if start_column.map_or(false, |s| column > s) { |
| 186 | s.write_str(CLASSICAL_WIRE)?; |
| 187 | } else { |
| 188 | s.write_str(BLANK)?; |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | writeln!(f, "{}", s.trim_end())?; |
| 194 | Ok(()) |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | const COLUMN_WIDTH: usize = 7; |
| 199 | const QUBIT_WIRE: &str = "───────"; |
| 200 | const CLASSICAL_WIRE: &str = "═══════"; |
| 201 | const QUBIT_WIRE_CROSS: &str = "───┼───"; |
| 202 | const CLASSICAL_WIRE_CROSS: &str = "═══╪═══"; |
| 203 | const CLASSICAL_WIRE_START: &str = " ╘═══"; |
| 204 | const QUBIT_WIRE_DASHED_CROSS: &str = "───┆───"; |
| 205 | const CLASSICAL_WIRE_DASHED_CROSS: &str = "═══┆═══"; |
| 206 | const VERTICAL_DASHED: &str = " ┆ "; |
| 207 | const VERTICAL: &str = " │ "; |
| 208 | const BLANK: &str = " "; |
| 209 | |
| 210 | /// "q_0 " |
| 211 | #[allow(clippy::doc_markdown)] |
| 212 | fn fmt_qubit_label(id: usize) -> String { |
| 213 | let rest = COLUMN_WIDTH - 2; |
| 214 | format!("q_{id: <rest$}") |
| 215 | } |
| 216 | |
| 217 | /// "── A ──" |
| 218 | fn fmt_on_qubit_wire(obj: &str) -> String { |
| 219 | format!("{:─^COLUMN_WIDTH$}", format!(" {obj} ")) |
| 220 | } |
| 221 | |
| 222 | /// "══ A ══" |
| 223 | fn fmt_on_classical_wire(obj: &str) -> String { |
| 224 | format!("{:═^COLUMN_WIDTH$}", format!(" {obj} ")) |
| 225 | } |
| 226 | |
| 227 | impl Display for Circuit { |
| 228 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 229 | let mut rows = vec![]; |
| 230 | |
| 231 | // Maintain a mapping from from Registers in the Circuit schema |
| 232 | // to row in the diagram |
| 233 | let mut register_to_row = FxHashMap::default(); |
| 234 | |
| 235 | // Initialize all qubit and classical wires |
| 236 | for q in &self.qubits { |
| 237 | rows.push(Row { |
| 238 | wire: Wire::Qubit { q_id: q.id }, |
| 239 | objects: FxHashMap::default(), |
| 240 | next_column: 1, |
| 241 | }); |
| 242 | |
| 243 | register_to_row.insert((q.id, None), rows.len() - 1); |
| 244 | |
| 245 | for i in 0..q.num_children { |
| 246 | rows.push(Row { |
| 247 | wire: Wire::Classical { start_column: None }, |
| 248 | objects: FxHashMap::default(), |
| 249 | next_column: 1, |
| 250 | }); |
| 251 | |
| 252 | register_to_row.insert((q.id, Some(i)), rows.len() - 1); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | for o in &self.operations { |
| 257 | // Row indexes for the targets for this operation |
| 258 | let targets = o |
| 259 | .targets |
| 260 | .iter() |
| 261 | .filter_map(|reg| { |
| 262 | let reg = (reg.q_id, reg.c_id); |
| 263 | register_to_row.get(®).cloned() |
| 264 | }) |
| 265 | .collect::<Vec<_>>(); |
| 266 | |
| 267 | // Row indexes for the controls for this operation |
| 268 | let controls = o |
| 269 | .controls |
| 270 | .iter() |
| 271 | .filter_map(|reg| { |
| 272 | let reg = (reg.q_id, reg.c_id); |
| 273 | register_to_row.get(®).cloned() |
| 274 | }) |
| 275 | .collect::<Vec<_>>(); |
| 276 | |
| 277 | let mut all_rows = targets.clone(); |
| 278 | all_rows.extend(controls.iter()); |
| 279 | all_rows.sort_unstable(); |
| 280 | // We'll need to know the entire range of rows for this operation so we can |
| 281 | // figure out the starting column and also so we can draw any |
| 282 | // vertical lines that cross wires. |
| 283 | let (begin, end) = all_rows.split_first().map_or((0, 0), |(first, tail)| { |
| 284 | (*first, tail.last().unwrap_or(first) + 1) |
| 285 | }); |
| 286 | |
| 287 | // The starting column - the first available column in all |
| 288 | // the rows that this operation spans. |
| 289 | let mut column = 1; |
| 290 | for row in &rows[begin..end] { |
| 291 | if row.next_column > column { |
| 292 | column = row.next_column; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | // Add the operation to the diagram |
| 297 | for i in targets { |
| 298 | let row = &mut rows[i]; |
| 299 | if matches!(row.wire, Wire::Classical { .. }) && o.is_measurement { |
| 300 | row.start_classical(column); |
| 301 | } else { |
| 302 | row.add_gate(column, &o.gate, o.display_args.as_deref(), o.is_adjoint); |
| 303 | }; |
| 304 | } |
| 305 | |
| 306 | if o.is_controlled || o.is_measurement { |
| 307 | for i in controls { |
| 308 | let row = &mut rows[i]; |
| 309 | if matches!(row.wire, Wire::Qubit { .. }) && o.is_measurement { |
| 310 | row.add_object(column, "M"); |
| 311 | } else { |
| 312 | row.add_object(column, "●"); |
| 313 | }; |
| 314 | } |
| 315 | |
| 316 | // If we have a control wire, draw vertical lines spanning all |
| 317 | // control and target wires and crossing any in between |
| 318 | // (vertical lines may overlap if there are multiple controls/targets, |
| 319 | // this is ok in practice) |
| 320 | for row in &mut rows[begin..end] { |
| 321 | row.add_vertical(column); |
| 322 | } |
| 323 | } else { |
| 324 | // No control wire. Draw dashed vertical lines to connect |
| 325 | // target wires if there are multiple targets |
| 326 | for row in &mut rows[begin..end] { |
| 327 | row.add_dashed_vertical(column); |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | // Find the end column for the whole circuit so that |
| 333 | // all qubit wires will extend until the end |
| 334 | let end_column = rows |
| 335 | .iter() |
| 336 | .max_by_key(|r| r.next_column) |
| 337 | .map_or(1, |r| r.next_column); |
| 338 | |
| 339 | // Draw the diagram |
| 340 | for row in rows { |
| 341 | row.fmt(f, end_column)?; |
| 342 | } |
| 343 | |
| 344 | Ok(()) |
| 345 | } |
| 346 | } |
| 347 | |