A Quantum Engineering Focused Linux Distro From Scratch — Day 9
Day 9 was another day spent on getting my rust tooling up to snuff. In the past I had demoed a transpilation methodology from WASM to QMASM using a hacky modification to the WebAssembly binary file decoder in Rust. It showed that WebAssembly could be lowered to pyQUBO compatible constraint expressions via a method inspired by Parallelizing Fortran Compiler.
I worked on my Mac today and I’d updated xcode recently, so I had to update cargo too.
rustup update
I wanted to turn the wasm transpilation demo into a production quality implementation, starting with the quantum rust package I had revisited on day 8. I started by reading up on inline assembly in rust and adding a few attributes to the State struct to allow us to target a specific quantum state to a backend and to enable lazy execution.
+enum Backend {
+ X86, // swaps out the coefficient impl
+ WASM, // swaps out the coefficient impl
+ RS, // leaves all impl usage as-is
+ QMASM, // swaps out the coefficient impl and uses wasm_pfc
+ QASM, // swaps out the ket impl
+ DELEGATED// swaps out the ket impl and uses publisher
+}
+
#[derive(Clone)]
pub struct State {
pub kets: Vec<Ket>,
pub num_qubits: usize,
- pub symbol: char
+ pub symbol: char,
+ pub backend: Backend,
+ pub lazy: bool,
+ pub verbose: bool
}
/// Initializes a quantum state with a given set of kets and number of qubits.
-pub fn create_state(kets:Vec<Ket>, num_qubits:usize, symbol:char) -> State {
- State{kets: kets, num_qubits: num_qubits, symbol: symbol}
+pub fn create_state(kets:Vec<Ket>, num_qubits:usize, symbol:char, backend:Option<Backend>, lazy:Option<bool>, verbose:Option<bool>) -> State {
+ State{kets: kets, num_qubits: num_qubits, symbol: symbol, backend: backend.unwrap_or('RS'), lazy: lazy.unwrap_or(false), verbose: verbose.unwrap_or(false)}
}
“x86” and “WASM” would indicate compilations to distinct flavours of classical assembly code, whereas “QMASM” and “QASM” would indicate compilation to quantum assembly languages. “RS” would indicate a simple and immediate calculation of the result using the available Rust impls, and “delegated” would simply result in a new call out to a cluster of services subscribed to an RPC topic capable of deciding for themselves what to do with the request.
The reason the coefficient.rs was written so verbosely — using no rust crate for complex number arithmetic — was to provide a template for these alternative implementations. I wrote new traits that captured all the operations on the coefficients in Dirac notation quantum states required for stabilizer simulations. If we were writing an x86 assembly simulator then it would only be necessary to write macros for the following, for example.
pub trait Coefficient {
type StructType;
fn equals_coefficient(&self, other: Self::StructType) -> bool;
fn multiply_by_coefficient(&self, other: Self::StructType) -> Self::StructType;
fn add_to_coefficient(&self, other: Self::StructType) -> Self::StructType;
fn get_magnitude(&self) -> f64;
fn get_imaginary(&self) -> bool;
fn set_magnitude(&mut self, magnitude:f64);
fn set_imaginary(&mut self);
fn clear_imaginary(&mut self);
fn negate_magnitude(&mut self);
fn multiply_by_i(&mut self);
fn multiply_by_number(&mut self, number:f64);
fn to_probability(&self) -> f64;
fn complex_conjugate(&mut self) -> Self::StructType;
fn print(&self);
}
I removed all of the operations that referenced the ComplexCoefficient from the trait, so that the trait was as simple as possible. These would be the operations between data registers that would need to be written in assembly. In compilation scenarios, the rest of the quantum rust library would be responsible for making sure that data and control flow dependencies got mapped correctly between these blocks of assembly. In other words, memory management would be solved outside of the trait implementation.
You might imagine that if we were compiling to the x86 instruction set we might use a mov instruction to load a literal into a register instead of instantiating a FloatCoefficient struct with an f64 attribute, for example.
#[derive(Copy, Clone)]
pub struct FloatCoefficient {
magnitude: f64,
imaginary: bool
}
/// Initializes a coefficient.
pub fn create_coefficient(magnitude:f64, imaginary:bool) -> FloatCoefficient {
FloatCoefficient{magnitude: magnitude, imaginary: imaginary}
}
To simplify things for the first pass of this, I made the assumption that each operation would receive literals from outside of the assembly. What this could look like is the following, from the Rust documentation.
The inout and out operands specify how registers get substituted into the assembly string template included in an asm! block. Using inout specifies that the mutable x variable will both be used to initialize a register at the beginning of the assembly and will be written to with the contents of the register at the end of the assembly block’s execution. Using out simply specifies that a register will be written out at the end of a block of inline assembly, but in this case it is written to _ so it is effectively discarded.
let mut x: u64 = 4;
unsafe {
asm!(
"mov {tmp}, {x}",
"shl {tmp}, 1",
"shl {x}, 2",
"add {x}, {tmp}",
x = inout(reg) x,
tmp = out(reg) _,
);
}
In many methods I already used string templates to print out assembly code. Consider the function below, for example.
/// Performs a Pauli Z gate on the target qubit.
pub fn z(&mut self, qubit:usize) {
for ket in &mut self.kets {
if self.verbose {
print!("z ({})", qubit);
ket.print();
print!(" =");
}
ket.z(qubit);
if self.verbose {
ket.print();
println!();
}
}
}
In some cases, these function also specified certain compilations of quantum operations, as with ket.y().
/// Performs a Pauli Y gate on the target qubit.
pub fn y(&mut self, qubit:usize) {
self.z(qubit);
self.x(qubit);
self.coefficient.multiply_by_i();
}
It looked very natural to replace this use of string templates to print debug messages to instead compile QASM, and to adopt the same approach to compiling x86, for example. Whether or not the assembly would also be immediately evaluated would depend on both the language and the lazy flag created earlier.
All I would have to do is re-implement the Coefficient trait using assembly instead of Rust, and the logic was already implemented once in Rust for me to reference.
Feel free to take a look at the full diff of changes made today at https://github.com/comp-phys-marc/quantum-rs/compare/main...backends.