diff --git a/Cargo.lock b/Cargo.lock index 60296a551929..fe707f7906f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "countme" version = "3.0.1" @@ -377,6 +386,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.4", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -399,6 +435,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8" +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "digest" version = "0.10.7" @@ -409,6 +466,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "drop_bomb" version = "0.1.5" @@ -937,6 +1003,21 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.28" @@ -990,6 +1071,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.1", +] + [[package]] name = "nalgebra" version = "0.33.2" @@ -1319,6 +1412,29 @@ dependencies = [ "xshell", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.0", +] + [[package]] name = "paste" version = "1.0.15" @@ -1677,6 +1793,7 @@ dependencies = [ "approx", "bitfield-struct", "bytemuck", + "crossterm", "hashbrown 0.15.5", "indexmap", "itertools 0.14.0", @@ -1998,6 +2115,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "regex" version = "1.12.2" @@ -2144,6 +2270,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "seq-macro" version = "0.3.6" @@ -2228,6 +2360,36 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "simba" version = "0.9.1" @@ -2509,6 +2671,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" @@ -2599,6 +2767,22 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2608,6 +2792,12 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.61.3" diff --git a/crates/cext/cbindgen.toml b/crates/cext/cbindgen.toml index e1963ff0a1ae..c14f6ade099d 100644 --- a/crates/cext/cbindgen.toml +++ b/crates/cext/cbindgen.toml @@ -47,3 +47,4 @@ prefix = "Qk" "CNeighbors" = "Neighbors" "COperationKind" = "OperationKind" "CDagNeighbors" = "DagNeighbors" +"CCircuitDrawerConfig" = "CircuitDrawerConfig" diff --git a/crates/cext/src/circuit.rs b/crates/cext/src/circuit.rs index 412c154b42ec..66f2a27d10d5 100644 --- a/crates/cext/src/circuit.rs +++ b/crates/cext/src/circuit.rs @@ -22,6 +22,7 @@ use num_complex::{Complex64, ComplexFloat}; use qiskit_circuit::bit::{ClassicalRegister, QuantumRegister}; use qiskit_circuit::bit::{ShareableClbit, ShareableQubit}; use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::circuit_drawer::draw_circuit; use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::operations::{ ArrayType, DelayUnit, Operation, Param, StandardGate, StandardInstruction, UnitaryGate, @@ -1215,3 +1216,81 @@ pub unsafe extern "C" fn qk_circuit_to_dag(circuit: *const CircuitData) -> *mut Box::into_raw(Box::new(dag)) } + +/// The configuration options for the ``qk_circuit_draw`` function. +#[repr(C)] +pub struct CCircuitDrawerConfig { + /// If `true`, bundles classical registers into single wires. + bundle_cregs: bool, + /// If `true`, merges the bottom and top lines of adjacent wires. + merge_wires: bool, + /// Sets the line length for wrapping the rendered text. Use 0 + /// to auto-detect console width. + fold: usize, +} + +/// @ingroup QkCircuit +/// Draw the circuit as text. +/// +/// @param circuit A pointer to the circuit to draw. +/// @param config A pointer to the ``QkCircuitDrawerConfig`` structure or NULL. If NULL, +/// the drawer will use these defaults: +/// * ``bundle_cregs = true`` +/// * ``merge_wires = true`` +/// * ``fold = 0`` +/// +/// @return A pointer to a null-terminated string containing the circuit representation. +/// You must use ``qk_str_free`` to release the allocated memory when done. +/// +/// # Example +/// ```c +/// QkCircuit *circuit = qk_circuit_new(2, 1); +/// +/// qk_circuit_gate(circuit, QkGate_H, (uint32_t[]){0}, NULL); +/// qk_circuit_gate(circuit, QkGate_CX, (uint32_t[]){0, 1}, NULL); +/// qk_circuit_measure(circuit, 0, 0); +/// qk_circuit_measure(circuit, 1, 0); +/// +/// QkCircuitDrawerConfig config = {false, true, 0}; +/// +/// char *circ_str = qk_circuit_draw(circuit, &config); +/// +/// printf("%s", circ_str); +/// +/// qk_str_free(circ_str); +/// qk_circuit_free(circuit); +/// ``` +/// +/// # Safety +/// +/// Behavior is undefined if ``circuit`` is not a valid, non-null pointer to a ``QkCircuit``, or +/// if ``config`` is not NULL and a non-valid pointer to a ``QkCircuitDrawerConfig`` struct. +#[unsafe(no_mangle)] +#[cfg(feature = "cbinding")] +pub unsafe extern "C" fn qk_circuit_draw( + circuit: *const CircuitData, + config: *const CCircuitDrawerConfig, +) -> *mut c_char { + // SAFETY: Per documentation, the pointer is non-null and aligned. + let circuit = unsafe { const_ptr_as_ref(circuit) }; + + let (bundle_cregs, merge_wires, fold) = if !config.is_null() { + // SAFETY: Per documentation, the pointer is to a valid QkCircuitDrawerConfig struct. + let config = unsafe { const_ptr_as_ref(config) }; + ( + config.bundle_cregs, + config.merge_wires, + if config.fold != 0 { + Some(config.fold) + } else { + None + }, + ) + } else { + (true, true, None) + }; + + let circuit_str = draw_circuit(circuit, bundle_cregs, merge_wires, fold).unwrap(); + + CString::new(circuit_str).unwrap().into_raw() +} diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 305ec615fbfe..a292721f11fd 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -27,6 +27,7 @@ nom.workspace = true nom-unicode.workspace = true nom-language.workspace = true num-bigint.workspace = true +crossterm = "0.29.0" [dependencies.pyo3] workspace = true diff --git a/crates/circuit/src/circuit_drawer.rs b/crates/circuit/src/circuit_drawer.rs new file mode 100644 index 000000000000..321c23084dbc --- /dev/null +++ b/crates/circuit/src/circuit_drawer.rs @@ -0,0 +1,1244 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::bit::{ClassicalRegister, ShareableClbit, ShareableQubit}; +use crate::circuit_data::CircuitData; +use crate::converters::QuantumCircuitData; +use crate::dag_circuit::DAGCircuit; +use crate::dag_circuit::NodeType; +use crate::operations::{Operation, OperationRef, Param, StandardGate, StandardInstruction}; +use crate::packed_instruction::PackedInstruction; +use crate::{Clbit, Qubit}; +use crossterm::terminal::size; +use hashbrown::{HashMap, HashSet}; +use itertools::{Itertools, MinMaxResult}; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use std::fmt::Debug; +use std::ops::Index; + +/// Draw the [CircuitData] object as string. +/// +/// # Arguments: +/// +/// * circuit: The CircuitData to draw. +/// * cregbundle: If true, classical bits of classical registers are bundled into one wire. +/// * mergewires: If true, adjacent wires are merged when rendered. +/// * fold: If not None, applies line wrapping using the specified amount. +/// +/// # Returns: +/// +/// The String representation of the circuit. +pub fn draw_circuit( + circuit: &CircuitData, + cregbundle: bool, + mergewires: bool, + fold: Option, +) -> PyResult { + let vis_mat = VisualizationMatrix::from_circuit(circuit, cregbundle)?; + + let text_drawer = TextDrawer::from_visualization_matrix(&vis_mat, cregbundle); + + let fold = match fold { + Some(f) => f, + None => { + let (term_width, _) = size().unwrap_or((80, 24)); + term_width as usize + } + }; + + Ok(text_drawer.draw(mergewires, fold)) +} + +/// Return a list of layers such that each layer contains a list of op node indices, representing instructions +/// whose qubits/clbits indices do not overlap. The instruction are packed into each layer as long as there +/// is no qubit/clbit overlap. +fn build_layers(dag: &DAGCircuit) -> Vec> { + let mut layers: Vec> = Vec::new(); + let mut current_layer: Option<&mut Vec> = None; + let mut used_wires = HashSet::::new(); + + for layer in dag.multigraph_layers() { + for node_index in layer.into_iter().sorted() { + if let NodeType::Operation(instruction_to_insert) = &dag.dag()[node_index] { + let (node_min, node_max) = get_instruction_range( + dag.get_qargs(instruction_to_insert.qubits), + dag.get_cargs(instruction_to_insert.clbits), + dag.num_qubits(), + ); + + // Check for instruction range overlap + if (node_min..=node_max).any(|idx| used_wires.contains(&idx)) { + current_layer = None; // Indication for starting a new layer + used_wires.clear(); + } + used_wires.extend(node_min..=node_max); + + if current_layer.is_none() { + layers.push(Vec::new()); + current_layer = layers.last_mut(); + } + + current_layer.as_mut().unwrap().push(node_index); + } + } + } + + layers +} + +/// Calculate the range (inclusive) of the given instruction qubits/clbits over the wire indices. +/// The assumption is that clbits always appear after the qubits in the visualization, hence the clbit indices +/// are offset by the number of instruction qubits when calculating the range. +fn get_instruction_range( + node_qubits: &[Qubit], + node_clbits: &[Clbit], + num_qubits: usize, +) -> (usize, usize) { + let indices = node_qubits + .iter() + .map(|q| q.index()) + .chain(node_clbits.iter().map(|c| c.index() + num_qubits)); + + match indices.minmax() { + MinMaxResult::MinMax(min, max) => (min, max), + MinMaxResult::OneElement(idx) => (idx, idx), + MinMaxResult::NoElements => panic!("Encountered an instruction without qubits and clbits"), + } +} + +#[derive(Clone, PartialEq, Eq)] +enum WireInputElement<'a> { + Qubit(&'a ShareableQubit), + Clbit(&'a ShareableClbit), + Creg(&'a ClassicalRegister), +} + +impl WireInputElement<'_> { + fn get_name(&self, circuit: &CircuitData) -> Option { + match self { + Self::Qubit(qubit) => { + let bit_info = circuit + .qubit_indices() + .get(qubit) + .expect("Bit should have location info"); + if !bit_info.registers().is_empty() { + let (register, index) = bit_info + .registers() + .first() + .expect("Register cannot be empty"); + if !register.is_empty() { + Some(format!("{}_{}: ", register.name(), index)) + } else { + Some(format!("{}: ", register.name())) + } + } else { + None + } + } + WireInputElement::Clbit(clbit) => { + let bit_info = circuit + .clbit_indices() + .get(clbit) + .expect("Bit should have location info"); + + if !bit_info.registers().is_empty() { + let (register, index) = bit_info + .registers() + .first() + .expect("Register cannot be empty"); + if !register.is_empty() { + Some(format!("{}_{}: ", register.name(), index)) + } else { + Some(format!("{}: ", register.name())) + } + } else { + None + } + } + WireInputElement::Creg(creg) => Some(format!("{}: {}/", creg.name(), creg.len())), + } + } +} + +/// Enum for representing elements that appear directly on a wire and how they're connected. +#[derive(Clone, Debug, Copy)] +enum OnWireElement<'a> { + Control(&'a PackedInstruction), + Swap(&'a PackedInstruction), + Barrier, + Reset, +} + +/// Represent elements that appear in a boxed operation. +#[derive(Clone)] +enum BoxedElement<'a> { + Single(&'a PackedInstruction), + Multi(&'a PackedInstruction), +} + +/// Enum for representing the elements stored in the visualization matrix. The elements +/// do not directly implement visualization capabilities, but rather carry enough information +/// to enable visualization later on by the actual drawer. +#[derive(Default, Clone)] +enum VisualizationElement<'a> { + #[default] + /// A wire element without any associated information. + Empty, + /// A Vertical line element, belonging to an instruction (e.g of a controlled gate or a measure). + VerticalLine(&'a PackedInstruction), + /// A circuit input element (qubit, clbit, creg). + Input(WireInputElement<'a>), + /// An element which is drawn without a surrounding box. Used only on qubit wires. + DirectOnWire(OnWireElement<'a>), + // A boxed element which can span one or more wires. Used only on qubit wires. + Boxed(BoxedElement<'a>), +} + +/// A representation of a single column (called here a layer) of a visualization matrix +#[derive(Clone)] +struct VisualizationLayer<'a>(Vec>); + +impl<'a> Index for VisualizationLayer<'a> { + type Output = VisualizationElement<'a>; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +impl<'a> VisualizationLayer<'a> { + fn len(&self) -> usize { + self.0.len() + } + + fn add_input(&mut self, input: WireInputElement<'a>, idx: usize) { + self.0[idx] = VisualizationElement::Input(input); + } + + /// Adds the required visualization elements to represent the given instruction + fn add_instruction( + &mut self, + cregbundle: bool, + inst: &'a PackedInstruction, + circuit: &CircuitData, + clbit_map: &[usize], + ) { + match inst.op.view() { + OperationRef::StandardGate(gate) => { + self.add_standard_gate(gate, inst, circuit); + } + OperationRef::StandardInstruction(std_inst) => { + self.add_standard_instruction(cregbundle, std_inst, inst, circuit, clbit_map); + } + OperationRef::Unitary(_) => { + self.add_unitary_gate(inst, circuit); + } + _ => unimplemented!( + "{}", + format!( + "Visualization is not implemented for instruction of type {:?}", + inst.op + ) + ), + } + } + + fn add_controls(&mut self, inst: &'a PackedInstruction, controls: &Vec) { + for control in controls { + self.0[*control] = VisualizationElement::DirectOnWire(OnWireElement::Control(inst)); + } + } + + fn add_vertical_lines(&mut self, vertical_lines: I, inst: &'a PackedInstruction) + where + I: Iterator, + { + for vline in vertical_lines { + self.0[vline] = VisualizationElement::VerticalLine(inst); + } + } + + fn add_standard_gate( + &mut self, + gate: StandardGate, + inst: &'a PackedInstruction, + circuit: &CircuitData, + ) { + let qargs = circuit.get_qargs(inst.qubits); + let (minima, maxima) = get_instruction_range(qargs, &[], 0); + + match gate { + StandardGate::ISwap + | StandardGate::DCX + | StandardGate::ECR + | StandardGate::RXX + | StandardGate::RYY + | StandardGate::RZZ + | StandardGate::RZX + | StandardGate::XXMinusYY + | StandardGate::XXPlusYY + | StandardGate::RCCX + | StandardGate::RC3X => { + for q in minima..=maxima { + self.0[q] = VisualizationElement::Boxed(BoxedElement::Multi(inst)); + } + } + StandardGate::H + | StandardGate::I + | StandardGate::X + | StandardGate::Y + | StandardGate::Z + | StandardGate::Phase + | StandardGate::R + | StandardGate::RX + | StandardGate::RY + | StandardGate::RZ + | StandardGate::S + | StandardGate::Sdg + | StandardGate::SX + | StandardGate::SXdg + | StandardGate::T + | StandardGate::Tdg + | StandardGate::U + | StandardGate::U1 + | StandardGate::U2 + | StandardGate::U3 => { + self.0[qargs[0].index()] = VisualizationElement::Boxed(BoxedElement::Single(inst)); + } + StandardGate::CH + | StandardGate::CX + | StandardGate::CY + | StandardGate::CZ + | StandardGate::CRX + | StandardGate::CRY + | StandardGate::CRZ + | StandardGate::CS + | StandardGate::CSdg + | StandardGate::CSX + | StandardGate::CU + | StandardGate::CU1 + | StandardGate::CU3 + | StandardGate::CCX + | StandardGate::CCZ + | StandardGate::C3X + | StandardGate::C3SX + | StandardGate::CPhase => { + self.0[qargs.last().unwrap().index()] = + VisualizationElement::Boxed(BoxedElement::Single(inst)); + if gate.num_ctrl_qubits() > 0 { + self.add_controls( + inst, + &qargs + .iter() + .take(qargs.len() - 1) + .map(|q| q.index()) + .collect(), + ); + } + + let vert_lines = (minima..=maxima) + .filter(|idx| !(qargs.iter().map(|q| q.0 as usize)).contains(idx)); + self.add_vertical_lines(vert_lines, inst); + } + StandardGate::GlobalPhase => {} + StandardGate::Swap | StandardGate::CSwap => { + // taking the last 2 elements of qargs + if gate == StandardGate::CSwap { + let control = vec![qargs[0].0 as usize]; + self.add_controls(inst, &control); + } + let swap_qubits = qargs.iter().map(|q| q.0 as usize).rev().take(2); + for qubit in swap_qubits { + self.0[qubit] = VisualizationElement::DirectOnWire(OnWireElement::Swap(inst)); + } + + let vert_lines = (minima..=maxima) + .filter(|idx| !(qargs.iter().map(|q| q.0 as usize)).contains(idx)); + self.add_vertical_lines(vert_lines, inst); + } + } + } + + fn add_standard_instruction( + &mut self, + cregbundle: bool, + std_inst: StandardInstruction, + inst: &'a PackedInstruction, + circuit: &CircuitData, + clbit_map: &[usize], + ) { + let qargs = circuit.get_qargs(inst.qubits); + let (minima, mut maxima) = + get_instruction_range(qargs, circuit.get_cargs(inst.clbits), circuit.num_qubits()); + + match std_inst { + StandardInstruction::Barrier(_) => { + for q in qargs { + self.0[q.index()] = VisualizationElement::DirectOnWire(OnWireElement::Barrier); + } + } + StandardInstruction::Reset => { + for q in qargs { + self.0[q.index()] = VisualizationElement::DirectOnWire(OnWireElement::Reset); + } + } + StandardInstruction::Measure => { + self.0[qargs.last().unwrap().index()] = + VisualizationElement::Boxed(BoxedElement::Single(inst)); + + // Some bits may be bundled, so we need to map the Clbit index to the proper wire index + if cregbundle { + maxima = clbit_map[circuit + .get_cargs(inst.clbits) + .first() + .expect("Measure should have a clbit arg") + .index()]; + } + self.add_vertical_lines(minima + 1..=maxima, inst); + } + StandardInstruction::Delay(_) => { + for q in qargs { + self.0[q.index()] = VisualizationElement::Boxed(BoxedElement::Single(inst)); + } + } + } + } + + fn add_unitary_gate(&mut self, inst: &'a PackedInstruction, circuit: &CircuitData) { + let qargs = circuit.get_qargs(inst.qubits); + if qargs.len() == 1 { + self.0[qargs.first().unwrap().index()] = + VisualizationElement::Boxed(BoxedElement::Single(inst)); + } else { + let (minima, maxima) = get_instruction_range(qargs, &[], 0); + + for q in minima..=maxima { + self.0[q] = VisualizationElement::Boxed(BoxedElement::Multi(inst)); + } + } + } +} + +/// A Plain, logical 2D representation of a circuit. +/// +/// A dense representation of the circuit of size N * (M + 1), where the first +/// layer(column) represents the qubits and clbits inputs in the circuits, and +/// M is the number of operation layers. +/// +/// This structure follows a column-major order, where each layer represents a column of the circuit, +#[derive(Clone)] +struct VisualizationMatrix<'a> { + /// Layers stored in the matrix. + layers: Vec>, + /// A reference to the circuit this matrix was constructed from. + circuit: &'a CircuitData, + /// A mapping from instruction's Clbit indices to the visualization matrix wires, + /// to be used when mapping clbits to bit of bundled cregs + clbit_map: Vec, +} + +impl<'a> VisualizationMatrix<'a> { + fn from_circuit(circuit: &'a CircuitData, bundle_cregs: bool) -> PyResult { + let dag = DAGCircuit::from_circuit_data(circuit, false, None, None, None, None)?; + + let mut node_index_to_inst: HashMap = + HashMap::with_capacity(circuit.data().len()); + for (idx, node_index) in dag.op_node_indices(true).enumerate() { + node_index_to_inst.insert(node_index, &circuit.data()[idx]); + } + + let inst_layers = build_layers(&dag); + + let num_wires = circuit.num_qubits() + + if !bundle_cregs { + circuit.num_clbits() + } else { + // Anonymous bits are not bundled so need to be counted explicitly + circuit.cregs_data().len() + circuit.num_clbits() + - circuit + .cregs_data() + .registers() + .iter() + .map(|r| r.len()) + .sum::() + }; + + let mut layers = vec![ + VisualizationLayer(vec![VisualizationElement::default(); num_wires]); + inst_layers.len() + 1 + ]; // Add 1 to account for the inputs layer + + let input_layer = layers.first_mut().unwrap(); + let mut input_idx = 0; + for qubit in circuit.qubits().objects() { + input_layer.add_input(WireInputElement::Qubit(qubit), input_idx); + input_idx += 1; + } + + let mut visited_cregs: HashSet<&ClassicalRegister> = HashSet::new(); + let mut clbit_map: Vec = Vec::new(); + for clbit in circuit.clbits().objects() { + if bundle_cregs { + let bit_location = circuit + .clbit_indices() + .get(clbit) + .expect("Bit should have bit info"); + if !bit_location.registers().is_empty() { + let creg = &bit_location + .registers() + .first() + .expect("Registers should not be empty") + .0; + + if visited_cregs.contains(creg) { + clbit_map.push(input_idx - 1); + } else { + input_layer.add_input(WireInputElement::Creg(creg), input_idx); + visited_cregs.insert(creg); + clbit_map.push(input_idx); + input_idx += 1; + } + continue; + } + } + + input_layer.add_input(WireInputElement::Clbit(clbit), input_idx); + clbit_map.push(input_idx); + input_idx += 1; + } + + for (i, layer) in inst_layers.iter().enumerate() { + for node_index in layer { + layers[i + 1].add_instruction( + bundle_cregs, + node_index_to_inst.get(node_index).unwrap(), + circuit, + &clbit_map, + ); + } + } + + Ok(VisualizationMatrix { + layers, + circuit, + clbit_map, + }) + } + + fn num_wires(&self) -> usize { + self.layers.first().map_or(0, |layer| layer.len()) + } + + fn num_layers(&self) -> usize { + self.layers.len() + } +} + +impl<'a> Index for VisualizationMatrix<'a> { + type Output = VisualizationLayer<'a>; + + fn index(&self, index: usize) -> &Self::Output { + &self.layers[index] + } +} + +impl Debug for VisualizationMatrix<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for w in 0..self.num_wires() { + for l in 0..self.num_layers() { + let element = &self[l][w]; + let label = match &element { + VisualizationElement::Empty => "~", + VisualizationElement::VerticalLine(inst) => { + let mut line = "|"; + if let Some(std_inst) = inst.op.try_standard_instruction() { + if std_inst == StandardInstruction::Measure { + line = "║"; + } + } + line + } + VisualizationElement::Input(input) => match input { + WireInputElement::Qubit(_) => "QR", + WireInputElement::Clbit(_) => "CR", + WireInputElement::Creg(_) => "C/", + }, + VisualizationElement::DirectOnWire(on_wire) => match on_wire { + OnWireElement::Barrier => "░", + OnWireElement::Control(_) => "■", + OnWireElement::Reset => "|0>", + OnWireElement::Swap(_) => "x", + }, + VisualizationElement::Boxed(_) => "[ ]", + }; + write!(f, "{:^5}", label)?; + } + writeln!(f)?; + } + Ok(()) + } +} + +// better name for the struct +#[derive(Clone)] +struct TextWireElement { + top: String, + mid: String, + bot: String, +} + +impl Debug for TextWireElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\n{}\n{}", self.top, self.mid, self.bot) + } +} + +impl TextWireElement { + fn width(&self) -> usize { + // return the max of all strings + // self.top.len().max(self.mid.len()).max(self.bot.len()) + let top = self.top.chars().count(); + let mid = self.mid.chars().count(); + let bot = self.bot.chars().count(); + if top >= mid && top >= bot { + top + } else if mid >= top && mid >= bot { + mid + } else { + bot + } + } + + fn left_pad_string(s: &mut String, pad_char: char, width: usize) { + let current_width = s.len(); + if current_width < width { + let pad_size = width - current_width; + let pad_str = pad_char.to_string().repeat(pad_size); + let new_str = format!("{}{}", pad_str, s); + *s = new_str; + } + } + + fn pad_string(s: &mut String, pad_char: char, width: usize) { + let current_width = s.chars().count(); + if current_width < width { + let pad_size = width - current_width; + let left_pad = pad_size / 2; + let right_pad = pad_size - left_pad; + let left_pad_str = pad_char.to_string().repeat(left_pad); + let right_pad_str = pad_char.to_string().repeat(right_pad); + let new_str = format!("{}{}{}", left_pad_str, s, right_pad_str); + *s = new_str; + } + } + + fn pad_wire_left(&mut self, mid_char: char, width: usize) { + let current_width = self.width(); + if current_width < width { + Self::left_pad_string(&mut self.top, ' ', width); + Self::left_pad_string(&mut self.mid, mid_char, width); + Self::left_pad_string(&mut self.bot, ' ', width); + } + } + + fn pad_wire(&mut self, mid_char: char, width: usize) { + let current_width = self.width(); + if current_width < width { + Self::pad_string(&mut self.top, ' ', width); + Self::pad_string(&mut self.mid, mid_char, width); + Self::pad_string(&mut self.bot, ' ', width); + } + } +} + +pub const Q_WIRE: char = '─'; +pub const C_WIRE: char = '═'; +pub const TOP_CON: char = '┴'; +pub const BOT_CON: char = '┬'; +pub const C_WIRE_CON_TOP: char = '╩'; +pub const C_BOT_CON: char = '╥'; +pub const Q_LEFT_CON: char = '┤'; +pub const Q_RIGHT_CON: char = '├'; +pub const CL_LEFT_CON: char = '╡'; +pub const CL_RIGHT_CON: char = '╞'; +pub const TOP_LEFT_BOX: char = '┌'; +pub const TOP_RIGHT_BOX: char = '┐'; +pub const BOT_LEFT_BOX: char = '└'; +pub const BOT_RIGHT_BOX: char = '┘'; +pub const BARRIER: char = '░'; +pub const BULLET: char = '■'; +pub const CONNECTING_WIRE: char = '│'; +pub const CL_CONNECTING_WIRE: char = '║'; +pub const Q_Q_CROSSED_WIRE: char = '┼'; +pub const Q_CL_CROSSED_WIRE: char = '╪'; +pub const CL_CL_CROSSED_WIRE: char = '╬'; +pub const CL_Q_CROSSED_WIRE: char = '╫'; + +/// Textual representation of the circuit +struct TextDrawer { + /// The array of textural wire elements corresponding to the visualization elements + wires: Vec>, +} + +impl Index for TextDrawer { + type Output = Vec; + + fn index(&self, index: usize) -> &Self::Output { + &self.wires[index] + } +} + +impl TextDrawer { + fn from_visualization_matrix(vis_mat: &VisualizationMatrix, cregbundle: bool) -> Self { + let mut text_drawer = TextDrawer { + wires: vec![Vec::new(); vis_mat.num_wires()], + }; + + for (i, layer) in vis_mat.layers.iter().enumerate() { + let layer_wires = Self::draw_layer(layer, vis_mat, cregbundle, i); + for (j, wire) in layer_wires.iter().enumerate() { + text_drawer.wires[j].push(wire.clone()); + } + } + text_drawer + } + + fn get_label(instruction: &PackedInstruction) -> String { + match instruction.op.view() { + OperationRef::StandardInstruction(std_instruction) => { + match std_instruction { + StandardInstruction::Measure => "M".to_string(), + StandardInstruction::Reset => "|0>".to_string(), + StandardInstruction::Barrier(_) => "░".to_string(), + StandardInstruction::Delay(delay_unit) => { + match instruction.params_view().first().unwrap() { + Param::Float(duration) => { + format!("Delay({}[{}])", duration, delay_unit) + } + Param::ParameterExpression(expr) => { + format!("Delay({}[{}])", expr, delay_unit) + } + Param::Obj(obj) => format!("Delay({:?}[{}])", obj, delay_unit), // TODO: extract the int + } + } + } + } + OperationRef::StandardGate(standard_gate) => { + let mut label = match standard_gate { + StandardGate::GlobalPhase => "", + StandardGate::H => "H", + StandardGate::I => "I", + StandardGate::X => "X", + StandardGate::Y => "Y", + StandardGate::Z => "Z", + StandardGate::Phase => "P", + StandardGate::R => "R", + StandardGate::RX => "Rx", + StandardGate::RY => "Ry", + StandardGate::RZ => "Rz", + StandardGate::S => "S", + StandardGate::Sdg => "Sdg", + StandardGate::SX => "√X", + StandardGate::SXdg => "√Xdg", + StandardGate::T => "T", + StandardGate::Tdg => "Tdg", + StandardGate::U => "U", + StandardGate::U1 => "U1", + StandardGate::U2 => "U2", + StandardGate::U3 => "U3", + StandardGate::CH => "H", + StandardGate::CX => "X", + StandardGate::CY => "Y", + StandardGate::CZ => "Z", + StandardGate::DCX => "Dcx", + StandardGate::ECR => "Ecr", + StandardGate::Swap => "", + StandardGate::ISwap => "Iswap", + StandardGate::CPhase => "P", + StandardGate::CRX => "Rx", + StandardGate::CRY => "Ry", + StandardGate::CRZ => "Rz", + StandardGate::CS => "S", + StandardGate::CSdg => "Sdg", + StandardGate::CSX => "Sx", + StandardGate::CU => "U", + StandardGate::CU1 => "U1", + StandardGate::CU3 => "U3", + StandardGate::RXX => "Rxx", + StandardGate::RYY => "Ryy", + StandardGate::RZZ => "Rzz", + StandardGate::RZX => "Rzx", + StandardGate::XXMinusYY => "XX-YY", + StandardGate::XXPlusYY => "XX+YY", + StandardGate::CCX => "X", + StandardGate::CCZ => "Z", + StandardGate::CSwap => "", + StandardGate::RCCX => "Rccx", + StandardGate::C3X => "X", + StandardGate::C3SX => "Sx", + StandardGate::RC3X => "Rcccx", + } + .to_string(); + + let custom_label = instruction.label(); + if custom_label.is_some() && custom_label.unwrap() != label { + label = custom_label.unwrap().to_string(); + } + if standard_gate.num_params() > 0 { + let params = instruction + .params_view() + .iter() + .map(|param| match param { + Param::Float(f) => format!("{}", f), + _ => format!("{:?}", param), + }) + .join(","); + label = format!("{}({})", label, params); + } + label + } + OperationRef::Unitary(_) => instruction.label().unwrap_or(" Unitary ").to_string(), + // Fallback for non-standard operations + _ => format!(" {} ", instruction.op.name()), + } + } + + fn get_layer_width(&self, ind: usize) -> usize { + self.wires + .iter() + .map(|wire| wire[ind].width()) + .max() + .unwrap_or(0) + } + + fn draw_layer( + layer: &VisualizationLayer, + vis_mat: &VisualizationMatrix, + cregbundle: bool, + layer_ind: usize, + ) -> Vec { + let mut wires: Vec = vec![]; + for (i, element) in layer.0.iter().enumerate() { + let wire = Self::draw_element(element, vis_mat, cregbundle, i); + wires.push(wire); + } + + let num_qubits = vis_mat.circuit.num_qubits(); + + let mut layer_width = 0; + for wire in wires.iter() { + let w = wire.width(); + if w > layer_width { + layer_width = w; + } + } + + for (i, wire) in wires.iter_mut().enumerate() { + if layer_ind == 0 { + wire.pad_wire_left(' ', layer_width); + } else if i < num_qubits { + wire.pad_wire(Q_WIRE, layer_width); + } else { + wire.pad_wire(C_WIRE, layer_width); + } + } + wires + } + + pub fn draw_element( + vis_ele: &VisualizationElement, + vis_mat: &VisualizationMatrix, + cregbundle: bool, + ind: usize, + ) -> TextWireElement { + let circuit = vis_mat.circuit; + let (top, mid, bot); + match vis_ele { + VisualizationElement::Boxed(sub_type) => { + // implement for cases where the box is on classical wires. The left and right connectors will change + // from single wired to double wired. + match sub_type { + BoxedElement::Single(inst) => { + let mut top_con = Q_WIRE; + let mut bot_con = Q_WIRE; + if let Some(gate) = inst.op.try_standard_gate() { + if gate.is_controlled_gate() { + let qargs = circuit.get_qargs(inst.qubits); + let (minima, maxima) = get_instruction_range(qargs, &[], 0); + if qargs.last().unwrap().index() > minima { + top_con = TOP_CON; + } + if qargs.last().unwrap().index() < maxima { + bot_con = BOT_CON; + } + } + } else if let Some(std_inst) = inst.op.try_standard_instruction() { + if std_inst == StandardInstruction::Measure { + bot_con = C_BOT_CON; + } + } + let label = format!(" {} ", Self::get_label(inst)); + let label_len = label.chars().count(); // To count unicode chars properly (e.g. in √X) + let left_len = (label_len - 1) / 2; + let right_len = label_len - left_len - 1; + top = format!( + "{}{}{}{}{}", + TOP_LEFT_BOX, + Q_WIRE.to_string().repeat(left_len), + top_con, + Q_WIRE.to_string().repeat(right_len), + TOP_RIGHT_BOX + ); + mid = format!("{}{}{}", Q_LEFT_CON, label, Q_RIGHT_CON); + bot = format!( + "{}{}{}{}{}", + BOT_LEFT_BOX, + Q_WIRE.to_string().repeat(left_len), + bot_con, + Q_WIRE.to_string().repeat(right_len), + BOT_RIGHT_BOX + ); + } + BoxedElement::Multi(inst) => { + let label = format!(" {} ", Self::get_label(inst)); + let label_len = label.chars().count(); // To count unicode chars properly (e.g. in √X) + let qargs = circuit.get_qargs(inst.qubits); + let (minima, maxima) = get_instruction_range(qargs, &[], 0); + let mid_idx = (minima + maxima) / 2; + + let num_affected = + if let Some(idx) = qargs.iter().position(|&x| x.index() == ind) { + format!("{:^width$}", idx, width = qargs.len()) + } else { + " ".to_string() + }; + + let mid_section = if ind == mid_idx { + format!( + "{:^total_q$}{:^label_len$}", + num_affected, + label, + total_q = qargs.len(), + label_len = label_len + ) + } else { + format!( + "{:^total_q$}{:^label_len$}", + num_affected, + " ", + total_q = qargs.len(), + label_len = label_len + ) + }; + + let left_len = (mid_section.len() - 1) / 2; + let right_len = mid_section.len() - left_len - 1; + top = if ind == minima { + format!( + "{}{}{}{}{}", + TOP_LEFT_BOX, + Q_WIRE.to_string().repeat(left_len), + Q_WIRE, + Q_WIRE.to_string().repeat(right_len), + TOP_RIGHT_BOX + ) + } else { + format!( + "{}{}{}", + CONNECTING_WIRE, + " ".repeat(mid_section.len()), + CONNECTING_WIRE + ) + }; + mid = format!("{}{}{}", Q_LEFT_CON, mid_section, Q_RIGHT_CON); + bot = if ind == maxima { + format!( + "{}{}{}{}{}", + BOT_LEFT_BOX, + Q_WIRE.to_string().repeat(left_len), + Q_WIRE, + Q_WIRE.to_string().repeat(right_len), + BOT_RIGHT_BOX + ) + } else { + format!( + "{}{}{}", + CONNECTING_WIRE, + " ".repeat(mid_section.len()), + CONNECTING_WIRE + ) + }; + } + } + } + VisualizationElement::DirectOnWire(on_wire) => { + let (wire_top, wire_symbol, wire_bot) = match on_wire { + OnWireElement::Control(inst) => { + let (minima, maxima) = + get_instruction_range(circuit.get_qargs(inst.qubits), &[], 0); + ( + if ind == minima { + " ".to_string() + } else { + CONNECTING_WIRE.to_string() + }, + BULLET.to_string(), + if ind == maxima { + " ".to_string() + } else { + CONNECTING_WIRE.to_string() + }, + ) + } + OnWireElement::Swap(inst) => { + let (minima, maxima) = + get_instruction_range(circuit.get_qargs(inst.qubits), &[], 0); + ( + if ind == minima { + " ".to_string() + } else { + CONNECTING_WIRE.to_string() + }, + "X".to_string(), + if ind == maxima { + " ".to_string() + } else { + CONNECTING_WIRE.to_string() + }, + ) + } + OnWireElement::Barrier => ( + BARRIER.to_string(), + BARRIER.to_string(), + BARRIER.to_string(), + ), + OnWireElement::Reset => { + (" ".to_string(), "|0>".to_string(), " ".to_string()) + } + }; + + top = format!(" {} ", wire_top); + mid = format!("{}{}{}", Q_WIRE, wire_symbol, Q_WIRE); + bot = format!(" {} ", wire_bot); + } + VisualizationElement::Input(input) => { + let input_name = input.get_name(circuit).unwrap_or_else(|| match input { + WireInputElement::Qubit(_) => format!("q_{}: ", ind), + WireInputElement::Clbit(_) => format!("c_{}: ", ind - circuit.num_qubits()), + WireInputElement::Creg(_) => unreachable!(), + }); + top = " ".repeat(input_name.len()); + bot = " ".repeat(input_name.len()); + mid = input_name; + } + VisualizationElement::VerticalLine(inst) => { + let is_measure = if let Some(std_inst) = inst.op.try_standard_instruction() { + std_inst == StandardInstruction::Measure + } else { + false + }; + + if is_measure { + top = CL_CONNECTING_WIRE.to_string(); + + let clbit = circuit.get_cargs(inst.clbits).first().unwrap(); + if ind == vis_mat.clbit_map[clbit.index()] { + mid = C_WIRE_CON_TOP.to_string(); + + let shareable_clbit = circuit.clbits().get(*clbit).unwrap(); + let registers = circuit + .clbit_indices() + .get(shareable_clbit) + .unwrap() + .registers(); + // TODO: if someone adds > 99 clbits + // the visualization will have an extra whitespace shift which + // needs to be fixed + bot = if cregbundle && !registers.is_empty() { + format!("{}", registers.first().unwrap().1) + } else { + " ".to_string() + } + } else { + bot = CL_CONNECTING_WIRE.to_string(); + mid = { + if ind < circuit.num_qubits() { + CL_Q_CROSSED_WIRE + } else { + CL_CL_CROSSED_WIRE + } + } + .to_string(); + } + } else { + top = CONNECTING_WIRE.to_string(); + bot = CONNECTING_WIRE.to_string(); + mid = { + if ind < circuit.num_qubits() { + Q_Q_CROSSED_WIRE + } else { + Q_CL_CROSSED_WIRE + } + } + .to_string(); + } + } + VisualizationElement::Empty => { + top = " ".to_string(); + bot = " ".to_string(); + mid = { + if ind < circuit.num_qubits() { + Q_WIRE + } else { + C_WIRE + } + } + .to_string(); + } + }; + TextWireElement { top, mid, bot } + } + + fn draw(&self, mergewires: bool, fold: usize) -> String { + // Calculate the layer ranges for each fold of the circuit + let num_layers = self.wires[0].len(); + // We skip the first (inputs) layer since it's printed for each fold, regardless + // of screen width limit + let layer_widths = (1..num_layers).map(|layer| self.get_layer_width(layer)); + let (mut start, mut current_fold) = (1usize, 0usize); + let mut ranges: Vec<(usize, usize)> = Vec::new(); + + for (i, layer_width) in layer_widths.enumerate() { + current_fold += layer_width; + if current_fold > fold { + ranges.push((start, i + 1)); + start = i + 1; + current_fold = layer_width; + } + } + ranges.push((start, num_layers)); + + let mut output = String::new(); + + for (start, end) in ranges { + let mut wire_strings: Vec = Vec::new(); + + for wire in &self.wires { + let top_line: String = wire[start..end] + .iter() + .map(|elem| elem.top.clone()) + .collect::>() + .join(""); + let mid_line: String = wire[start..end] + .iter() + .map(|elem| elem.mid.clone()) + .collect::>() + .join(""); + let bot_line: String = wire[start..end] + .iter() + .map(|elem| elem.bot.clone()) + .collect::>() + .join(""); + wire_strings.push(format!( + "{}{}{}{}", + if start > 1 { "«" } else { "" }, + wire[0].top, + top_line, + if end < num_layers - 1 { "»" } else { "" } + )); + wire_strings.push(format!( + "{}{}{}{}", + if start > 1 { "«" } else { "" }, + wire[0].mid, + mid_line, + if end < num_layers - 1 { "»" } else { "" } + )); + wire_strings.push(format!( + "{}{}{}{}", + if start > 1 { "«" } else { "" }, + wire[0].bot, + bot_line, + if end < num_layers - 1 { "»" } else { "" } + )); + } + for wire_idx in 0..wire_strings.len() { + if mergewires && wire_idx % 3 == 2 && wire_idx < wire_strings.len() - 3 { + // Merge the bot_line of the this wire with the top_line of the next wire + let merged_line = + Self::merge_lines(&wire_strings[wire_idx], &wire_strings[wire_idx + 1]); + output.push_str(&format!("{}\n", merged_line)); + } else if !mergewires || wire_idx % 3 != 0 || wire_idx == 0 { + // if mergewires, skip all top_line strings but the very first one + output.push_str(&format!("{}\n", wire_strings[wire_idx])); + } + } + } + + output + } + + pub fn merge_lines(top: &str, bot: &str) -> String { + let mut ret = String::new(); + + for (topc, botc) in top.chars().zip(bot.chars()) { + if topc == botc { + ret.push(topc); + } else if "┼╪".contains(topc) && botc == ' ' { + ret.push('│'); + } else if topc == ' ' { + ret.push(botc); + } else if "┬╥".contains(topc) && " ║│".contains(botc) { + ret.push(topc); + } else if "┬│".contains(topc) && botc == '═' { + ret.push('╪'); + } else if "┬│".contains(topc) && botc == '─' { + ret.push('┼'); + } else if "└┘║│░─═".contains(topc) && botc == ' ' { + ret.push(topc); + } else if "║╥".contains(topc) && botc == '═' { + ret.push('╬'); + } else if "║╥".contains(topc) && botc == '─' { + ret.push('╫'); + } else if "║╫╬".contains(topc) && botc == ' ' { + ret.push('║'); + } else if "│┼╪".contains(topc) && botc == ' ' { + ret.push('│'); + } else if topc == '└' && botc == '┌' { + ret.push('├'); + } else if topc == '┘' && botc == '┐' { + ret.push('┤'); + } else if "┐┌".contains(botc) { + ret.push('┬'); + } else if "┘└".contains(topc) && botc == '─' { + ret.push('┴'); + } else if botc == ' ' { + ret.push(topc); + } else { + ret.push(botc); + } + } + + ret + } +} + +#[pyfunction(name = "draw")] +#[pyo3(signature = (circuit, cregbundle=true, mergewires=true, fold=None))] +pub fn py_drawer( + circuit: QuantumCircuitData, + cregbundle: bool, + mergewires: bool, + fold: Option, +) -> PyResult { + draw_circuit(&circuit.data, cregbundle, mergewires, fold) +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 38ab85d79949..d1a5cff7d0d2 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -16,6 +16,7 @@ pub mod annotation; pub mod bit; pub mod bit_locator; pub mod circuit_data; +pub mod circuit_drawer; pub mod circuit_instruction; pub mod classical; pub mod converters; @@ -36,9 +37,8 @@ pub mod parameter_table; pub mod register_data; pub mod slice; pub mod util; -pub mod vf2; - mod variable_mapper; +pub mod vf2; use pyo3::PyTypeInfo; use pyo3::exceptions::PyValueError; @@ -256,6 +256,7 @@ pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_function(wrap_pyfunction!(circuit_drawer::py_drawer, m)?)?; let classical_mod = PyModule::new(m.py(), "classical")?; classical::register_python(&classical_mod)?; m.add_submodule(&classical_mod)?; diff --git a/docs/cdoc/qk-circuit.rst b/docs/cdoc/qk-circuit.rst index 6d5206e44082..089ef7136d92 100644 --- a/docs/cdoc/qk-circuit.rst +++ b/docs/cdoc/qk-circuit.rst @@ -59,6 +59,9 @@ Data Types .. doxygenstruct:: QkCircuitInstruction :members: +.. doxygenstruct:: QkCircuitDrawerConfig + :members: + Functions ========= diff --git a/releasenotes/notes/c-api-circuit-drawer-6d9888e4809cccd4.yaml b/releasenotes/notes/c-api-circuit-drawer-6d9888e4809cccd4.yaml new file mode 100644 index 000000000000..ddfe9e7f690c --- /dev/null +++ b/releasenotes/notes/c-api-circuit-drawer-6d9888e4809cccd4.yaml @@ -0,0 +1,7 @@ +--- +features_c: + - | + Added a new function, :c:func:`qk_circuit_draw`, to enable visualizing a quantum circuit + as text through the C API. The function uses :c:struct:`QkCircuitDrawerConfig` to configure + visualization options. Refer to the function documentation for more details. + diff --git a/test/c/test_circuit.c b/test/c/test_circuit.c index 15cbe33c88fc..679d35b47509 100644 --- a/test/c/test_circuit.c +++ b/test/c/test_circuit.c @@ -957,6 +957,35 @@ static int test_circuit_to_dag(void) { return result; } +/** + * A sanity check to ensure the circuit drawer handles all the supported operations. + */ +static int test_circuit_draw(void) { + QkCircuit *circuit = qk_circuit_new(10, 2); + + uint32_t qubits[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + double params[3] = {1.41, 2.71, 3.14}; + for (uint8_t gate = 0; gate <= QkGate_RC3X; ++gate) { + qk_circuit_gate(circuit, gate, &qubits[gate % 8], params); + } + qk_circuit_barrier(circuit, qubits, 10); + qk_circuit_delay(circuit, 3, 100.0, QkDelayUnit_NS); + qk_circuit_measure(circuit, 0, 0); + QkComplex64 c0 = {0, 0}; + QkComplex64 c1 = {1, 0}; + QkComplex64 unitary[2 * 2] = {c0, c1, c1, c0}; + qk_circuit_unitary(circuit, unitary, (uint32_t[]){5}, 1, true); + + QkCircuitDrawerConfig config = {false, true, 80}; + + char *circ_str = qk_circuit_draw(circuit, &config); + + qk_str_free(circ_str); + qk_circuit_free(circuit); + + return Ok; +} + int test_circuit(void) { int num_failed = 0; num_failed += RUN_TEST(test_empty); @@ -978,6 +1007,7 @@ int test_circuit(void) { num_failed += RUN_TEST(test_unitary_gate_1q); num_failed += RUN_TEST(test_unitary_gate_3q); num_failed += RUN_TEST(test_circuit_to_dag); + num_failed += RUN_TEST(test_circuit_draw); fflush(stderr); fprintf(stderr, "=== Number of failed subtests: %i\n", num_failed);