Skip to content

Conversation

to24toro
Copy link
Contributor

@to24toro to24toro commented Apr 21, 2025

Fixes #13145 and Continue with #13666

Summary

This PR introduces and integrates several key modules for a Rust-based quantum computing framework, laying the foundation for exporting circuits in QASM 3.0 format. The modules added are:
1. printer.rs:
• Implements functionality for formatted output generation of AST or other hierarchical structures.
2. symbols.rs:
• Manages a symbol table used for mapping variable names to their corresponding entities.
• Handling bit info, register info, and gate info in the symbol table.
• Provides helper methods for creating, binding, and looking up symbols with robust error handling.
3. ast.rs:
• Defines the AST structure for representing quantum programs.
• Contains data structures such as nodes, expressions, and declarations that model the core components of quantum circuit descriptions.
4. exporter.rs:
• Implements a builder for generating QASM 3.0 programs.
• Integrates with Python-based tools (via pyo3) to extract circuit data and parameters.

Details and comments

Current Limitations

The current implementation has limited QASM 3.0 output functionality and relies on partial Python integration. The following features are not yet supported:
• Control Flow Constructs: While QASM 3.0 supports control flow, such as loops and conditionals, these are not yet implemented.
• Classical Variables: Handling classical variables is not included in the current architecture.
• Not standard Gates: Generating QASM for not standard gates and mapping them to Qiskit’s standard gates is pending.

These tasks require further discussion to determine whether they should be included in the scope of Qiskit Issue #13145.

What is Pending

1. Support for:
• Control flow constructs like loops, conditionals, and subroutines.
• Advanced QASM 3.0 features, including classical variable.

Difference from previous PR(#13666 )

• Support for Registers.
• Support for all the standard gates defined by qiskit.
• Support for custom gates.
• Add tests in test_exporter.py.

We can export the below circuit.

from qiskit import qasm3
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit import Clbit, Qubit
from qiskit.circuit.library import XXPlusYYGate
qr = QuantumRegister(2,"a")
cr = ClassicalRegister(1)
qc = QuantumCircuit(2)
qc.add_bits([Clbit()])
qc.add_register(qr)
qc.add_bits([Qubit()])
qc.add_register(cr)
qc.append(XXPlusYYGate(0.4,0.12),[0,1])
qc.measure_all()
print(qasm3.dumps_experimental(qc))

Output is here:

OPENQASM 3.0;
include "stdgates.inc";
gate sxdg _gate_q_0 {
  s _gate_q_0;
  h _gate_q_0;
  s _gate_q_0;
}
gate xx_plus_yy(_gate_p_0, _gate_p_1) _gate_q_0, _gate_q_1 {
  rz(_gate_p_1) _gate_q_0;
  rz(-1.5707963267948966) _gate_q_1;
  sx _gate_q_1;
  rz(1.5707963267948966) _gate_q_1;
  s _gate_q_0;
  cx _gate_q_1, _gate_q_0;
  ry(-0.5*_gate_p_0) _gate_q_1;
  ry(-0.5*_gate_p_0) _gate_q_0;
  cx _gate_q_1, _gate_q_0;
  sdg _gate_q_0;
  rz(-1.5707963267948966) _gate_q_1;
  sxdg _gate_q_1;
  rz(1.5707963267948966) _gate_q_1;
  rz(-1.0*_gate_p_1) _gate_q_0;
}
bit _bit0;
bit[1] c1;
bit[5] meas;
qubit _qubit4;
qubit[2] q;
qubit[2] a;
xx_plus_yy(0.4, 0.12) q[0], q[1];
barrier q[0], q[1], a[0], a[1], _qubit4;
meas[0] = measure q[0];
meas[1] = measure q[1];
meas[2] = measure a[0];
meas[3] = measure a[1];
meas[4] = measure _qubit4;
import numpy as np
import warnings

from qiskit import qasm3
from qiskit.exceptions import ExperimentalWarning
from qiskit.circuit.library import RealAmplitudes

warnings.filterwarnings("ignore", category=ExperimentalWarning)

# This is a little more than 5k gates.
base = RealAmplitudes(27, reps=100, flatten=True)
base = base.assign_parameters(np.random.rand(base.num_parameters))

print("Current OpenQASM 3 time")
%timeit qasm3.dumps(base)
print("New OpenQASM 3 time")
%timeit qasm3.dumps_experimental(base)
Current OpenQASM 3 time
45.6 ms ± 282 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
New OpenQASM 3 time
4.33 ms ± 165 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

@to24toro to24toro requested a review from a team as a code owner April 21, 2025 09:52
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@to24toro to24toro added performance Rust This PR or issue is related to Rust code in the repository labels Apr 21, 2025
@coveralls
Copy link

coveralls commented Apr 21, 2025

Pull Request Test Coverage Report for Build 15285103341

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 1166 of 1755 (66.44%) changed or added relevant lines in 7 files are covered.
  • 266 unchanged lines in 8 files lost coverage.
  • Overall coverage decreased (-0.5%) to 87.849%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/qasm3/init.py 12 18 66.67%
crates/qasm3/src/ast.rs 20 53 37.74%
crates/qasm3/src/lib.rs 50 109 45.87%
crates/qasm3/src/printer.rs 297 509 58.35%
crates/qasm3/src/exporter.rs 782 1061 73.7%
Files with Coverage Reduction New Missed Lines %
crates/circuit/src/symbol_expr.rs 1 75.39%
crates/quantum_info/src/convert_2q_block_matrix.rs 1 92.65%
crates/transpiler/src/passes/optimize_1q_gates_decomposition.rs 1 90.8%
crates/transpiler/src/passes/unitary_synthesis.rs 1 94.85%
crates/quantum_info/src/versor_u2.rs 3 81.85%
crates/qasm2/src/lex.rs 4 91.98%
crates/circuit/src/packed_instruction.rs 8 93.23%
crates/circuit/src/operations.rs 247 87.82%
Totals Coverage Status
Change from base Build 15279236676: -0.5%
Covered Lines: 79735
Relevant Lines: 90764

💛 - Coveralls

@mtreinish mtreinish added priority: high Changelog: New Feature Include in the "Added" section of the changelog mod: qasm3 Related to OpenQASM 3 import or export labels Apr 21, 2025
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I haven't looked through entirely completely yet, but some top-level comments I had from a quick play:

  1. I think you might have run your performance numbers with a debug build? I see rather better performance than you did.
  2. The order that gate definitions (like xx_plus_yy and sxdg in the example) is non-deterministic - it can change if you restart python. We want it to be deterministic, but more importantly, the order is very important - in the example, sxdg must be defined before xx_plus_yy, because xx_plus_yy uses sxdg.
  3. The AST doesn't interact with any sort of symbol table, and it takes most things by ownership, including (and most importantly) Identifier. So when we're outputting a circuit with many many gates, we're first allocating lots and lots of String instances with the same value, then copying them over into the output. It will likely be much faster to have the AST be defined in reference to a symbol table, so the string representations of identifiers are stored separately, and the AST only stores an integer index into the table. You can do this completely separately if you like, or you might find an Interner<str> from qiskit_circuit::interner to be a quick way to get something good enough.

@to24toro
Copy link
Contributor Author

@jakelishman
Thank you for your comments.

  1. I forgot to execute running with a release mode. I updated the performance metrics.
  2. I changed to use Indexmap instead of hashmap at fbcf28a .
  3. As you mentioned, I also believe it can be made faster, and I have confirmed it — it was about 12× faster on my local PC by avoiding string copying when creating Identifier. But I want to separate a PR because I am not confident whether we can safely use this symbol table architecture that includes qiskit_circuit::interner when introducing controlflow statements, that is, using new_scope. I am afraid that it will be difficult to correctly manage the appropriate Interner across different scopes.

to24toro and others added 2 commits May 12, 2025 09:43
A syntactic (but not logical) merge conflict in `qiskit_circuit::bit`
was trivial, but a logical merge conflict caused by the PyO3 0.25 update
needed some actual code changes.
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work on this Kento, and the updates from the last review. In general, given this is in the "experimental" interface, and I've been sat on it a long time, I want to move to merge as-is, based solely on the observable user behaviour. That said, I think there's a few places in the code where I'd look to improvements, for any of runtime performance, ease of future maintenance, and code organisation. I've left some comments to these effects, which we might want to convert into full issues to track potential follow-up work (alongside obviously making it feature complete).

I fixed up the last merge conflicts, including some parsing of the Python-space kwargs that I'm not 100% sure how it worked before, but I assume is something that changed in a PyO3 update.


About Identifier vs interning: there should be no difference in safety, because as long as every Interned<str> refers to keys in the same Interner, equality comparisons between strings and interned keys are the same thing, just comparisons between interned keys (and the memory taken to store them) are much faster. The trouble of comparing names between scopes is the same whether we use an explicit string or a key. I'm happy for that to be a follow-up, though.


The review comments largely relate to the high-level API and the design of the data structures, and have some places we might want to change in the future in follow-ups as this drives towards being the primary exporter. I didn't look at the actual implementation of the QASM3Builder methods in any significant detail - I know a lot of it is ported from the Python version (which comes with a lot of tech debt itself), and it's pretty dependent on the data structures, so if those change at all, a lot of the discussion of the individual methods would become obsolete.

Comment on lines +108 to +109
#[derive(Debug, Clone)]
pub struct IntegerLiteral(pub(crate) i32);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are likely problems ahead of us if we're limiting integers to 32 bits (registers are in theory arbitrary sized), but we can look at changing the internal representation of that in the future.

Comment on lines +114 to +119
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BitstringLiteral {
pub value: String,
pub width: u32,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bitstring literal can always be represented by a (big) integer, so strictly some u64 or unsigned BigInteger would be more efficient here, but again, we can look at that later.

Comment on lines +121 to +134
#[derive(Debug, Clone)]
pub struct DurationLiteral {
pub value: f64,
pub unit: DurationUnit,
}

#[derive(Debug, Clone)]
pub enum DurationUnit {
Nanosecond,
Microsecond,
Millisecond,
Second,
Sample,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR was written, we gained a more Rust-native way to represent durations in qiskit_circuit, so in the future we can swap to that.

Comment on lines +124 to +126
fn start_line(&mut self) {
write!(self.stream, "{}", self.indent.repeat(self.current_indent)).unwrap();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the way through the printer: panicking in Rust is generally supposed to be for un-recoverable errors, particularly in a library, like Qiskit. We should ideally return a ::std::io::Result<()> from each of these functions, and bubble up the failure properly to Python space.

Comment on lines +150 to +155
if let Some(version) = &node.version {
self.visit(&Node::Version(version))
};
for include in node.includes.iter() {
self.visit(&Node::Include(include));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor: wrapping a Version object in a Node::Version in order to dispatch it back down to Version just ends up being a bit of a waste of time (though I'd imagine the compiler will actually optimise it out here) - there's no particular reason not to just call visit_version directly (and so on for other similar examples).

Comment on lines +42 to +56
// These are the prefixes used for the loose qubit and bit names.
lazy_static! {
static ref BIT_PREFIX: &'static str = "_bit";
}
lazy_static! {
static ref QUBIT_PREFIX: &'static str = "_qubit";
}

// These are the prefixes used for the gate parameters and qubits.
lazy_static! {
static ref GATE_PARAM_PREFIX: &'static str = "_gate_p";
}
lazy_static! {
static ref GATE_QUBIT_PREFIX: &'static str = "_gate_q";
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ones don't need to be lazy - const BIT_PREFIX: &str = "_bit"; would work fine.

Comment on lines +157 to +176
#[derive(Debug, Clone)]
struct DumpOptions {
includes: Vec<String>,
basis_gates: Vec<String>,
disable_constants: bool,
allow_aliasing: bool,
indent: String,
}

impl Default for DumpOptions {
fn default() -> Self {
Self {
includes: vec!["stdgates.inc".to_string()],
basis_gates: vec![],
disable_constants: true,
allow_aliasing: false,
indent: " ".to_string(),
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are just the options that go into making an Exporter, would it have made sense to skip the intermediate structure and go straight to having constructors of Exporter? Exporter itself is just a configuration definition for QASM3Builder, and this is a configuration definition for Exporter.

Comment on lines +179 to +184
#[pyo3(signature = (circuit, /, kwargs=None))]
pub fn dumps(
_py: Python,
circuit: &Bound<PyAny>,
kwargs: Option<&Bound<PyDict>>,
) -> PyResult<String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making a fake kwargs dictionary like this is a bit unusual for a Rust interface. You can specify keyword args properly using the pyo3(signature = ...) derive macro, like you have, including specifying defaults. But here, since it's only called directly by Python space that already sets all the defaults (and only intended to be called that way), I'm not sure that the kwargs indirection is achieving anything over just making all the configuration options explicitly required.

Comment on lines 230 to 289
#[pyfunction]
#[pyo3(signature = (circuit,stream, /, kwargs=None))]
pub fn dump(
_py: Python,
circuit: &Bound<PyAny>,
stream: &Bound<PyAny>,
kwargs: Option<&Bound<PyDict>>,
) -> PyResult<()> {
let mut options = DumpOptions::default();

if let Some(kw) = kwargs {
let kw = kw.as_ref();
if let Ok(val) = kw.get_item("includes") {
options.includes = val.extract::<Vec<String>>()?;
}
if let Ok(val) = kw.get_item("basis_gates") {
options.basis_gates = val.extract::<Vec<String>>()?;
}
if let Ok(val) = kw.get_item("disable_constants") {
options.disable_constants = val.extract::<bool>()?;
}
if let Ok(val) = kw.get_item("allow_aliasing") {
options.allow_aliasing = val.extract::<bool>()?;
}
if let Ok(val) = kw.get_item("indent") {
options.indent = val.extract::<String>()?;
}
}
let circuit_data = circuit
.getattr("_data")?
.downcast::<CircuitData>()?
.borrow();

let islayout = !circuit.getattr("layout")?.is_none();

let exporter = exporter::Exporter::new(
options.includes,
options.basis_gates,
options.disable_constants,
options.allow_aliasing,
options.indent,
);

let mut output = Vec::new();
exporter
.dump(&circuit_data, islayout, &mut output)
.map_err(|err| {
QASM3ImporterError::new_err(format!(
"failed to export circuit using qasm3.dump_experimental: {:?}",
err
))
})?;

let output_str = String::from_utf8(output)
.map_err(|e| QASM3ImporterError::new_err(format!("invalid utf-8 output: {:?}", e)))?;

stream.call_method1("write", (output_str,))?;

Ok(())
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepting a text-mode stream in the Python-space dump isn't going to transition well to Rust - if we're given an open Python-space stream, we almost certainly should handle it by creating a full string from Rust and then copying it across to Python space, unless this is a performance pipeline we really want to optimise, in which case we should make a Rust object that implements Write, and buffers writes out to the underlying Python stream so we aren't paying a huge runtime penalty for going Rust -> Python all the time (like how general file-backed streams also buffer writes).

For the time being, I feel like the whole Rust-space dump is unnecessary, and we could achieve the same thing just in the Python-space wrapper, with it calling Rust-space dumps.

Comment on lines +396 to +406
return _qasm3.dumps(
circuit,
{
"includes": includes,
"basis_gates": basis_gates,
"disable_constants": disable_constants,
"alias_classical_registers": alias_classical_registers,
"allow_aliasing": allow_aliasing,
"indent": indent,
},
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the strange calling convention I mentioned in the previous review comment - it'd be more natural if these were just regular keyword arguments, even if it is just an internal function.

@jakelishman jakelishman enabled auto-merge May 27, 2025 20:35
@jakelishman jakelishman added this pull request to the merge queue May 27, 2025
Merged via the queue into Qiskit:main with commit bbcc987 May 27, 2025
26 checks passed
rahaman-quantum pushed a commit to rahaman-quantum/qiskit that referenced this pull request Jun 20, 2025
…ssical Variables in Rust (Qiskit#14226)

* draft for qasm3exporter

* refactoring exporter and ast

* wip: simplify symbol table

* Remove unused struct

* Add #[allow(dead_code)] to ast.rs

* Remove scopes from symbol_table because we might not support controlflow and scope switching

* Remove qubit_map, clbit_map, and some functions for handling controlflow beucase we might not support controlflow

* Add tests and format

* format

* Added registers to BitLocations

* Edited ast.rs

* Exporter.rs for standard gates

* Fixed exporter.py

* printer.rs

* Add test

* format

* python format

* Fix dumps from python

* refactoring exporter.rs

* Fix printer.py

* Reorder each functions

* Replace hashmap with indexmap in using gates in symboltable

---------

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: qasm3 Related to OpenQASM 3 import or export performance priority: high Rust This PR or issue is related to Rust code in the repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Port QASM3 exporter to rust
5 participants