diff --git a/Cargo.lock b/Cargo.lock index 029f708..c2a4081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1065,7 +1065,7 @@ dependencies = [ [[package]] name = "qsharp-bridge" -version = "0.2.0" +version = "0.2.1" dependencies = [ "expect-test", "num-bigint", diff --git a/Cargo.toml b/Cargo.toml index 2e0275d..64492c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qsharp-bridge" -version = "0.2.0" +version = "0.2.1" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/examples/python/jupyter/quantikz.ipynb b/examples/python/jupyter/quantikz.ipynb index 80e56ef..5e38c5b 100644 --- a/examples/python/jupyter/quantikz.ipynb +++ b/examples/python/jupyter/quantikz.ipynb @@ -6,7 +6,7 @@ "source": [ "First, import the library.\n", "\n", - "You will also need to have LaTeX installed on your system for rendering the diagrams. We also import official Q# library as we will use it for comparison of the generated diagrams." + "We will also need to have LaTeX installed on your system for rendering the diagrams. We also import official Q# library as we will use it for comparison of the generated diagrams." ] }, { @@ -29,7 +29,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, load the Jupyter TikZ extension. " + "Next, let's load the Jupyter TikZ extension. " ] }, { @@ -45,7 +45,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now you can write some Q# code. This will be the Q# code for which we will generate a circuit diagram." + "Now we can write some Q# code. This will be the Q# code for which we will generate a circuit diagram. We will set up two test code snippets - one that consists of a single operation, and another that has an operation calling another operation." ] }, { @@ -55,6 +55,23 @@ "outputs": [], "source": [ "code_1 = \"\"\"\n", + "operation Main() : Result[] {\n", + " use qubits = Qubit[8];\n", + "\n", + " // apply Hadamard gate to the first qubit\n", + " H(qubits[0]);\n", + "\n", + " // apply CNOT gates to create entanglement\n", + " for qubit in qubits[1..Length(qubits) - 1] {\n", + " CNOT(qubits[0], qubit);\n", + " }\n", + "\n", + " // return measurement results\n", + " MResetEachZ(qubits)\n", + "}\n", + "\"\"\"\n", + "\n", + "code_2 = \"\"\"\n", "namespace MyQuantumApp {\n", " @EntryPoint()\n", " operation Run() : (Result, Result) {\n", @@ -71,23 +88,6 @@ " CNOT(q1, q2);\n", " }\n", "}\n", - "\"\"\"\n", - "\n", - "code_2 = \"\"\"\n", - "operation Main() : Result[] {\n", - " use qubits = Qubit[8];\n", - "\n", - " // apply Hadamard gate to the first qubit\n", - " H(qubits[0]);\n", - "\n", - " // apply CNOT gates to create entanglement\n", - " for qubit in qubits[1..Length(qubits) - 1] {\n", - " CNOT(qubits[0], qubit);\n", - " }\n", - "\n", - " // return measurement results\n", - " MResetEachZ(qubits)\n", - "}\n", "\"\"\"" ] }, @@ -95,7 +95,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Once the Q# code is written, you can use the `quantikz` function to generate and display the circuit diagram directly in the Jupyter notebook." + "Once the Q# code is written, we can use the `quantikz` function to generate and display the circuit diagram directly in the Jupyter notebook." ] }, { @@ -104,8 +104,8 @@ "metadata": {}, "outputs": [], "source": [ - "quantikz_diagram_1 = quantikz(code_1)\n", - "quantikz_diagram_2 = quantikz(code_2)\n", + "quantikz_diagram_1 = quantikz(code_1, options=QuantikzGenerationOptions(group_by_scope=False))\n", + "quantikz_diagram_2 = quantikz(code_2, options=QuantikzGenerationOptions(group_by_scope=False))\n", "\n", "# for debugging, display the generated LaTeX code\n", "print(quantikz_diagram_1)\n", @@ -116,7 +116,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Use the TikZ extension to render a PDF from the LaTeX code generated by Q# Bridge." + "We can now use the TikZ extension to render a PDF from the LaTeX code generated by Q# Bridge to verify the output." ] }, { @@ -143,6 +143,13 @@ "shell.run_cell_magic('tikz', '-l quantikz', quantikz_diagram_2)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice how the two-operation sample was actually decomposed into its constituent individual gates in the generated diagram - this is because we set `group_by_scope` to `False` in the `QuantikzGenerationOptions` when generating the diagrams." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -158,7 +165,7 @@ "source": [ "qsharp.init() # this ensures clean state\n", "qsharp.eval(code_1)\n", - "Circuit(qsharp.circuit(\"MyQuantumApp.Run()\")) " + "Circuit(qsharp.circuit(\"Main()\")) " ] }, { @@ -170,7 +177,7 @@ "\n", "qsharp.init() # this ensures clean state\n", "qsharp.eval(code_2)\n", - "Circuit(qsharp.circuit(\"Main()\")) " + "Circuit(qsharp.circuit(\"MyQuantumApp.Run()\")) " ] }, { @@ -179,6 +186,39 @@ "source": [ "We should see that the diagrams generated using `quantikz` and the built-in `Circuit` widget are identical. This confirms that our LaTeX generation is working correctly!" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Q# compiler (and, by extension, Q# Bridge) can also group gates on the circuit into groups scoped by an operation. In our case that would mean a large single block representing `PrepareBellState`.\n", + "\n", + "Let's set `group_by_scope` of the `QuantikzGenerationOptions` to `True` and pass that into the `quantikz` function - and see how the generated diagram changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "quantikz_diagram_3 = quantikz(code_2, options=QuantikzGenerationOptions(group_by_scope=True))\n", + "\n", + "# for debugging, display the generated LaTeX code\n", + "print(quantikz_diagram_3)\n", + "\n", + "shell = get_ipython()\n", + "assert shell is not None\n", + "\n", + "shell.run_cell_magic('tikz', '-l quantikz', quantikz_diagram_3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Congratualations! You have successfully generated quantum circuit diagrams from Q# code using Q# Bridge and LaTeX in a Jupyter notebook." + ] } ], "metadata": { diff --git a/platforms/csharp/QSharp.Community.QSharpBridge/QSharp.Community.QSharpBridge.csproj b/platforms/csharp/QSharp.Community.QSharpBridge/QSharp.Community.QSharpBridge.csproj index 33d893d..5296f1e 100644 --- a/platforms/csharp/QSharp.Community.QSharpBridge/QSharp.Community.QSharpBridge.csproj +++ b/platforms/csharp/QSharp.Community.QSharpBridge/QSharp.Community.QSharpBridge.csproj @@ -2,7 +2,7 @@ net8.0 - 0.2.0 + 0.2.1 enable enable true diff --git a/platforms/python/qsharp-bridge/setup.py b/platforms/python/qsharp-bridge/setup.py index 9bd108e..206c832 100644 --- a/platforms/python/qsharp-bridge/setup.py +++ b/platforms/python/qsharp-bridge/setup.py @@ -14,7 +14,7 @@ CARGO_MANIFEST_PATH = os.path.abspath(os.path.join(HERE, "../../../Cargo.toml")) CARGO_TARGET_DIR = os.path.abspath(os.path.join(HERE, "../../../target/release")) BINDINGS_SRC = os.path.abspath(os.path.join(HERE, "../../../bindings/qsharp_bridge.py")) -VERSION = os.environ.get("PACKAGE_VERSION", "0.2.0") +VERSION = os.environ.get("PACKAGE_VERSION", "0.2.1") def get_lib_filename(): """ diff --git a/src/lib.rs b/src/lib.rs index 8cf4380..06b5fb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ use crate::sim::run_qs; use crate::sim::run_qs_with_options; use crate::quantikz::quantikz; use crate::quantikz::quantikz_operation; +use crate::quantikz::QuantikzGenerationOptions; pub mod noise; pub mod qasm; diff --git a/src/qsharp-bridge.udl b/src/qsharp-bridge.udl index 8ca3d49..52d5c3b 100644 --- a/src/qsharp-bridge.udl +++ b/src/qsharp-bridge.udl @@ -16,10 +16,10 @@ namespace qsharp_bridge { string qasm2_expression([ByRef]string expression, QasmGenerationOptions generation_options); [Throws=QsError] - string quantikz([ByRef]string source); + string quantikz([ByRef]string source, QuantikzGenerationOptions options); [Throws=QsError] - string quantikz_operation([ByRef]string operation, [ByRef]string source); + string quantikz_operation([ByRef]string operation, [ByRef]string source, QuantikzGenerationOptions options); [Throws=QsError] string estimate([ByRef]string source, string? job_params); @@ -33,6 +33,10 @@ dictionary QasmGenerationOptions { QasmResetBehavior reset_behavior; }; +dictionary QuantikzGenerationOptions { + boolean group_by_scope; +}; + enum QasmResetBehavior { "Supported", "Ignored", diff --git a/src/quantikz.rs b/src/quantikz.rs index db0ad64..7f54c33 100644 --- a/src/quantikz.rs +++ b/src/quantikz.rs @@ -2,11 +2,16 @@ use qsc::{ LanguageFeatures, PackageType, SourceMap, interpret::{CircuitEntryPoint, CircuitGenerationMethod, Interpreter}, target::Profile, }; -use qsc_circuit::{Circuit, ComponentColumn, Operation, TracerConfig}; +use qsc_circuit::{Circuit, Operation, TracerConfig}; use std::collections::HashMap; use crate::sim::QsError; +#[derive(Clone, Copy, Debug, Default)] +pub struct QuantikzGenerationOptions { + pub group_by_scope: bool, +} + type RegisterMap = HashMap<(usize, Option), usize>; #[derive(Clone)] @@ -15,17 +20,18 @@ struct Row { is_classical: bool, } -pub fn quantikz(source: &str) -> Result { - generate_quantikz_circuit(source, CircuitEntryPoint::EntryPoint) +pub fn quantikz(source: &str, options: QuantikzGenerationOptions) -> Result { + generate_quantikz_circuit(source, CircuitEntryPoint::EntryPoint, options) } -pub fn quantikz_operation(operation: &str, source: &str) -> Result { - generate_quantikz_circuit(source, CircuitEntryPoint::Operation(operation.to_string())) +pub fn quantikz_operation(operation: &str, source: &str, options: QuantikzGenerationOptions) -> Result { + generate_quantikz_circuit(source, CircuitEntryPoint::Operation(operation.to_string()), options) } fn generate_quantikz_circuit( source: &str, entry_point: CircuitEntryPoint, + options: QuantikzGenerationOptions, ) -> Result { let sources = SourceMap::new([("test.qs".into(), source.into())], None); let (std_id, store) = qsc::compile::package_store_with_stdlib(Profile::Unrestricted.into()); @@ -48,7 +54,10 @@ fn generate_quantikz_circuit( let circuit = interpreter.circuit( entry_point, CircuitGenerationMethod::ClassicalEval, - TracerConfig::default(), + TracerConfig { + group_by_scope: options.group_by_scope, + ..Default::default() + }, )?; Ok(circuit_to_quantikz(&circuit)) @@ -56,20 +65,29 @@ fn generate_quantikz_circuit( pub fn circuit_to_quantikz(c: &Circuit) -> String { let (mut rows, register_to_row) = build_rows(c); - - let grid = if c.component_grid.len() == 1 - && c.component_grid[0].components.len() == 1 - && !c.component_grid[0].components[0].children().is_empty() - { - c.component_grid[0].components[0].children() + let grid = &c.component_grid; + + // Check if we have a single top-level operation that wraps the entire circuit. + // If so, we unwrap it and use its children grid. + let maybe_children = if grid.len() == 1 && grid[0].components.len() == 1 { + match &grid[0].components[0] { + Operation::Unitary(u) => Some(&u.children), + Operation::Measurement(m) => Some(&m.children), + _ => None, + } } else { - &c.component_grid + None }; - let col_count = grid.len(); + let columns: Vec> = maybe_children + .filter(|children| !children.is_empty()) + .map(|children| children.iter().map(|c| c.components.iter().collect()).collect()) + .unwrap_or_else(|| grid.iter().map(|c| c.components.iter().collect()).collect()); + + let col_count = columns.len(); let mut table = initialize_table(rows.len(), col_count, &rows); - populate_table(grid, ®ister_to_row, &mut table, &mut rows); + populate_table(&columns, ®ister_to_row, &mut table, &mut rows); render_latex(&rows, &table) } @@ -113,14 +131,14 @@ fn initialize_table(row_count: usize, col_count: usize, rows: &[Row]) -> Vec], register_to_row: &RegisterMap, table: &mut [Vec], rows: &mut [Row], ) { - for (col_index, col) in grid.iter().enumerate() { + for (col_index, col) in columns.iter().enumerate() { let table_col = col_index; - for op in &col.components { + for op in col { // For measurements, we want to draw on the qubit line, so we treat qubits as targets for visual placement let targets = get_rows_for_operation(op, register_to_row, true); let controls = get_rows_for_operation(op, register_to_row, false); @@ -140,23 +158,31 @@ fn process_operation( ) { match op { Operation::Unitary(u) => { - process_unitary( - &u.gate, - &op.args(), - u.is_adjoint, - col, - targets, - controls, - table, - ); + if !u.children.is_empty() { + process_group(&u.gate, &u.args, col, targets, controls, table); + } else { + process_unitary( + &u.gate, + &op.args(), + u.is_adjoint, + col, + targets, + controls, + table, + ); + } } - Operation::Measurement(_) => { - for &t in targets { - table[t][col] = String::from("\\meter{}"); - rows[t].is_classical = true; - // Switch the rest of the wire to classical - for next_c in (col + 1)..table[t].len() { - table[t][next_c] = String::from("\\cw"); + Operation::Measurement(m) => { + if !m.children.is_empty() { + process_group(&m.gate, &m.args, col, targets, controls, table); + } else { + for &t in targets { + table[t][col] = String::from("\\meter{}"); + rows[t].is_classical = true; + // Switch the rest of the wire to classical + for next_c in (col + 1)..table[t].len() { + table[t][next_c] = String::from("\\cw"); + } } } } @@ -173,6 +199,30 @@ fn process_operation( } } +fn process_group( + name: &str, + args: &[String], + col: usize, + targets: &[usize], + controls: &[usize], + table: &mut [Vec], +) { + let simple_name = name.split('.').last().unwrap_or(name); + let label = operation_label(simple_name, args, false); + + // Find the min and max row indices to span the box + if let (Some(&min_row), Some(&max_row)) = (targets.iter().min(), targets.iter().max()) { + let wires = max_row - min_row + 1; + table[min_row][col] = format!("\\gate[wires={}]{{{}}}", wires, label); + + // Add controls + for &ctrl in controls { + let offset = min_row as isize - ctrl as isize; + table[ctrl][col] = format!("\\ctrl{{{}}}", offset); + } + } +} + fn process_unitary( name: &str, args: &[String], @@ -274,7 +324,7 @@ fn get_rows_for_operation( if is_target { &m.qubits } else { - &m.qubits + &vec![] } } Operation::Unitary(u) => { diff --git a/tests/tests_quantikz.rs b/tests/tests_quantikz.rs index 155220d..6297b44 100644 --- a/tests/tests_quantikz.rs +++ b/tests/tests_quantikz.rs @@ -1,5 +1,5 @@ use expect_test::expect; -use qsharp_bridge::quantikz::{quantikz, quantikz_operation}; +use qsharp_bridge::quantikz::{quantikz, quantikz_operation, QuantikzGenerationOptions}; #[test] fn quantikz_one_gate() { @@ -13,7 +13,8 @@ fn quantikz_one_gate() { M(q); } } - " + ", + QuantikzGenerationOptions { group_by_scope: false } ).expect("quantikz generation should succeed"); expect![[r#" @@ -35,7 +36,7 @@ fn quantikz_operation_one_gate() { } } "; - let tex = quantikz_operation("Test.Main", source).expect("quantikz generation should succeed"); + let tex = quantikz_operation("Test.Main", source, QuantikzGenerationOptions { group_by_scope: false }).expect("quantikz generation should succeed"); expect![[r#" \begin{quantikz} \lstick{$\ket{0}_{0}$} & \gate{H} & \meter{} & \cw \\ @@ -55,7 +56,8 @@ fn quantikz_toffoli() { CCNOT(q[0], q[1], q[2]); } } - " + ", + QuantikzGenerationOptions { group_by_scope: false } ).expect("quantikz generation should succeed"); expect![[r#" @@ -80,7 +82,7 @@ fn quantikz_operation_toffoli() { } "; - let tex = quantikz_operation("Test.Main", source).expect("quantikz generation should succeed"); + let tex = quantikz_operation("Test.Main", source, QuantikzGenerationOptions { group_by_scope: false }).expect("quantikz generation should succeed"); expect![[r#" \begin{quantikz} @@ -103,7 +105,8 @@ fn quantikz_swap_gate() { SWAP(q[0], q[1]); } } - " + ", + QuantikzGenerationOptions { group_by_scope: false } ).expect("quantikz generation should succeed"); expect![[r#" @@ -127,7 +130,7 @@ fn quantikz_operation_swap_gate() { } "; - let tex = quantikz_operation("Test.Main", source).expect("quantikz generation should succeed"); + let tex = quantikz_operation("Test.Main", source, QuantikzGenerationOptions { group_by_scope: false }).expect("quantikz generation should succeed"); expect![[r#" \begin{quantikz} @@ -158,7 +161,8 @@ fn quantikz_complex_sample() { let r2 = M(q2); } } - "# + "#, + QuantikzGenerationOptions { group_by_scope: false } ).expect("quantikz generation should succeed"); expect![[r#" @@ -190,7 +194,7 @@ fn quantikz_operation_complex_sample() { } "#; - let tex = quantikz_operation("Test.Main", source).expect("quantikz generation should succeed"); + let tex = quantikz_operation("Test.Main", source, QuantikzGenerationOptions { group_by_scope: false }).expect("quantikz generation should succeed"); expect![[r#" \begin{quantikz} @@ -221,7 +225,8 @@ fn quantikz_rotation_circuit() { M(q1); } } - "# + "#, + QuantikzGenerationOptions { group_by_scope: false } ).expect("quantikz generation should succeed"); expect![[r#" @@ -253,7 +258,7 @@ fn quantikz_operation_rotation_circuit() { } "#; - let tex = quantikz_operation("Test.Main", source).expect("quantikz generation should succeed"); + let tex = quantikz_operation("Test.Main", source, QuantikzGenerationOptions { group_by_scope: false }).expect("quantikz generation should succeed"); expect![[r#" \begin{quantikz} @@ -286,7 +291,8 @@ fn quantikz_cat_state() { MResetEachZ(qubits) } } - " + ", + QuantikzGenerationOptions { group_by_scope: false } ).expect("quantikz generation should succeed"); expect![[r#" @@ -326,7 +332,7 @@ fn quantikz_operation_cat_state() { } "; - let tex = quantikz_operation("Test.Main", source).expect("quantikz generation should succeed"); + let tex = quantikz_operation("Test.Main", source, QuantikzGenerationOptions { group_by_scope: false }).expect("quantikz generation should succeed"); expect![[r#" \begin{quantikz} @@ -341,4 +347,30 @@ fn quantikz_operation_cat_state() { \end{quantikz} "#]] .assert_eq(&tex); -} \ No newline at end of file +} +#[test] +fn quantikz_grouped_operation_should_unwrap_top_level() { + let source = r" + namespace Test { + operation PrepareBellState(q1 : Qubit, q2: Qubit) : Unit { + H(q1); + CNOT(q1, q2); + } + + @EntryPoint() + operation Run() : Result { + use (q1, q2) = (Qubit(), Qubit()); + PrepareBellState(q1, q2); + let r = M(q1); + Reset(q1); + Reset(q2); + return r; + } + } + "; + + let tex = quantikz(source, QuantikzGenerationOptions { group_by_scope: true }).expect("quantikz generation should succeed"); + + assert!(tex.contains("PrepareBellState"), "Should contain inner operation name"); + assert!(!tex.contains("\\gate[wires=2]{Run}"), "Should not contain top-level wrapper"); +}