From cacd36201b11f06e493c486da6bf59782f9caf08 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 4 Jun 2026 11:35:26 -0700 Subject: [PATCH 01/67] initial lexer implementation --- Cargo.toml | 1 + source/compiler/qsc_stim_parser/Cargo.toml | 10 ++ source/compiler/qsc_stim_parser/src/lex.rs | 145 +++++++++++++++++++++ source/compiler/qsc_stim_parser/src/lib.rs | 1 + 4 files changed, 157 insertions(+) create mode 100644 source/compiler/qsc_stim_parser/Cargo.toml create mode 100644 source/compiler/qsc_stim_parser/src/lex.rs create mode 100644 source/compiler/qsc_stim_parser/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index ef4886ae8f6..a3beb773c31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "source/compiler/qsc_hir", "source/compiler/qsc_openqasm_compiler", "source/compiler/qsc_openqasm_parser", + "source/compiler/qsc_stim_parser", "source/compiler/qsc_linter", "source/compiler/qsc_lowerer", "source/compiler/qsc_parse", diff --git a/source/compiler/qsc_stim_parser/Cargo.toml b/source/compiler/qsc_stim_parser/Cargo.toml new file mode 100644 index 00000000000..50860fd23e1 --- /dev/null +++ b/source/compiler/qsc_stim_parser/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "qsc_my_parser" +edition.workspace = true +version.workspace = true + +[dependencies] +enum-iterator.workspace = true +qsc_data_structures = { path = "../qsc_data_structures" } + +[dev-dependencies] diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs new file mode 100644 index 00000000000..3a031ef8028 --- /dev/null +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -0,0 +1,145 @@ +use enum_iterator::{Sequence, next}; +use qsc_data_structures::span::Span; +use std::iter::Peekable; +use std::str::CharIndices; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct Token { + pub(crate) kind: TokenKind, + pub(crate) span: Span, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] +pub enum TokenKind { + Newline, // \n + Whitespace, // spaces, tabs + Comment, // # ... + Uint, // unsigned integers + Double, // floating-point numbers + InstructionName, // H, X, CNOT, etc. + Rec, // rec[- ...] + Sweep, // sweep[...] + Tag, // "[...]" + Open(Delim), // ( { + Close(Delim), // ) } + Star, // * + Bang, // ! + Minus, // - + Comma, // , + Unknown, // unknown token +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] +pub enum Delim { + Paren, + Brace, +} + +pub struct Lexer<'a> { + input: &'a str, + input_len: u32, + chars: Peekable>, +} + +impl<'a> Lexer<'a> { + pub fn new(input: &'a str) -> Self { + Self { + input, + input_len: input + .len() + .try_into() + .expect("input length should fit into u32"), + chars: input.char_indices().peekable(), + } + } + + fn eat_while(&mut self, mut f: impl FnMut(char) -> bool) { + while self.chars.next_if(|i| f(i.1)).is_some() {} + } + + fn whitespace(&mut self) { + self.eat_while(char::is_whitespace); + } + + fn comment(&mut self) { + self.eat_while(|c| c != '\n'); + } + + fn scan_number(&mut self) -> TokenKind { + self.eat_while(|c| c.is_ascii_digit()); + if self.chars.next_if(|(_, c)| *c == '.').is_some() { + self.eat_while(|c| c.is_ascii_digit()); + return TokenKind::Double; + } + return TokenKind::Uint; + } + + fn scan_bracketed(&mut self) { + if self.chars.next_if(|(_, c)| *c == '[').is_some() { + self.eat_while(|c| c != ']'); + self.chars.next_if(|(_, c)| *c == ']'); + } + } + + fn scan_identifier(&mut self, lo: usize) -> TokenKind { + self.eat_while(|c| c.is_alphanumeric() || c == '_'); + let hi: usize = self + .chars + .peek() + .map_or(self.input_len as usize, |(i, _)| *i); + // TODO: What if some identifier starts with "rec" but is not a rec token? + match &self.input[lo..hi] { + "rec" => { + self.scan_bracketed(); + TokenKind::Rec + } + "sweep" => { + self.scan_bracketed(); + TokenKind::Sweep + } + _ => TokenKind::InstructionName, + } + } +} + +impl Iterator for Lexer<'_> { + type Item = Token; + + fn next(&mut self) -> Option { + use Delim::{Brace, Paren}; + let (offset, c) = self.chars.next()?; + let lo: u32 = offset.try_into().expect("offset should fit into u32"); + let token_kind = match c { + '\n' => TokenKind::Newline, + ' ' | '\t' => { + self.whitespace(); + TokenKind::Whitespace + } + '#' => { + self.comment(); + TokenKind::Comment + } + '(' => TokenKind::Open(Paren), + ')' => TokenKind::Close(Paren), + '{' => TokenKind::Open(Brace), + '}' => TokenKind::Close(Brace), + '*' => TokenKind::Star, + '!' => TokenKind::Bang, + '-' => TokenKind::Minus, + ',' => TokenKind::Comma, + '0'..='9' => self.scan_number(), + 'A'..='Z' | 'a'..='z' => self.scan_identifier(lo as usize), + '[' => { + self.scan_bracketed(); + TokenKind::Tag + } + _ => TokenKind::Unknown, + }; + + let hi: u32 = self.chars.peek().map_or(self.input_len, |(i, _)| *i as u32); + Some(Token { + kind: token_kind, + span: Span { lo, hi }, + }) + } +} diff --git a/source/compiler/qsc_stim_parser/src/lib.rs b/source/compiler/qsc_stim_parser/src/lib.rs new file mode 100644 index 00000000000..88eb3c4d149 --- /dev/null +++ b/source/compiler/qsc_stim_parser/src/lib.rs @@ -0,0 +1 @@ +pub mod lex; From 8716674db205d5588cbe101abea19072802c8ebf Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 4 Jun 2026 17:02:28 -0700 Subject: [PATCH 02/67] add display implementation for TokenKind --- source/compiler/qsc_stim_parser/src/lex.rs | 39 ++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 3a031ef8028..f07f6d8b9e8 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -1,7 +1,10 @@ -use enum_iterator::{Sequence, next}; +use enum_iterator::Sequence; use qsc_data_structures::span::Span; -use std::iter::Peekable; use std::str::CharIndices; +use std::{ + fmt::{self, Display, Formatter}, + iter::Peekable, +}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct Token { @@ -29,12 +32,44 @@ pub enum TokenKind { Unknown, // unknown token } +impl Display for TokenKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + TokenKind::Newline => f.write_str("newline"), + TokenKind::Whitespace => f.write_str("whitespace"), + TokenKind::Comment => f.write_str("comment"), + TokenKind::Uint => f.write_str("uint"), + TokenKind::Double => f.write_str("double"), + TokenKind::InstructionName => f.write_str("instruction_name"), + TokenKind::Rec => f.write_str("rec"), + TokenKind::Sweep => f.write_str("sweep"), + TokenKind::Tag => write!(f, "tag"), + TokenKind::Open(delim) => write!(f, "open({})", delim), + TokenKind::Close(delim) => write!(f, "close({})", delim), + TokenKind::Star => write!(f, "star"), + TokenKind::Bang => write!(f, "bang"), + TokenKind::Minus => write!(f, "minus"), + TokenKind::Comma => write!(f, "comma"), + TokenKind::Unknown => write!(f, "unknown"), + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum Delim { Paren, Brace, } +impl Display for Delim { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Delim::Paren => f.write_str("paren"), + Delim::Brace => f.write_str("brace"), + } + } +} + pub struct Lexer<'a> { input: &'a str, input_len: u32, From 35804bac09b1cbd8fb4057e65e2758800ad2fd15 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 8 Jun 2026 22:40:27 -0700 Subject: [PATCH 03/67] temporary commit to save progress --- .../qsc_stim_parser/examples/lex_stim.rs | 31 ++++ source/compiler/qsc_stim_parser/src/lex.rs | 26 +-- source/compiler/qsc_stim_parser/src/lib.rs | 1 + source/compiler/qsc_stim_parser/src/parser.rs | 175 ++++++++++++++++++ 4 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 source/compiler/qsc_stim_parser/examples/lex_stim.rs create mode 100644 source/compiler/qsc_stim_parser/src/parser.rs diff --git a/source/compiler/qsc_stim_parser/examples/lex_stim.rs b/source/compiler/qsc_stim_parser/examples/lex_stim.rs new file mode 100644 index 00000000000..a900a897672 --- /dev/null +++ b/source/compiler/qsc_stim_parser/examples/lex_stim.rs @@ -0,0 +1,31 @@ +use qsc_my_parser::lex::Lexer; + +fn main() { + let stim_code = "\ +H 0 +CNOT 0 1 +M 0 1 +DETECTOR rec[-1] rec[-2] +OBSERVABLE_INCLUDE(0) rec[-1] +"; + + println!("Input:\n{stim_code}"); + println!("{:-<50}", ""); + println!("{:<20} {:<10} {:}", "TOKEN KIND", "SPAN", "TEXT"); + println!("{:-<50}", ""); + + let lexer = Lexer::new(stim_code); + for token in lexer { + let text = &stim_code[token.span.lo as usize..token.span.hi as usize]; + let text_display = match token.kind { + qsc_my_parser::lex::TokenKind::Newline => "\\n".to_string(), + _ => format!("{:?}", text), + }; + println!( + "{:<20} {:<10} {}", + token.kind.to_string(), + format!("{}..{}", token.span.lo, token.span.hi), + text_display + ); + } +} diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index f07f6d8b9e8..0d573e99608 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -7,16 +7,15 @@ use std::{ }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct Token { - pub(crate) kind: TokenKind, - pub(crate) span: Span, +pub struct Token { + pub kind: TokenKind, + pub span: Span, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum TokenKind { Newline, // \n - Whitespace, // spaces, tabs - Comment, // # ... + Procedure, // #! ... Uint, // unsigned integers Double, // floating-point numbers InstructionName, // H, X, CNOT, etc. @@ -36,8 +35,7 @@ impl Display for TokenKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { TokenKind::Newline => f.write_str("newline"), - TokenKind::Whitespace => f.write_str("whitespace"), - TokenKind::Comment => f.write_str("comment"), + TokenKind::Procedure => f.write_str("procedure"), TokenKind::Uint => f.write_str("uint"), TokenKind::Double => f.write_str("double"), TokenKind::InstructionName => f.write_str("instruction_name"), @@ -88,7 +86,7 @@ impl<'a> Lexer<'a> { } } - fn eat_while(&mut self, mut f: impl FnMut(char) -> bool) { + fn eat_while(&mut self, mut f: impl Fn(char) -> bool) { while self.chars.next_if(|i| f(i.1)).is_some() {} } @@ -148,11 +146,15 @@ impl Iterator for Lexer<'_> { '\n' => TokenKind::Newline, ' ' | '\t' => { self.whitespace(); - TokenKind::Whitespace + return self.next(); } '#' => { - self.comment(); - TokenKind::Comment + if self.chars.next_if(|(_, c)| *c == '!').is_some() { + TokenKind::Procedure + } else { + self.comment(); + return self.next(); + } } '(' => TokenKind::Open(Paren), ')' => TokenKind::Close(Paren), @@ -178,3 +180,5 @@ impl Iterator for Lexer<'_> { }) } } + +// TODO: PAULI CAN COME IMMEDIATELY BEFORE THE UINT (NO SPACE) diff --git a/source/compiler/qsc_stim_parser/src/lib.rs b/source/compiler/qsc_stim_parser/src/lib.rs index 88eb3c4d149..b481f475069 100644 --- a/source/compiler/qsc_stim_parser/src/lib.rs +++ b/source/compiler/qsc_stim_parser/src/lib.rs @@ -1 +1,2 @@ pub mod lex; +pub mod parser; diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs new file mode 100644 index 00000000000..30ffd62da1c --- /dev/null +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -0,0 +1,175 @@ +use crate::lex::Delim::Brace; +use crate::lex::Lexer; +use crate::lex::Token; +use crate::lex::TokenKind; +use qsc_data_structures::span::Span; +use std::{ + fmt::{self, Display, Formatter}, + iter::Peekable, +}; + +pub struct Circuit { + pub span: Span, + pub items: Vec, +} + +pub enum Item { + Line(Line), + Block(Block), +} + +pub struct Block { + pub span: Span, + pub block_instruction: Instruction, // currently, only the "REPEAT" instruction is supported + pub items: Vec, +} + +pub struct Line { + pub span: Span, + pub kind: LineKind, +} + +pub enum LineKind { + Instruction(Instruction), + Comment(String), +} + +pub struct Instruction { + pub span: Span, + pub name: String, + pub tag: Option, + pub args: Vec, + pub targets: Vec, +} + +pub struct Target { + pub span: Span, + pub kind: TargetKind, +} + +pub enum TargetKind { + Qubit { + negated: bool, + value: u32, + }, + MeasurementRecord { + value: u32, + }, + SweepBit { + value: u32, + }, + Pauli { + negated: bool, + pauli: Pauli, + value: u32, + }, + Combiner { + value: bool, + }, +} + +pub enum Pauli { + X, + Y, + Z, +} + +struct Parser<'a> { + input: &'a str, + tokens: Peekable>, +} + +impl<'a> Parser<'a> { + pub fn new(input: &'a str) -> Self { + Self { + input, + tokens: Lexer::new(input).peekable(), + } + } + + pub fn expect(&mut self, kind: TokenKind) -> Token { + let token = self.tokens.next().expect("expected token"); + if token.kind != kind { + panic!("expected token of kind {:?}", kind); + } + token + } + + pub fn parse(&mut self) -> Circuit { + let input_len = self + .input + .len() + .try_into() + .expect("input length should fit into u32"); + + let mut items = Vec::new(); + while let Some(item) = self.parse_item() { + items.push(item); + } + + Circuit { + span: Span { + lo: 0, + hi: input_len, + }, + items, + } + } + + fn parse_item(&mut self) -> Option { + // TODO do I want to throw an error in the none case here? + let token = self.tokens.peek()?; + if token.kind == TokenKind::InstructionName { + let instruction = self.parse_instruction(); + //TODO use something else instead of unwrap here, I don't want it to panic + match self.tokens.peek().unwrap().kind { + TokenKind::Open(Brace) => { + return Some(Item::Block(self.parse_block(instruction))); + } + _ => { + return Some(Item::Line(self.parse_line(Some(instruction)))); + } + } + } else { + let line = self.parse_line(None); + return Some(Item::Line(line)); + } + } + + fn parse_instruction(&mut self) -> Instruction {} + + fn parse_block(&mut self, instruction: Instruction) -> Block { + let lo = instruction.span.lo; + let mut items = Vec::new(); + self.expect(TokenKind::Newline); + while self + .tokens + .peek() + .is_some_and(|t| t.kind != TokenKind::Close(Brace)) + { + let item = self.parse_item(); + if item.is_some() { + items.push(item.unwrap()); + } + } + let closing_brace = self.expect(TokenKind::Close(Brace)); + let hi = closing_brace.span.hi; + Block { + span: Span { lo, hi }, + block_instruction: instruction, + items, + } + } + + fn parse_line(&mut self, instruction: Option) -> Line { + let lo: u32; + if instruction.is_none() { + // PROCEDURE! + } else { + return Line { + span: instruction.unwrap().span, + kind: LineKind::Instruction(instruction.unwrap()), + }; + } + } +} From d2502249d6dc3ecbbabedfe024894b5784bb6736 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 15:22:13 -0700 Subject: [PATCH 04/67] update lex --- source/compiler/qsc_stim_parser/src/lex.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 0d573e99608..223c9c7b27b 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -15,7 +15,6 @@ pub struct Token { #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum TokenKind { Newline, // \n - Procedure, // #! ... Uint, // unsigned integers Double, // floating-point numbers InstructionName, // H, X, CNOT, etc. @@ -26,7 +25,6 @@ pub enum TokenKind { Close(Delim), // ) } Star, // * Bang, // ! - Minus, // - Comma, // , Unknown, // unknown token } @@ -35,7 +33,6 @@ impl Display for TokenKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { TokenKind::Newline => f.write_str("newline"), - TokenKind::Procedure => f.write_str("procedure"), TokenKind::Uint => f.write_str("uint"), TokenKind::Double => f.write_str("double"), TokenKind::InstructionName => f.write_str("instruction_name"), @@ -46,7 +43,6 @@ impl Display for TokenKind { TokenKind::Close(delim) => write!(f, "close({})", delim), TokenKind::Star => write!(f, "star"), TokenKind::Bang => write!(f, "bang"), - TokenKind::Minus => write!(f, "minus"), TokenKind::Comma => write!(f, "comma"), TokenKind::Unknown => write!(f, "unknown"), } @@ -90,6 +86,10 @@ impl<'a> Lexer<'a> { while self.chars.next_if(|i| f(i.1)).is_some() {} } + fn newline(&mut self) { + self.eat_while(|c| c == '\n'); + } + fn whitespace(&mut self) { self.eat_while(char::is_whitespace); } @@ -143,14 +143,17 @@ impl Iterator for Lexer<'_> { let (offset, c) = self.chars.next()?; let lo: u32 = offset.try_into().expect("offset should fit into u32"); let token_kind = match c { - '\n' => TokenKind::Newline, + '\n' => { + self.newline(); + TokenKind::Newline + } ' ' | '\t' => { self.whitespace(); return self.next(); } '#' => { if self.chars.next_if(|(_, c)| *c == '!').is_some() { - TokenKind::Procedure + TokenKind::InstructionName } else { self.comment(); return self.next(); @@ -162,7 +165,6 @@ impl Iterator for Lexer<'_> { '}' => TokenKind::Close(Brace), '*' => TokenKind::Star, '!' => TokenKind::Bang, - '-' => TokenKind::Minus, ',' => TokenKind::Comma, '0'..='9' => self.scan_number(), 'A'..='Z' | 'a'..='z' => self.scan_identifier(lo as usize), @@ -181,4 +183,4 @@ impl Iterator for Lexer<'_> { } } -// TODO: PAULI CAN COME IMMEDIATELY BEFORE THE UINT (NO SPACE) +//TODO: Deal with escaping From 5462765f5684c76321226da8fe68e5f020800c52 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 15:22:26 -0700 Subject: [PATCH 05/67] update package name --- source/compiler/qsc_stim_parser/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/compiler/qsc_stim_parser/Cargo.toml b/source/compiler/qsc_stim_parser/Cargo.toml index 50860fd23e1..6f3e59d207c 100644 --- a/source/compiler/qsc_stim_parser/Cargo.toml +++ b/source/compiler/qsc_stim_parser/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "qsc_my_parser" +name = "qsc_stim_parser" edition.workspace = true version.workspace = true From c66a59d5a032df151d10e048bc1b8fe571b490d8 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 15:22:48 -0700 Subject: [PATCH 06/67] first finalized parser --- source/compiler/qsc_stim_parser/src/parser.rs | 268 +++++++++++++++--- 1 file changed, 226 insertions(+), 42 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 30ffd62da1c..9b83e796fc9 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -1,4 +1,5 @@ use crate::lex::Delim::Brace; +use crate::lex::Delim::Paren; use crate::lex::Lexer; use crate::lex::Token; use crate::lex::TokenKind; @@ -6,6 +7,7 @@ use qsc_data_structures::span::Span; use std::{ fmt::{self, Display, Formatter}, iter::Peekable, + str::FromStr, }; pub struct Circuit { @@ -18,20 +20,15 @@ pub enum Item { Block(Block), } -pub struct Block { - pub span: Span, - pub block_instruction: Instruction, // currently, only the "REPEAT" instruction is supported - pub items: Vec, -} - pub struct Line { pub span: Span, - pub kind: LineKind, + pub instruction: Instruction, } -pub enum LineKind { - Instruction(Instruction), - Comment(String), +pub struct Block { + pub span: Span, + pub block_instruction: Instruction, // currently, only the "REPEAT" instruction is supported + pub items: Vec, } pub struct Instruction { @@ -63,9 +60,7 @@ pub enum TargetKind { pauli: Pauli, value: u32, }, - Combiner { - value: bool, - }, + Combiner, } pub enum Pauli { @@ -74,6 +69,23 @@ pub enum Pauli { Z, } +impl FromStr for Pauli { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "X" => Ok(Pauli::X), + "Y" => Ok(Pauli::Y), + "Z" => Ok(Pauli::Z), + _ => Err(()), + } + } +} + +pub fn parse(input: &str) -> Circuit { + Parser::new(input).parse() +} + struct Parser<'a> { input: &'a str, tokens: Peekable>, @@ -117,42 +129,43 @@ impl<'a> Parser<'a> { } fn parse_item(&mut self) -> Option { - // TODO do I want to throw an error in the none case here? - let token = self.tokens.peek()?; - if token.kind == TokenKind::InstructionName { + // TODO WHAT IF IT STARTS WITH A NEWLINE? + + if let TokenKind::InstructionName = self.tokens.peek()?.kind { + // Could be the start of a block or of a line let instruction = self.parse_instruction(); - //TODO use something else instead of unwrap here, I don't want it to panic - match self.tokens.peek().unwrap().kind { - TokenKind::Open(Brace) => { + if let Some(token) = self.tokens.peek() { + if token.kind == TokenKind::Open(Brace) { return Some(Item::Block(self.parse_block(instruction))); } - _ => { - return Some(Item::Line(self.parse_line(Some(instruction)))); - } } + return Some(Item::Line(self.parse_line(instruction))); } else { - let line = self.parse_line(None); - return Some(Item::Line(line)); + // TODO error! The start of every item should be an instruction; + None } } - fn parse_instruction(&mut self) -> Instruction {} - fn parse_block(&mut self, instruction: Instruction) -> Block { let lo = instruction.span.lo; let mut items = Vec::new(); + self.expect(TokenKind::Open(Brace)); self.expect(TokenKind::Newline); - while self - .tokens - .peek() - .is_some_and(|t| t.kind != TokenKind::Close(Brace)) - { - let item = self.parse_item(); - if item.is_some() { - items.push(item.unwrap()); + loop { + if self + .tokens + .peek() + .is_some_and(|t| t.kind == TokenKind::Close(Brace)) + { + break; + } + match self.parse_item() { + Some(item) => items.push(item), + None => break, } } let closing_brace = self.expect(TokenKind::Close(Brace)); + self.expect(TokenKind::Newline); let hi = closing_brace.span.hi; Block { span: Span { lo, hi }, @@ -161,15 +174,186 @@ impl<'a> Parser<'a> { } } - fn parse_line(&mut self, instruction: Option) -> Line { - let lo: u32; - if instruction.is_none() { - // PROCEDURE! + fn parse_line(&mut self, instruction: Instruction) -> Line { + self.expect(TokenKind::Newline); + Line { + span: instruction.span, + instruction, + } + } + + fn parse_instruction(&mut self) -> Instruction { + let name_token = self.expect(TokenKind::InstructionName); + let lo = name_token.span.lo; + let name = self.extract_string(name_token, None); + + let tag_token = self.tokens.next_if(|t| t.kind == TokenKind::Tag); + let tag: Option; + match tag_token { + Some(tag_token) => { + tag = Some(self.extract_string( + tag_token, + Some(Span { + lo: tag_token.span.lo + 1, + hi: tag_token.span.hi - 1, + }), + )); // Remove the surrounding brackets + } + None => { + tag = None; + } + } + + let mut args = Vec::new(); + let mut targets = Vec::new(); + + if self + .tokens + .peek() + .is_some_and(|t| t.kind == TokenKind::Open(Paren)) + { + self.expect(TokenKind::Open(Paren)); // consume '(' + // Parse first arg (no leading comma) + if self + .tokens + .peek() + .is_some_and(|t| t.kind != TokenKind::Close(Paren)) + { + let arg = self.expect(TokenKind::Double); + args.push(self.extract_double(arg, None)); + } + // Each subsequent arg must be preceded by a comma + while self + .tokens + .peek() + .is_some_and(|t| t.kind != TokenKind::Close(Paren)) + { + self.expect(TokenKind::Comma); + let arg = self.expect(TokenKind::Double); + args.push(self.extract_double(arg, None)); + } + self.expect(TokenKind::Close(Paren)); + } + + while let Some(&token) = self.tokens.peek() { + if !self.is_target_start(&token) { + break; + } + targets.push(self.parse_target()); + } + + let hi = targets[targets.len() - 1].span.hi; + + Instruction { + span: Span { lo, hi }, + name, + tag, + args, + targets, + } + } + + fn is_target_start(&self, token: &Token) -> bool { + match token.kind { + TokenKind::Uint + | TokenKind::Rec + | TokenKind::Sweep + | TokenKind::Bang + | TokenKind::Star => true, + TokenKind::InstructionName => { + let text = self.extract_string(*token, None); + text.starts_with('X') || text.starts_with('Y') || text.starts_with('Z') // STARTS WITH PAULI + } + _ => false, + } + } + + fn parse_target(&mut self) -> Target { + let negated_token = self.tokens.next_if(|t| t.kind == TokenKind::Bang); + let negated = negated_token.is_some(); + let first_token = self.tokens.next().expect("target empty"); + let lo = negated_token.map_or(first_token.span.lo, |t| t.span.lo); + let span = Span { + lo, + hi: first_token.span.hi, + }; + + match first_token.kind { + TokenKind::Uint => Target { + span, + kind: TargetKind::Qubit { + negated, + value: self.extract_uint(first_token, None), + }, + }, + TokenKind::InstructionName => Target { + span, + kind: TargetKind::Pauli { + negated, + pauli: self + .extract_string( + first_token, + Some(Span { + lo: span.lo, + hi: span.lo + 1, + }), + ) + .parse::() + .unwrap(), + value: self.extract_uint( + first_token, + Some(Span { + lo: span.lo + 1, + hi: span.hi, + }), + ), + }, + }, // Already validated + TokenKind::Rec => Target { + span, + kind: TargetKind::MeasurementRecord { + value: self.extract_uint( + first_token, + Some(Span { + lo: span.lo + 5, + hi: span.hi - 1, + }), + ), // Strips 'rec[-' prefix and trailing ']' TODO validate it + }, + }, + TokenKind::Sweep => Target { + span, + kind: TargetKind::SweepBit { + value: self.extract_uint( + first_token, + Some(Span { + lo: span.lo + 6, + hi: span.hi - 1, + }), + ), + }, // Strips 'sweep[' prefix and trailing ']' TODO validate it + }, + TokenKind::Star => Target { + span, + kind: TargetKind::Combiner, + }, + _ => panic!("Unexpected target kind"), + } + } + + fn extract_uint(&mut self, token: Token, span: Option) -> u32 { + self.extract_string(token, span).parse().unwrap() + } + + fn extract_double(&mut self, token: Token, span: Option) -> f64 { + self.extract_string(token, span).parse().unwrap() + } + + fn extract_string(&self, token: Token, span: Option) -> String { + if let Some(span) = span { + self.input[span.lo as usize..span.hi as usize].to_string() } else { - return Line { - span: instruction.unwrap().span, - kind: LineKind::Instruction(instruction.unwrap()), - }; + self.input[token.span.lo as usize..token.span.hi as usize].to_string() } } } From 2c03a107fbc43057bbde67268df80fbc65aac716 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 21:16:05 -0700 Subject: [PATCH 07/67] fix bugs, improve debugging --- Cargo.lock | 8 ++ .../qsc_stim_parser/examples/lex_stim.rs | 4 +- .../qsc_stim_parser/examples/parse_stim.rs | 97 +++++++++++++++++++ source/compiler/qsc_stim_parser/src/lex.rs | 18 ++-- source/compiler/qsc_stim_parser/src/parser.rs | 26 +++-- 5 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 source/compiler/qsc_stim_parser/examples/parse_stim.rs diff --git a/Cargo.lock b/Cargo.lock index ca74fbffded..e7cea171726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2518,6 +2518,14 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "qsc_stim_parser" +version = "0.0.0" +dependencies = [ + "enum-iterator", + "qsc_data_structures", +] + [[package]] name = "qsc_wasm" version = "0.0.0" diff --git a/source/compiler/qsc_stim_parser/examples/lex_stim.rs b/source/compiler/qsc_stim_parser/examples/lex_stim.rs index a900a897672..c5fb00e6797 100644 --- a/source/compiler/qsc_stim_parser/examples/lex_stim.rs +++ b/source/compiler/qsc_stim_parser/examples/lex_stim.rs @@ -1,4 +1,4 @@ -use qsc_my_parser::lex::Lexer; +use qsc_stim_parser::lex::Lexer; fn main() { let stim_code = "\ @@ -18,7 +18,7 @@ OBSERVABLE_INCLUDE(0) rec[-1] for token in lexer { let text = &stim_code[token.span.lo as usize..token.span.hi as usize]; let text_display = match token.kind { - qsc_my_parser::lex::TokenKind::Newline => "\\n".to_string(), + qsc_stim_parser::lex::TokenKind::Newline => "\\n".to_string(), _ => format!("{:?}", text), }; println!( diff --git a/source/compiler/qsc_stim_parser/examples/parse_stim.rs b/source/compiler/qsc_stim_parser/examples/parse_stim.rs new file mode 100644 index 00000000000..90f86e3f38e --- /dev/null +++ b/source/compiler/qsc_stim_parser/examples/parse_stim.rs @@ -0,0 +1,97 @@ +use qsc_stim_parser::parser::{Circuit, Instruction, Item, Pauli, Target, TargetKind, parse}; + +fn print_circuit(circuit: &Circuit) { + println!("(circuit"); + for item in &circuit.items { + print_item(item, 1); + } + println!(")"); +} + +fn print_item(item: &Item, indent: usize) { + let pad = " ".repeat(indent); + match item { + Item::Line(line) => { + print!("{pad}("); + print_instruction(&line.instruction); + println!(")"); + } + Item::Block(block) => { + print!("{pad}("); + print_instruction(&block.block_instruction); + println!(); + for item in &block.items { + print_item(item, indent + 1); + } + println!("{pad})"); + } + } +} + +fn print_instruction(instr: &Instruction) { + print!("{}", instr.name); + if let Some(tag) = &instr.tag { + print!("[{}]", tag); + } + if !instr.args.is_empty() { + for arg in &instr.args { + print!(" {}", arg); + } + } + for target in &instr.targets { + print!(" "); + print_target(target); + } +} + +fn print_target(target: &Target) { + match &target.kind { + TargetKind::Qubit { negated, value } => { + if *negated { + print!("!"); + } + print!("{}", value); + } + TargetKind::MeasurementRecord { value } => print!("rec[-{}]", value), + TargetKind::SweepBit { value } => print!("sweep[{}]", value), + TargetKind::Pauli { + negated, + pauli, + value, + } => { + if *negated { + print!("!"); + } + let p = match pauli { + Pauli::X => "X", + Pauli::Y => "Y", + Pauli::Z => "Z", + }; + print!("{}{}", p, value); + } + TargetKind::Combiner => print!("*"), + } +} + +fn main() { + let stim_code = "\ +H 0 1 +CNOT 0 1 +X_ERROR(0.1) 0 1 +M 0 1 +DETECTOR[D0] rec[-1] rec[-2] +OBSERVABLE_INCLUDE(0) rec[-1] +REPEAT 100 { + H 0 + CNOT 0 1 + M 0 1 + DETECTOR rec[-1] rec[-2] +} +"; + + println!("Input:\n{stim_code}"); + println!("{:=<60}", ""); + + let circuit = parse(stim_code); + print_circuit(&circuit); +} diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 223c9c7b27b..b7ce12492df 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -104,14 +104,7 @@ impl<'a> Lexer<'a> { self.eat_while(|c| c.is_ascii_digit()); return TokenKind::Double; } - return TokenKind::Uint; - } - - fn scan_bracketed(&mut self) { - if self.chars.next_if(|(_, c)| *c == '[').is_some() { - self.eat_while(|c| c != ']'); - self.chars.next_if(|(_, c)| *c == ']'); - } + TokenKind::Uint } fn scan_identifier(&mut self, lo: usize) -> TokenKind { @@ -123,11 +116,13 @@ impl<'a> Lexer<'a> { // TODO: What if some identifier starts with "rec" but is not a rec token? match &self.input[lo..hi] { "rec" => { - self.scan_bracketed(); + self.eat_while(|c| c != ']'); + self.chars.next_if(|(_, c)| *c == ']'); TokenKind::Rec } "sweep" => { - self.scan_bracketed(); + self.eat_while(|c| c != ']'); + self.chars.next_if(|(_, c)| *c == ']'); TokenKind::Sweep } _ => TokenKind::InstructionName, @@ -169,7 +164,8 @@ impl Iterator for Lexer<'_> { '0'..='9' => self.scan_number(), 'A'..='Z' | 'a'..='z' => self.scan_identifier(lo as usize), '[' => { - self.scan_bracketed(); + self.eat_while(|c| c != ']'); + self.chars.next_if(|(_, c)| *c == ']'); TokenKind::Tag } _ => TokenKind::Unknown, diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 9b83e796fc9..61e40decb24 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -4,33 +4,34 @@ use crate::lex::Lexer; use crate::lex::Token; use crate::lex::TokenKind; use qsc_data_structures::span::Span; -use std::{ - fmt::{self, Display, Formatter}, - iter::Peekable, - str::FromStr, -}; +use std::{iter::Peekable, str::FromStr}; +#[derive(Debug)] pub struct Circuit { pub span: Span, pub items: Vec, } +#[derive(Debug)] pub enum Item { Line(Line), Block(Block), } +#[derive(Debug)] pub struct Line { pub span: Span, pub instruction: Instruction, } +#[derive(Debug)] pub struct Block { pub span: Span, pub block_instruction: Instruction, // currently, only the "REPEAT" instruction is supported pub items: Vec, } +#[derive(Debug)] pub struct Instruction { pub span: Span, pub name: String, @@ -39,11 +40,13 @@ pub struct Instruction { pub targets: Vec, } +#[derive(Debug)] pub struct Target { pub span: Span, pub kind: TargetKind, } +#[derive(Debug)] pub enum TargetKind { Qubit { negated: bool, @@ -63,6 +66,7 @@ pub enum TargetKind { Combiner, } +#[derive(Debug)] pub enum Pauli { X, Y, @@ -107,6 +111,14 @@ impl<'a> Parser<'a> { token } + fn expect_number(&mut self) -> Token { + let token = self.tokens.next().expect("expected number"); + if token.kind != TokenKind::Uint && token.kind != TokenKind::Double { + panic!("expected number, got {:?}", token.kind); + } + token + } + pub fn parse(&mut self) -> Circuit { let input_len = self .input @@ -219,7 +231,7 @@ impl<'a> Parser<'a> { .peek() .is_some_and(|t| t.kind != TokenKind::Close(Paren)) { - let arg = self.expect(TokenKind::Double); + let arg = self.expect_number(); args.push(self.extract_double(arg, None)); } // Each subsequent arg must be preceded by a comma @@ -229,7 +241,7 @@ impl<'a> Parser<'a> { .is_some_and(|t| t.kind != TokenKind::Close(Paren)) { self.expect(TokenKind::Comma); - let arg = self.expect(TokenKind::Double); + let arg = self.expect_number(); args.push(self.extract_double(arg, None)); } self.expect(TokenKind::Close(Paren)); From 561e3e95f22cb912b8b2b4a043590255dac30cde Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 21:34:05 -0700 Subject: [PATCH 08/67] make tests consume from arbitrary example.stim file --- .../qsc_stim_parser/examples/lex_stim.rs | 12 ++++-------- .../qsc_stim_parser/examples/parse_stim.rs | 18 +++--------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/source/compiler/qsc_stim_parser/examples/lex_stim.rs b/source/compiler/qsc_stim_parser/examples/lex_stim.rs index c5fb00e6797..c42e2238589 100644 --- a/source/compiler/qsc_stim_parser/examples/lex_stim.rs +++ b/source/compiler/qsc_stim_parser/examples/lex_stim.rs @@ -1,20 +1,16 @@ use qsc_stim_parser::lex::Lexer; +use std::fs; fn main() { - let stim_code = "\ -H 0 -CNOT 0 1 -M 0 1 -DETECTOR rec[-1] rec[-2] -OBSERVABLE_INCLUDE(0) rec[-1] -"; + let stim_code = + fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); println!("Input:\n{stim_code}"); println!("{:-<50}", ""); println!("{:<20} {:<10} {:}", "TOKEN KIND", "SPAN", "TEXT"); println!("{:-<50}", ""); - let lexer = Lexer::new(stim_code); + let lexer = Lexer::new(&stim_code); for token in lexer { let text = &stim_code[token.span.lo as usize..token.span.hi as usize]; let text_display = match token.kind { diff --git a/source/compiler/qsc_stim_parser/examples/parse_stim.rs b/source/compiler/qsc_stim_parser/examples/parse_stim.rs index 90f86e3f38e..983bdc6b223 100644 --- a/source/compiler/qsc_stim_parser/examples/parse_stim.rs +++ b/source/compiler/qsc_stim_parser/examples/parse_stim.rs @@ -74,24 +74,12 @@ fn print_target(target: &Target) { } fn main() { - let stim_code = "\ -H 0 1 -CNOT 0 1 -X_ERROR(0.1) 0 1 -M 0 1 -DETECTOR[D0] rec[-1] rec[-2] -OBSERVABLE_INCLUDE(0) rec[-1] -REPEAT 100 { - H 0 - CNOT 0 1 - M 0 1 - DETECTOR rec[-1] rec[-2] -} -"; + let stim_code = std::fs::read_to_string("examples/example.stim") + .expect("Failed to read examples/example.stim"); println!("Input:\n{stim_code}"); println!("{:=<60}", ""); - let circuit = parse(stim_code); + let circuit = parse(&stim_code); print_circuit(&circuit); } From c07ee11dd9b2e42c00ee10304e67384fe3587f87 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 22:04:40 -0700 Subject: [PATCH 09/67] fix repeated newline bug, improve parsing of custom instructions --- source/compiler/qsc_stim_parser/src/lex.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index b7ce12492df..8623046f781 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -86,16 +86,13 @@ impl<'a> Lexer<'a> { while self.chars.next_if(|i| f(i.1)).is_some() {} } - fn newline(&mut self) { - self.eat_while(|c| c == '\n'); - } - fn whitespace(&mut self) { self.eat_while(char::is_whitespace); } fn comment(&mut self) { self.eat_while(|c| c != '\n'); + self.whitespace(); } fn scan_number(&mut self) -> TokenKind { @@ -139,7 +136,7 @@ impl Iterator for Lexer<'_> { let lo: u32 = offset.try_into().expect("offset should fit into u32"); let token_kind = match c { '\n' => { - self.newline(); + self.whitespace(); TokenKind::Newline } ' ' | '\t' => { @@ -148,6 +145,7 @@ impl Iterator for Lexer<'_> { } '#' => { if self.chars.next_if(|(_, c)| *c == '!').is_some() { + self.eat_while(|c| !c.is_whitespace()); TokenKind::InstructionName } else { self.comment(); From a73fc12410f8f54b982f58ff9eb1828a07007d5f Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 22:05:11 -0700 Subject: [PATCH 10/67] fix bug of custom function without target --- source/compiler/qsc_stim_parser/src/parser.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 61e40decb24..e714f33434f 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -254,7 +254,10 @@ impl<'a> Parser<'a> { targets.push(self.parse_target()); } - let hi = targets[targets.len() - 1].span.hi; + let hi = targets + .last() + .map(|t| t.span.hi) + .unwrap_or(name_token.span.hi); Instruction { span: Span { lo, hi }, From 85b0955aa6d76af547642442e8d324835a942081 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 9 Jun 2026 22:16:37 -0700 Subject: [PATCH 11/67] handle scientific notation in the lexer --- source/compiler/qsc_stim_parser/src/lex.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 8623046f781..cd25f25a4f6 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -97,11 +97,26 @@ impl<'a> Lexer<'a> { fn scan_number(&mut self) -> TokenKind { self.eat_while(|c| c.is_ascii_digit()); + let mut is_double = false; if self.chars.next_if(|(_, c)| *c == '.').is_some() { self.eat_while(|c| c.is_ascii_digit()); - return TokenKind::Double; + is_double = true; + } + if self + .chars + .next_if(|(_, c)| *c == 'e' || *c == 'E') + .is_some() + { + // scientific notation + self.chars.next_if(|(_, c)| *c == '+' || *c == '-'); + self.eat_while(|c| c.is_ascii_digit()); + is_double = true; + } + if is_double { + TokenKind::Double + } else { + TokenKind::Uint } - TokenKind::Uint } fn scan_identifier(&mut self, lo: usize) -> TokenKind { From a6cbe239fc7857e2833103c01080b1b7ed48f4e4 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 11 Jun 2026 11:20:30 -0700 Subject: [PATCH 12/67] improve lex and parse manual testing --- .../qsc_stim_parser/examples/lex_stim.rs | 18 +++-- .../qsc_stim_parser/examples/parse_stim.rs | 70 ++++++++++--------- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/source/compiler/qsc_stim_parser/examples/lex_stim.rs b/source/compiler/qsc_stim_parser/examples/lex_stim.rs index c42e2238589..ef1371cc151 100644 --- a/source/compiler/qsc_stim_parser/examples/lex_stim.rs +++ b/source/compiler/qsc_stim_parser/examples/lex_stim.rs @@ -1,14 +1,16 @@ use qsc_stim_parser::lex::Lexer; use std::fs; +use std::io::Write; fn main() { let stim_code = fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); - println!("Input:\n{stim_code}"); - println!("{:-<50}", ""); - println!("{:<20} {:<10} {:}", "TOKEN KIND", "SPAN", "TEXT"); - println!("{:-<50}", ""); + let mut out = + fs::File::create("examples/lex_output.txt").expect("Failed to create output file"); + + writeln!(out, "{:<20} {:<10} {:}", "TOKEN KIND", "SPAN", "TEXT").unwrap(); + writeln!(out, "{:-<50}", "").unwrap(); let lexer = Lexer::new(&stim_code); for token in lexer { @@ -17,11 +19,15 @@ fn main() { qsc_stim_parser::lex::TokenKind::Newline => "\\n".to_string(), _ => format!("{:?}", text), }; - println!( + writeln!( + out, "{:<20} {:<10} {}", token.kind.to_string(), format!("{}..{}", token.span.lo, token.span.hi), text_display - ); + ) + .unwrap(); } + + println!("Wrote examples/lex_output.txt"); } diff --git a/source/compiler/qsc_stim_parser/examples/parse_stim.rs b/source/compiler/qsc_stim_parser/examples/parse_stim.rs index 983bdc6b223..59f95b9dbea 100644 --- a/source/compiler/qsc_stim_parser/examples/parse_stim.rs +++ b/source/compiler/qsc_stim_parser/examples/parse_stim.rs @@ -1,85 +1,89 @@ use qsc_stim_parser::parser::{Circuit, Instruction, Item, Pauli, Target, TargetKind, parse}; +use std::fs; +use std::io::Write; -fn print_circuit(circuit: &Circuit) { - println!("(circuit"); +fn write_circuit(out: &mut impl Write, circuit: &Circuit) { + writeln!(out, "(circuit").unwrap(); for item in &circuit.items { - print_item(item, 1); + write_item(out, item, 1); } - println!(")"); + writeln!(out, ")").unwrap(); } -fn print_item(item: &Item, indent: usize) { +fn write_item(out: &mut impl Write, item: &Item, indent: usize) { let pad = " ".repeat(indent); match item { Item::Line(line) => { - print!("{pad}("); - print_instruction(&line.instruction); - println!(")"); + write!(out, "{pad}(").unwrap(); + write_instruction(out, &line.instruction); + writeln!(out, ")").unwrap(); } Item::Block(block) => { - print!("{pad}("); - print_instruction(&block.block_instruction); - println!(); + write!(out, "{pad}(").unwrap(); + write_instruction(out, &block.block_instruction); + writeln!(out).unwrap(); for item in &block.items { - print_item(item, indent + 1); + write_item(out, item, indent + 1); } - println!("{pad})"); + writeln!(out, "{pad})").unwrap(); } } } -fn print_instruction(instr: &Instruction) { - print!("{}", instr.name); +fn write_instruction(out: &mut impl Write, instr: &Instruction) { + write!(out, "{}", instr.name).unwrap(); if let Some(tag) = &instr.tag { - print!("[{}]", tag); + write!(out, "[{}]", tag).unwrap(); } if !instr.args.is_empty() { for arg in &instr.args { - print!(" {}", arg); + write!(out, " {}", arg).unwrap(); } } for target in &instr.targets { - print!(" "); - print_target(target); + write!(out, " ").unwrap(); + write_target(out, target); } } -fn print_target(target: &Target) { +fn write_target(out: &mut impl Write, target: &Target) { match &target.kind { TargetKind::Qubit { negated, value } => { if *negated { - print!("!"); + write!(out, "!").unwrap(); } - print!("{}", value); + write!(out, "{}", value).unwrap(); } - TargetKind::MeasurementRecord { value } => print!("rec[-{}]", value), - TargetKind::SweepBit { value } => print!("sweep[{}]", value), + TargetKind::MeasurementRecord { value } => write!(out, "rec[-{}]", value).unwrap(), + TargetKind::SweepBit { value } => write!(out, "sweep[{}]", value).unwrap(), TargetKind::Pauli { negated, pauli, value, } => { if *negated { - print!("!"); + write!(out, "!").unwrap(); } let p = match pauli { Pauli::X => "X", Pauli::Y => "Y", Pauli::Z => "Z", }; - print!("{}{}", p, value); + write!(out, "{}{}", p, value).unwrap(); } - TargetKind::Combiner => print!("*"), + TargetKind::Combiner => write!(out, "*").unwrap(), } } fn main() { - let stim_code = std::fs::read_to_string("examples/example.stim") - .expect("Failed to read examples/example.stim"); - - println!("Input:\n{stim_code}"); - println!("{:=<60}", ""); + let stim_code = + fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); let circuit = parse(&stim_code); - print_circuit(&circuit); + + let mut out = + fs::File::create("examples/parse_output.txt").expect("Failed to create output file"); + write_circuit(&mut out, &circuit); + + println!("Wrote examples/parse_output.txt"); } From 9c724bb0210075013701348223f3441325483301 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 11 Jun 2026 17:54:24 -0700 Subject: [PATCH 13/67] save initial qir emitting code --- source/compiler/qsc_stim_parser/src/lib.rs | 4 + source/compiler/qsc_stim_parser/src/parser.rs | 3 + source/compiler/qsc_stim_parser/src/qir.rs | 333 ++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 source/compiler/qsc_stim_parser/src/qir.rs diff --git a/source/compiler/qsc_stim_parser/src/lib.rs b/source/compiler/qsc_stim_parser/src/lib.rs index b481f475069..c59408e5bce 100644 --- a/source/compiler/qsc_stim_parser/src/lib.rs +++ b/source/compiler/qsc_stim_parser/src/lib.rs @@ -1,2 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + pub mod lex; pub mod parser; +pub mod qir; diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index e714f33434f..f1788c1a0b6 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use crate::lex::Delim::Brace; use crate::lex::Delim::Paren; use crate::lex::Lexer; diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs new file mode 100644 index 00000000000..cc02e04e43f --- /dev/null +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use qsc_data_structures::target; + +use crate::parser::*; +use std::collections::HashSet; +use std::fmt::Write; + +enum InstructionKind { + PauliGate, + SingleQubitCliffordGate, + TwoQubitCliffordGate, + NoiseChannel, + CollapsingGate, + PairMeasurementGate, + GeneralizedPauliProductGate, + ControlFlow, + Annotations, + CustomInstruction, +} + +struct Emitter { + qir_output: String, + num_qubits: u32, + num_results: u32, + last_preselect_begin: Option, + num_preselect_expects: u32, + used_intrinsics: HashSet, + labels: Vec, +} + +impl Emitter { + fn new() -> Self { + Self { + qir_output: String::new(), + num_qubits: 0, + num_results: 0, + last_preselect_begin: None, + num_preselect_expects: 0, + used_intrinsics: HashSet::new(), + labels: Vec::new(), + } + } + + fn emit_circuit(&mut self, circuit: &Circuit) { + let items = &circuit.items; + for item in items { + self.emit_item(&item); + } + } + + fn emit_item(&mut self, item: &Item) { + match item { + Item::Block(block) => self.emit_block(block), + Item::Line(line) => self.emit_line(line), + } + } + + fn emit_block(&mut self, block: &Block) { + let Block { + block_instruction, + items, + .. + } = block; + + self.emit_instruction(block_instruction); + for item in items { + self.emit_item(item); + } + } + + fn emit_line(&mut self, line: &Line) { + let Line { instruction, .. } = line; + self.emit_instruction(instruction); + } + + fn emit_instruction(&mut self, instruction: &Instruction) { + match self.instruction_kind(instruction.name) { + InstructionKind::PauliGate => { + self.emit_pauli_gate(instruction); + } + InstructionKind::SingleQubitCliffordGate => { + self.emit_single_qubit_clifford_gate(instruction); + } + InstructionKind::TwoQubitCliffordGate => { + self.emit_two_qubit_clifford_gate(instruction); + } + InstructionKind::NoiseChannel => { + self.emit_noise_channel(instruction); + } + InstructionKind::CollapsingGate => { + self.emit_collapsing_gate(instruction); + } + InstructionKind::PairMeasurementGate => { + self.emit_pair_measurement_gate(instruction); + } + InstructionKind::GeneralizedPauliProductGate => { + self.emit_generalized_pauli_product_gate(instruction); + } + InstructionKind::ControlFlow => { + self.emit_control_flow(instruction); + } + InstructionKind::Annotations => { + self.emit_annotations(instruction); + } + InstructionKind::CustomInstruction => { + self.emit_custom_instruction(instruction); + } + } + } + + fn emit_pauli_gate(&mut self, instruction: &Instruction) { + let gate = instruction.name.to_lowercase(); + for target in &instruction.targets { + write!(self.qir_output, "__quantum__qis__{}__body", gate).unwrap(); + self.emit_target(&target); + } + } + + fn emit_single_qubit_clifford_gate(&mut self, instruction: &Instruction) { + let gate = instruction.name.to_lowercase(); + if gate == "h" || gate == "s" { + for target in &instruction.targets { + write!(self.qir_output, "__quantum__qis__{}__body", gate).unwrap(); + self.emit_target(&target); + } + } else if gate == "sqrt_x" { + // decomposed into H S H + for target in &instruction.targets { + write!(self.qir_output, "__quantum__qis__h__body").unwrap(); + self.emit_target(&target); + write!(self.qir_output, "__quantum__qis__s__body").unwrap(); + self.emit_target(&target); + write!(self.qir_output, "__quantum__qis__h__body").unwrap(); + self.emit_target(&target); + } + } + } + + fn emit_two_qubit_clifford_gate(&mut self, instruction: &Instruction) { + let gate = instruction.name.to_lowercase(); + if gate == "cz" { + let targets = &instruction.targets; + for pair in targets.chunks(2) { + let [control, target] = pair else { + unreachable!() + }; + write!(self.qir_output, "__quantum__qis__cz__body").unwrap(); + self.emit_target(&control); + self.emit_target(&target); + } + } + } + + fn emit_noise_channel(&mut self, instruction: &Instruction) {} + + fn emit_collapsing_gate(&mut self, instruction: &Instruction) { + let gate = instruction.name.to_lowercase(); + if gate == "r" { + for target in &instruction.targets { + write!(self.qir_output, "__quantum__qis__reset__body").unwrap(); + self.emit_target(&target); + } + } else if gate == "mr" { + for target in &instruction.targets { + write!(self.qir_output, "__quantum__qis__mresetz__body").unwrap(); + self.emit_target(&target); + } + } else if gate == "mrx" { + // decomposed into H MRZ H + for target in &instruction.targets { + write!(self.qir_output, "__quantum__qis__h__body").unwrap(); + self.emit_target(&target); + write!(self.qir_output, "__quantum__qis__mresetz__body").unwrap(); + self.emit_target(&target); + write!(self.qir_output, "__quantum__qis__h__body").unwrap(); + self.emit_target(&target); + } + } + } + + fn emit_pair_measurement_gate(&mut self, instruction: &Instruction) {} + + fn emit_generalized_pauli_product_gate(&mut self, instruction: &Instruction) {} + + fn emit_control_flow(&mut self, instruction: &Instruction) {} + + fn emit_annotations(&mut self, instruction: &Instruction) {} + + fn emit_custom_instruction(&mut self, instruction: &Instruction) { + let instructionName = instruction.name.to_lowercase(); + if instructionName == "#!preselect_begin" { + self.last_preselect_begin = match self.last_preselect_begin { + None => Some(0), + Some(n) => Some(n + 1), + }; + write!( + self.qir_output, + "preselect_begin_{}:", + self.last_preselect_begin.unwrap() // It shouldn't be none! + ) + .unwrap(); + } else if instructionName == "#!preselect_expect" { + write!(self.qir_output, "preselect_r{}", self.num_preselect_expects).unwrap(); + self.num_preselect_expects += 1; + write!(self.qir_output, " ").unwrap(); // whitespace + write!( + self.qir_output, + "= call i1 @__quantum__qis__read_result__body" + ) + .unwrap(); + self.emit_target(&instruction.targets[0]); + // EMIT BREAK, br i1 %preselect_r1, label %preselect_fail_1, label %continue_1 + // HAVE IT THE OTHER WAY AROUDN IF TARGETS[1] IS 1 + } + } + + fn emit_targets(&mut self, targets: &[Target]) { + write!(self.qir_output, "(").unwrap(); + for target in targets { + self.emit_target(target); + write!(self.qir_output, ", ").unwrap(); + } + self.qir_output.pop(); // remove trailing whitespace + self.qir_output.pop(); // remove trailing comma + write!(self.qir_output, ")\n").unwrap(); + } + + // fn emit_target(&mut self, target: &Target) { + // match target.kind { + // TargetKind::Qubit { negated, value } => { + // if negated { + // write!(self.qir_output, "!q{}", value).unwrap(); + // } else { + // write!(self.qir_output, "q{}", value).unwrap(); + // } + // } + // TargetKind::MeasurementRecord { value } => { + // write!(self.qir_output, "m{}", value).unwrap(); + // } + // TargetKind::SweepBit { value } => { + // write!(self.qir_output, "s{}", value).unwrap(); + // } + // TargetKind::Pauli { negated, pauli, value } => { + // if negated { + // write!(self.qir_output, "!").unwrap(); + // } + // match pauli { + // Pauli::X => write!(self.qir_output, "X").unwrap(), + // Pauli::Y => write!(self.qir_output, "Y").unwrap(), + // Pauli::Z => write!(self.qir_output, "Z").unwrap(), + // } + // write!(self.qir_output, "{}", value).unwrap(); + // } + // TargetKind::Combiner => { + // write!(self.qir_output, "combiner").unwrap(); + // } + // } + // } + + fn instruction_kind(name: &str) -> InstructionKind { + match name { + // Pauli Gates + "I" | "X" | "Y" | "Z" => InstructionKind::PauliGate, + + // Single Qubit Clifford Gates + "C_NXYZ" | "C_NZYX" | "C_XNYZ" | "C_XYNZ" | "C_XYZ" | "C_ZNYX" | "C_ZYNX" | "C_ZYX" + | "H" | "H_NXY" | "H_NXZ" | "H_NYZ" | "H_XY" | "H_XZ" | "H_YZ" | "S" | "SQRT_X" + | "SQRT_X_DAG" | "SQRT_Y" | "SQRT_Y_DAG" | "SQRT_Z" | "SQRT_Z_DAG" | "S_DAG" => { + InstructionKind::SingleQubitCliffordGate + } + + // Two Qubit Clifford Gates + "CNOT" | "CX" | "CXSWAP" | "CY" | "CZ" | "CZSWAP" | "II" | "ISWAP" | "ISWAP_DAG" + | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" | "SQRT_ZZ" | "SQRT_ZZ_DAG" + | "SWAP" | "SWAPCX" | "SWAPCZ" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" + | "ZCX" | "ZCY" | "ZCZ" => InstructionKind::TwoQubitCliffordGate, + + // Noise Channels + "CORRELATED_ERROR" + | "DEPOLARIZE1" + | "DEPOLARIZE2" + | "E" + | "ELSE_CORRELATED_ERROR" + | "HERALDED_ERASE" + | "HERALDED_PAULI_CHANNEL_1" + | "II_ERROR" + | "I_ERROR" + | "PAULI_CHANNEL_1" + | "PAULI_CHANNEL_2" + | "X_ERROR" + | "Y_ERROR" + | "Z_ERROR" => InstructionKind::NoiseChannel, + + // Collapsing Gates + "M" | "MR" | "MRX" | "MRY" | "MRZ" | "MX" | "MY" | "MZ" | "R" | "RX" | "RY" | "RZ" => { + InstructionKind::CollapsingGate + } + + // Pair Measurement Gates + "MXX" | "MYY" | "MZZ" => InstructionKind::PairMeasurementGate, + + // Generalized Pauli Product Gates + "MPP" | "SPP" | "SPP_DAG" => InstructionKind::GeneralizedPauliProductGate, + + // Control Flow + "REPEAT" => InstructionKind::ControlFlow, + + // Annotations + "DETECTOR" | "MPAD" | "OBSERVABLE_INCLUDE" | "QUBIT_COORDS" | "SHIFT_COORDS" + | "TICK" => InstructionKind::Annotations, + + "#!preselect_begin" | "#!preselect_expect" => InstructionKind::CustomInstruction, + _ => InstructionKind::CustomInstruction, + } + } + + fn append_header(&mut self) {} + + fn append_footer(&mut self) {} + + fn into_qir(&mut self, circuit: &Circuit) -> String { + self.emit_circuit(circuit); + self.append_header(); + self.append_footer(); + self.qir_output + } +} + +pub fn emit_qir(circuit: &Circuit) -> String { + Emitter::new().into_qir(circuit) +} From 3459edc9aea9d6f8bb225878ccf0b790484df316 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Fri, 12 Jun 2026 12:24:33 -0700 Subject: [PATCH 14/67] python, interop, and cpu-simulators changes --- source/compiler/qsc_eval/src/backend.rs | 86 +++--- .../qsc_eval/src/backend/noise_tests.rs | 12 +- .../qdk_package/qdk/_device/_atom/__init__.py | 10 +- source/qdk_package/qdk/_native.pyi | 22 +- source/qdk_package/src/qir_simulation.rs | 50 ++-- .../src/qir_simulation/correlated_noise.rs | 15 +- .../qir_simulation/correlated_noise/tests.rs | 42 ++- .../src/qir_simulation/cpu_simulators.rs | 33 +-- .../csv_dir_test/test_noise_intrinsic.csv | 2 +- .../tests/test_adaptive_cpu_noise.py | 2 +- .../tests/test_clifford_simulator.py | 6 +- .../tests/test_correlated_noise.py | 79 +++--- .../tests/test_simulators_gates_noisy.py | 56 ++++ .../tests/test_sparse_simulator.py | 5 +- source/simulators/benches/sim_time.rs | 10 +- source/simulators/benches/sim_time_noisy.rs | 7 +- .../src/cpu_full_state_simulator.rs | 150 +++-------- .../src/cpu_full_state_simulator/noise.rs | 64 ----- .../correlated_noise.rs | 4 +- source/simulators/src/noise_config.rs | 254 ++++++++++-------- source/simulators/src/noise_config/tests.rs | 134 ++++----- source/simulators/src/noise_config/uq1_63.rs | 9 +- source/simulators/src/stabilizer_simulator.rs | 143 ++++------ .../src/stabilizer_simulator/noise.rs | 62 ----- 24 files changed, 560 insertions(+), 697 deletions(-) delete mode 100644 source/simulators/src/cpu_full_state_simulator/noise.rs delete mode 100644 source/simulators/src/stabilizer_simulator/noise.rs diff --git a/source/compiler/qsc_eval/src/backend.rs b/source/compiler/qsc_eval/src/backend.rs index 3c4ae0e8ee1..f065a699f3f 100644 --- a/source/compiler/qsc_eval/src/backend.rs +++ b/source/compiler/qsc_eval/src/backend.rs @@ -10,10 +10,11 @@ use ndarray::Array2; use num_bigint::BigUint; use num_complex::Complex; use num_traits::Zero; -use qdk_simulators::cpu_full_state_simulator::noise::{Fault, PauliFault}; -use qdk_simulators::noise_config::{CumulativeNoiseConfig, CumulativeNoiseTable}; -use qdk_simulators::stabilizer_simulator::{self, StabilizerSimulator}; -use qdk_simulators::{MeasurementResult, NearlyZero, Simulator as _, SparseStateSim}; +use qdk_simulators::{ + MeasurementResult, NearlyZero, Simulator as _, SparseStateSim, + noise_config::{CumulativeNoiseConfig, CumulativeNoiseTable, FaultTerm}, + stabilizer_simulator::StabilizerSimulator, +}; use qsc_data_structures::index_map::IndexMap; use rand::{Rng, RngExt}; use rand::{SeedableRng, rngs::StdRng}; @@ -537,7 +538,7 @@ pub struct SparseSim { /// Noiseless Sparse simulator to be used by this instance. pub sim: SparseStateSim, /// Noise configuration for this simulator instance, which defines the probabilities of different faults occurring during simulation. - pub noise_config: Option>, + pub noise_config: Option, /// Pauli noise that is applied after a gate or before a measurement is executed. /// Service functions aren't subject to noise. /// Note: this is legacy functionality maintained for backward compatibility. @@ -579,7 +580,7 @@ impl SparseSim { } #[must_use] - pub fn new_with_noise_config(noise_config: CumulativeNoiseConfig) -> Self { + pub fn new_with_noise_config(noise_config: CumulativeNoiseConfig) -> Self { Self { sim: SparseStateSim::new(None), noise_config: Some(noise_config), @@ -615,7 +616,7 @@ impl SparseSim { fn apply_faults( &mut self, - get_table: impl Fn(&CumulativeNoiseConfig) -> &CumulativeNoiseTable, + get_table: impl Fn(&CumulativeNoiseConfig) -> &CumulativeNoiseTable, qs: &[usize], ) { if self.rng.is_none() { @@ -633,57 +634,39 @@ impl SparseSim { .noise_config .take() .expect("noise config should always be present"); - let noise_table = get_table(&noise_config); - if noise_table.loss > 0.0 { - // Check each qubit for loss before applying other faults, since loss will prevent other faults from being applied and also prevent gates from executing. - for &q in qs { + let fault = get_table(&noise_config) + .sampler + .sample(self.rng.as_mut().expect("RNG should be present")); + + if let Some(fault) = fault { + assert!(fault.0.len() == qs.len()); + for (&q, term) in qs.iter().zip(fault.0.iter()) { if self.is_qubit_lost(q) { continue; } - let p = self - .rng - .as_mut() - .expect("RNG should be present") - .random_range(0.0..1.0); - if p < noise_table.loss { - // The qubit is lost, so we reset it. - // It is not safe to release the qubit here, as that may - // interfere with later operations (gates or measurements) - // or even normal qubit release at end of scope. - if self.sim.measure(q) { - self.sim.x(q); + match term { + FaultTerm::I => {} + FaultTerm::X => self.sim.x(q), + FaultTerm::Y => self.sim.y(q), + FaultTerm::Z => self.sim.z(q), + FaultTerm::Loss => { + if !self.is_qubit_lost(q) { + // The qubit is lost, so we reset it. + // It is not safe to release the qubit here, as that may + // interfere with later operations (gates or measurements) + // or even normal qubit release at end of scope. + if self.sim.measure(q) { + self.sim.x(q); + } + // Mark the qubit as lost. + self.lost_qubits.set_bit(q as u64, true); + } } - // Mark the qubit as lost. - self.lost_qubits.set_bit(q as u64, true); } } } - let fault = noise_table - .sampler - .sample(self.rng.as_mut().expect("RNG should be present")); - match fault { - Fault::None => {} - Fault::Pauli(paulis) => { - assert!(paulis.len() == qs.len()); - for (&q, pauli) in qs.iter().zip(paulis.iter()) { - if self.is_qubit_lost(q) { - continue; - } - match pauli { - PauliFault::I => {} - PauliFault::X => self.sim.x(q), - PauliFault::Y => self.sim.y(q), - PauliFault::Z => self.sim.z(q), - } - } - } - Fault::S | Fault::Loss => { - panic!("Unexpected fault type from noise table sampler: {fault:?}"); - } - } - self.noise_config = Some(noise_config); } @@ -1205,10 +1188,7 @@ impl CliffordSim { } #[must_use] - pub fn new_with_noise_config( - num_qubits: usize, - noise_config: CumulativeNoiseConfig, - ) -> Self { + pub fn new_with_noise_config(num_qubits: usize, noise_config: CumulativeNoiseConfig) -> Self { let seed = rand::rng().next_u32(); Self { sim: StabilizerSimulator::new(num_qubits, 1, seed, noise_config.into()), diff --git a/source/compiler/qsc_eval/src/backend/noise_tests.rs b/source/compiler/qsc_eval/src/backend/noise_tests.rs index 1c046f64119..13449c07af7 100644 --- a/source/compiler/qsc_eval/src/backend/noise_tests.rs +++ b/source/compiler/qsc_eval/src/backend/noise_tests.rs @@ -258,7 +258,6 @@ fn noise_config_with_single_qubit_fault( qubits: 1, pauli_strings: vec![encode_pauli(pauli)], probabilities: vec![1.0], - loss: 0.0, }; set_gate(&mut config, table); config @@ -275,7 +274,6 @@ fn noise_config_with_two_qubit_fault( qubits: 2, pauli_strings: vec![encode_pauli(pauli)], probabilities: vec![1.0], - loss: 0.0, }; set_gate(&mut config, table); config @@ -527,9 +525,8 @@ fn noise_config_mz_with_loss() { let mut config = NoiseConfig::NOISELESS; config.mz = NoiseTable { qubits: 1, - pauli_strings: vec![], - probabilities: vec![], - loss: 1.0, + pauli_strings: vec![encode_pauli("L")], + probabilities: vec![1.0], }; let mut sim = SparseSim::new_with_noise_config(config.into()); let q = sim.qubit_allocate().expect("sparse simulator is infinite"); @@ -548,9 +545,8 @@ fn noise_config_gate_loss_causes_measurement_loss() { let mut config = NoiseConfig::NOISELESS; config.x = NoiseTable { qubits: 1, - pauli_strings: vec![], - probabilities: vec![], - loss: 1.0, + pauli_strings: vec![encode_pauli("L")], + probabilities: vec![1.0], }; let mut sim = SparseSim::new_with_noise_config(config.into()); let q = sim.qubit_allocate().expect("sparse simulator is infinite"); diff --git a/source/qdk_package/qdk/_device/_atom/__init__.py b/source/qdk_package/qdk/_device/_atom/__init__.py index 1df3925360a..5585c7b19ca 100644 --- a/source/qdk_package/qdk/_device/_atom/__init__.py +++ b/source/qdk_package/qdk/_device/_atom/__init__.py @@ -274,27 +274,27 @@ def simulate( noise.t.x = noise.rz.x noise.t.y = noise.rz.y noise.t.z = noise.rz.z - noise.t.loss = noise.rz.loss + noise.t.l = noise.rz.l if noise.t_adj.is_noiseless(): noise.t_adj.x = noise.rz.x noise.t_adj.y = noise.rz.y noise.t_adj.z = noise.rz.z - noise.t_adj.loss = noise.rz.loss + noise.t_adj.l = noise.rz.l if noise.s.is_noiseless(): noise.s.x = noise.rz.x noise.s.y = noise.rz.y noise.s.z = noise.rz.z - noise.s.loss = noise.rz.loss + noise.s.l = noise.rz.l if noise.s_adj.is_noiseless(): noise.s_adj.x = noise.rz.x noise.s_adj.y = noise.rz.y noise.s_adj.z = noise.rz.z - noise.s_adj.loss = noise.rz.loss + noise.s_adj.l = noise.rz.l if noise.z.is_noiseless(): noise.z.x = noise.rz.x noise.z.y = noise.rz.y noise.z.z = noise.rz.z - noise.z.loss = noise.rz.loss + noise.z.l = noise.rz.l compiled = self.compile(qir) module = Module.from_ir(Context(), str(compiled)) diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index d1aab18ad8e..6a421574e54 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -845,6 +845,12 @@ class IdleNoiseParams: s_probability: float class NoiseTable: + # Deprecated. Setting `loss` distributes the per-qubit loss probability + # across the correlated loss fault strings ('L' for a single-qubit + # operation; 'IL', 'LI', and 'LL' for a two-qubit operation), so that it + # is equivalent to applying loss independently to each qubit. Reading + # `loss` reconstructs that per-qubit probability. Prefer setting the loss + # fault strings directly via `set_pauli_noise`. loss: float def __init__(self, num_qubits: int): @@ -870,6 +876,13 @@ class NoiseTable: for arbitrary pauli fields. Setting an element that was previously set overrides that entry with the new value. + + In addition to the Pauli characters 'I', 'X', 'Y', 'Z', a string + may contain 'L' to indicate that the corresponding qubit is lost + when this entry is sampled. Loss is correlated with the rest of the + string: the Pauli is applied to the non-lost qubits and the qubits + marked 'L' are lost (measured and reset). For example, `noise_table.xl` + applies an X to the first qubit and loses the second. """ @overload @@ -878,10 +891,17 @@ class NoiseTable: The correlated pauli noise to use in simulation. Setting an element that was previously set overrides that entry with the new value. + In addition to the Pauli characters 'I', 'X', 'Y', 'Z', a string + may contain 'L' to indicate that the corresponding qubit is lost + when this entry is sampled. Loss is correlated with the rest of the + string: the Pauli is applied to the non-lost qubits and the qubits + marked 'L' are lost (measured and reset). For example, `noise_table.xl` + applies an X to the first qubit and loses the second. + Example:: noise_table = NoiseTable(2) - noise_table.set_pauli_noise([("XI", 1e-10), ("XZ", 1e-8)]) + noise_table.set_pauli_noise([("XI", 1e-10), ("XL", 1e-8)]) """ @overload diff --git a/source/qdk_package/src/qir_simulation.rs b/source/qdk_package/src/qir_simulation.rs index eccf18f248e..7b819b63cae 100644 --- a/source/qdk_package/src/qir_simulation.rs +++ b/source/qdk_package/src/qir_simulation.rs @@ -17,7 +17,7 @@ use pyo3::{ }; use qdk_simulators::{ bytecode, - noise_config::{encode_pauli, is_pauli_identity}, + noise_config::{PauliAndLossString, encode_pauli, is_pauli_identity}, }; use rustc_hash::FxHashMap; @@ -357,9 +357,7 @@ impl From for IdleNoiseParams { #[pyclass(from_py_object, module = "qdk._native")] pub struct NoiseTable { qubits: u32, - pauli_noise: FxHashMap, - #[pyo3(get, set)] - pub loss: Probability, + pauli_noise: FxHashMap, } impl NoiseTable { @@ -379,10 +377,10 @@ impl NoiseTable { // Validate pauli string chars. if !pauli_string .chars() - .all(|c| matches!(c, 'I' | 'X' | 'Y' | 'Z')) + .all(|c| matches!(c, 'I' | 'X' | 'Y' | 'Z' | 'L')) { return Err(PyAttributeError::new_err(format!( - "Pauli string can only contain 'I', 'X', 'Y', 'Z' characters, found {pauli_string}" + "Pauli string can only contain 'I', 'X', 'Y', 'Z', 'L' characters, found {pauli_string}" ))); } // Validate number of qubits. @@ -489,6 +487,27 @@ impl NoiseTable { } Ok(()) } + + /// Distributes a per-qubit loss probability across the correlated loss + /// fault strings so that it is equivalent to applying loss independently + /// to each qubit targeted by the operation: a single-qubit operation sets + /// the `L` entry, and a two-qubit operation sets `IL`, `LI`, and `LL`. + fn set_loss(&mut self, value: Probability) -> PyResult<()> { + Self::validate_probability(value)?; + match self.qubits { + 1 => self.set_pauli_noise_elt("L", value), + 2 => { + let single = value * (1.0 - value); + let both = value * value; + self.set_pauli_noise_elt("IL", single)?; + self.set_pauli_noise_elt("LI", single)?; + self.set_pauli_noise_elt("LL", both) + } + n => Err(PyAttributeError::new_err(format!( + "The `loss` attribute is only supported for one- and two-qubit operations, but this operation targets {n} qubits." + ))), + } + } } #[allow( @@ -503,7 +522,6 @@ impl NoiseTable { NoiseTable { qubits: num_qubits, pauli_noise: FxHashMap::default(), - loss: 0.0, } } @@ -514,10 +532,14 @@ impl NoiseTable { /// for arbitrary pauli fields. fn __getattr__(&mut self, name: &str) -> PyResult { if name == "loss" { - Ok(self.loss) - } else { - self.get_pauli_noise_elt(&name.to_uppercase()) + return Err(PyAttributeError::new_err( + "`.loss` is a convenience over setting correlated faults individually. +To get the loss probabilities, access the correlated strings individually. +E.g.: `noise_config.cz.IL`" + .to_string(), + )); } + self.get_pauli_noise_elt(&name.to_uppercase()) } #[allow( @@ -533,8 +555,7 @@ impl NoiseTable { /// previously set overrides that entry with the new value. fn __setattr__(&mut self, name: &str, value: Probability) -> PyResult<()> { if name == "loss" { - self.loss = value; - Ok(()) + self.set_loss(value) } else { self.set_pauli_noise_elt(&name.to_uppercase(), value) } @@ -612,7 +633,7 @@ or one argument of type 'list[tuple[str, float]]', but found {py_args:?}" } pub fn is_noiseless(&self) -> PyResult { - Ok(self.pauli_noise.is_empty() && self.loss == 0.0) + Ok(self.pauli_noise.is_empty()) } } @@ -628,7 +649,6 @@ impl From for qdk_simulators::noise_config::NoiseTable qubits: value.qubits, pauli_strings, probabilities, - loss: generic_float_cast(value.loss), } } } @@ -647,7 +667,6 @@ fn from_noise_table_ref( qubits: value.qubits, pauli_strings, probabilities, - loss: generic_float_cast(value.loss), } } @@ -667,7 +686,6 @@ impl From> for NoiseTable NoiseTable { qubits: value.qubits, pauli_noise, - loss: generic_float_cast(value.loss), } } } diff --git a/source/qdk_package/src/qir_simulation/correlated_noise.rs b/source/qdk_package/src/qir_simulation/correlated_noise.rs index 4859d837ef8..faff78f93bb 100644 --- a/source/qdk_package/src/qir_simulation/correlated_noise.rs +++ b/source/qdk_package/src/qir_simulation/correlated_noise.rs @@ -84,7 +84,6 @@ pub fn parse_noise_table(contents: &str) -> Result { return Ok(NoiseTable { qubits: qubits.unwrap_or(0), pauli_noise, - loss: 0.0, }); } @@ -157,7 +156,6 @@ pub fn parse_noise_table(contents: &str) -> Result { Ok(NoiseTable { qubits, pauli_noise, - loss: 0.0, }) } @@ -228,14 +226,17 @@ fn parse_noise_chunk(contents: &str, line_offset: usize) -> Result 0u64, - b'X' => 1u64, - b'Y' => 3u64, - b'Z' => 2u64, + b'I' => 0, + b'X' => 1, + b'Y' => 3, + b'Z' => 2, + b'L' => 4, _ => { return Err(ParseError::InvalidPauliChar { line: i, @@ -243,7 +244,7 @@ fn parse_noise_chunk(contents: &str, line_offset: usize) -> Result( noise_config: Option<&Bound<'py, NoiseConfig>>, seed: Option, ) -> PyResult> { - use qdk_simulators::cpu_full_state_simulator::noise::Fault; if noise_config.is_some() { let make_simulator = |num_qubits, num_results, seed, noise| { NoisySimulator::new(num_qubits as usize, num_results as usize, seed, noise) @@ -69,10 +68,9 @@ pub fn run_cpu_full_state<'py>( make_simulator, ) } else { - let make_simulator = - |num_qubits, num_results, seed, _noise: Arc>| { - NoiselessSimulator::new(num_qubits as usize, num_results as usize, seed, ()) - }; + let make_simulator = |num_qubits, num_results, seed, _noise: Arc| { + NoiselessSimulator::new(num_qubits as usize, num_results as usize, seed, ()) + }; py_run( py, input, @@ -270,8 +268,6 @@ pub fn run_cpu_adaptive<'py>( noise_config: Option<&Bound<'py, NoiseConfig>>, seed: Option, ) -> PyResult> { - use qdk_simulators::cpu_full_state_simulator::noise::Fault; - let program: bytecode::AdaptiveProgram = adaptive_program_from_pydict(input)?; let noise: noise_config::NoiseConfig = if let Some(nc) = noise_config { @@ -281,16 +277,14 @@ pub fn run_cpu_adaptive<'py>( }; let output = if noise_config.is_some() { - let make_simulator = - |num_qubits, num_results, seed, noise: Arc>| { - NoisySimulator::new(num_qubits, num_results, seed, noise) - }; + let make_simulator = |num_qubits, num_results, seed, noise: Arc| { + NoisySimulator::new(num_qubits, num_results, seed, noise) + }; run_adaptive(&program, shots, seed, noise, make_simulator) } else { - let make_simulator = - |num_qubits, num_results, seed, _noise: Arc>| { - NoiselessSimulator::new(num_qubits, num_results, seed, ()) - }; + let make_simulator = |num_qubits, num_results, seed, _noise: Arc| { + NoiselessSimulator::new(num_qubits, num_results, seed, ()) + }; run_adaptive(&program, shots, seed, noise, make_simulator) }; @@ -317,8 +311,6 @@ pub fn run_clifford_adaptive<'py>( noise_config: Option<&Bound<'py, NoiseConfig>>, seed: Option, ) -> PyResult> { - use qdk_simulators::stabilizer_simulator::noise::Fault; - let program: bytecode::AdaptiveProgram = adaptive_program_from_pydict(input)?; let noise: noise_config::NoiseConfig = if let Some(nc) = noise_config { @@ -327,10 +319,9 @@ pub fn run_clifford_adaptive<'py>( noise_config::NoiseConfig::NOISELESS }; - let make_simulator = - |num_qubits, num_results, seed, noise: Arc>| { - StabilizerSimulator::new(num_qubits, num_results, seed, noise) - }; + let make_simulator = |num_qubits, num_results, seed, noise: Arc| { + StabilizerSimulator::new(num_qubits, num_results, seed, noise) + }; let output = run_adaptive(&program, shots, seed, noise, make_simulator); let mut array = Vec::with_capacity(shots as usize); diff --git a/source/qdk_package/tests/csv_dir_test/test_noise_intrinsic.csv b/source/qdk_package/tests/csv_dir_test/test_noise_intrinsic.csv index ec5217aae2e..861980410b1 100644 --- a/source/qdk_package/tests/csv_dir_test/test_noise_intrinsic.csv +++ b/source/qdk_package/tests/csv_dir_test/test_noise_intrinsic.csv @@ -1 +1 @@ -YYY,1.0 +YYL,1.0 diff --git a/source/qdk_package/tests/test_adaptive_cpu_noise.py b/source/qdk_package/tests/test_adaptive_cpu_noise.py index 5e7bed7761e..5660891c0a3 100644 --- a/source/qdk_package/tests/test_adaptive_cpu_noise.py +++ b/source/qdk_package/tests/test_adaptive_cpu_noise.py @@ -260,7 +260,7 @@ def test_noise_intrinsics_load_csv_dir(sim_type): noise = NoiseConfig() noise.load_csv_dir(str(Path(__file__).parent / "csv_dir_test")) output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) - assert output == [[Result.One, Result.Zero, Result.One]] + assert output == [[Result.One, Result.Zero, Result.Loss]] NOISE_INTRINSICS_WITH_REGISTERS_QIR = r""" diff --git a/source/qdk_package/tests/test_clifford_simulator.py b/source/qdk_package/tests/test_clifford_simulator.py index 9d9c87800e9..4e130459354 100644 --- a/source/qdk_package/tests/test_clifford_simulator.py +++ b/source/qdk_package/tests/test_clifford_simulator.py @@ -278,7 +278,7 @@ def test_clifford_run_bitflip_noise(): result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) # Reasonable results obtained from manual run - assert result == ["0000000011000001"] + assert result == ["0000010000001000"] # Same execution should work with the operation itself. output = qsharp.run( @@ -296,7 +296,7 @@ def test_clifford_run_bitflip_noise(): ) result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) - assert result == ["0000000011000001"] + assert result == ["0000010000001000"] def test_clifford_run_mixed_noise(): @@ -319,7 +319,7 @@ def test_clifford_run_mixed_noise(): result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) # Reasonable results obtained from manual run - assert result == ["00000-0000000001"] + assert result == ["000000000--000-0"] def test_clifford_run_isolated_loss(): diff --git a/source/qdk_package/tests/test_correlated_noise.py b/source/qdk_package/tests/test_correlated_noise.py index 0a330bba29f..6466691d4a0 100644 --- a/source/qdk_package/tests/test_correlated_noise.py +++ b/source/qdk_package/tests/test_correlated_noise.py @@ -1,30 +1,37 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +import os import pytest -import sys from pathlib import Path from qdk.simulation import NoiseConfig, run_qir from qdk import Result import qdk.openqasm +from qdk.simulation._simulation import try_create_gpu_adapter -SKIP_REASON = "GPU is not available" +# --------------------------------------------------------------------------- +# Simulator-type parametrization +# --------------------------------------------------------------------------- -gpu_info = "Unknown" -try: - from qdk._native import try_create_gpu_adapter +def gpu_param(): + skip_reason = "" + try: + try_create_gpu_adapter() + if not os.environ.get("QDK_GPU_TESTS"): + skip_reason = "Env variable QDK_GPU_TESTS is not set" + except Exception: + skip_reason = "No GPU available" - gpu_info = try_create_gpu_adapter() - # Printing to stderr so that it is visible if CI run fails - print(f"*** USING GPU: {gpu_info}", file=sys.stderr) - GPU_AVAILABLE = True -except OSError as e: - GPU_AVAILABLE = False - SKIP_REASON = str(e) + return pytest.param( + "gpu", + marks=pytest.mark.skipif(bool(skip_reason), reason=skip_reason), + ) + + +SIM_TYPES = ["cpu", "clifford", gpu_param()] -CPU_SIMULATORS = ("clifford", "cpu") QASM_WITH_CORRELATED_NOISE = """ OPENQASM 3.0; include "stdgates.inc"; @@ -45,50 +52,36 @@ ) -def test_noiseless_simulation(): - for type in CPU_SIMULATORS: - output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=None, type=type) - assert output == [[Result.Zero, Result.One, Result.Zero]] - - -@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON) -def test_noiseless_simulation_gpu(): - output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=None, type="gpu") +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_noiseless_simulation(sim_type): + output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=None, type=sim_type) assert output == [[Result.Zero, Result.One, Result.Zero]] -def test_noisy_simulation(): - noise = NoiseConfig() - table = noise.intrinsic("test_noise_intrinsic", 3) - table.yyy = 1.0 - for type in CPU_SIMULATORS: - output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=type) - assert output == [[Result.One, Result.Zero, Result.One]] - - -@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON) -def test_noisy_simulation_gpu(): +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_noisy_simulation(sim_type): noise = NoiseConfig() table = noise.intrinsic("test_noise_intrinsic", 3) table.yyy = 1.0 - output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type="gpu") + output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) assert output == [[Result.One, Result.Zero, Result.One]] -def test_load_csv_dir(): +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_correlated_loss_only_entry(sim_type): noise = NoiseConfig() - noise.load_csv_dir(str(Path(__file__).parent / "csv_dir_test")) - for type in CPU_SIMULATORS: - output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=type) - assert output == [[Result.One, Result.Zero, Result.One]] + table = noise.intrinsic("test_noise_intrinsic", 3) + table.yyl = 1.0 + output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) + assert output == [[Result.One, Result.Zero, Result.Loss]] -@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON) -def test_load_csv_dir_gpu(): +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_load_csv_dir(sim_type): noise = NoiseConfig() noise.load_csv_dir(str(Path(__file__).parent / "csv_dir_test")) - output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type="gpu") - assert output == [[Result.One, Result.Zero, Result.One]] + output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type=sim_type) + assert output == [[Result.One, Result.Zero, Result.Loss]] def test_noisy_simulation_with_missing_gates_fails(): diff --git a/source/qdk_package/tests/test_simulators_gates_noisy.py b/source/qdk_package/tests/test_simulators_gates_noisy.py index c6d6bb009a8..65fa76da6b5 100644 --- a/source/qdk_package/tests/test_simulators_gates_noisy.py +++ b/source/qdk_package/tests/test_simulators_gates_noisy.py @@ -256,6 +256,62 @@ def test_two_qubit_loss(sim_type): ) +# =========================================================================== +# Correlated loss tests ('L' in a noise string) +# =========================================================================== + + +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_correlated_loss_only_entry(sim_type): + # An "L"-only entry loses the qubit with the entry's probability, like the + # scalar loss field, but expressed inside a correlated string. + noise = NoiseConfig() + noise.x.set_pauli_noise("L", 0.1) + results = compile_and_run( + "{use q = Qubit(); X(q); MResetZ(q)}", + shots=1000, + seed=SEED, + noise=noise, + sim_type=sim_type, + ) + check_histogram(results, {"-": 0.1, "1": 0.9}) + + +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_correlated_pauli_and_loss(sim_type): + # "XL" applies an X to the control qubit and loses the target qubit, both + # with probability 0.1, as a single correlated event. The X on the control + # cancels the gate's X, so the control reads 0 exactly when the target is lost. + noise = NoiseConfig() + noise.cx.set_pauli_noise("XL", 0.1) + results = compile_and_run( + "{use qs = Qubit[2]; X(qs[0]); CNOT(qs[0], qs[1]); [MResetZ(qs[0]), MResetZ(qs[1])]}", + shots=10_000, + seed=SEED, + noise=noise, + sim_type=sim_type, + ) + # Without noise: control=1, target=1 => "11". + # With the "XL" event (p=0.1): control flips to 0, target lost => "0-". + check_histogram(results, {"11": 0.9, "0-": 0.1}, tolerance=0.03) + + +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_correlated_multi_qubit_loss(sim_type): + # "LL" loses both qubits of the gate together with probability 0.1. + noise = NoiseConfig() + noise.cz.set_pauli_noise("LL", 0.1) + results = compile_and_run( + "{use qs = Qubit[2]; CZ(qs[0], qs[1]); [MResetZ(qs[0]), MResetZ(qs[1])]}", + shots=10_000, + seed=SEED, + noise=noise, + sim_type=sim_type, + ) + # Either both qubits are lost together ("--", p=0.1) or neither ("00", p=0.9). + check_histogram(results, {"--": 0.1, "00": 0.9}, tolerance=0.03) + + # =========================================================================== # Two-qubit gate noise tests # =========================================================================== diff --git a/source/qdk_package/tests/test_sparse_simulator.py b/source/qdk_package/tests/test_sparse_simulator.py index 4c1af9d97f8..b2e36c4a291 100644 --- a/source/qdk_package/tests/test_sparse_simulator.py +++ b/source/qdk_package/tests/test_sparse_simulator.py @@ -96,7 +96,7 @@ def test_sparse_mixed_noise(): result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) # Reasonable results obtained from manual run - assert result == ["0000010000000-00"] + assert result == ["0000000000-00011"] def test_sparse_isolated_loss(): @@ -322,8 +322,7 @@ def generate_op_sequence( @pytest.mark.parametrize("noisy_gate, noise_number", [(0, 2), (1, 1), (2, 2), (3, 2)]) def test_sparse_permuted_rotations(noisy_gate: int, noise_number: int): qsharp.init(target_profile=TargetProfile.Base) - - n_shots = 400 + n_shots = 1000 n_qubits = 11 seed = 20 p_loss = 0.1 diff --git a/source/simulators/benches/sim_time.rs b/source/simulators/benches/sim_time.rs index 93197937774..4c08fe26ce5 100644 --- a/source/simulators/benches/sim_time.rs +++ b/source/simulators/benches/sim_time.rs @@ -9,7 +9,6 @@ use qdk_simulators::{ noise_config::{CumulativeNoiseConfig, NoiseConfig}, stabilizer_simulator::{ StabilizerSimulator, - noise::Fault, operation::{Operation, cz, h, id, mov, mz, s, x, y, z}, }, }; @@ -57,8 +56,7 @@ fn random_gates(num_gates: usize) -> Vec { fn sim_1k_gates(c: &mut Criterion) { const NUM_GATES: usize = 1_000; let gates = random_gates(NUM_GATES); - let noise: Arc> = - Arc::new(>::NOISELESS.into()); + let noise: Arc = Arc::new(>::NOISELESS.into()); c.bench_function("1k gates", |b| { b.iter(|| { let mut simulator = @@ -71,8 +69,7 @@ fn sim_1k_gates(c: &mut Criterion) { fn sim_20k_gates(c: &mut Criterion) { const NUM_GATES: usize = 20_000; let gates = random_gates(NUM_GATES); - let noise: Arc> = - Arc::new(>::NOISELESS.into()); + let noise: Arc = Arc::new(>::NOISELESS.into()); c.bench_function("20k gates", |b| { b.iter(|| { let mut simulator = @@ -85,8 +82,7 @@ fn sim_20k_gates(c: &mut Criterion) { fn sim_1m_gates(c: &mut Criterion) { const NUM_GATES: usize = 1_000_000; let gates = random_gates(NUM_GATES); - let noise: Arc> = - Arc::new(>::NOISELESS.into()); + let noise: Arc = Arc::new(>::NOISELESS.into()); c.bench_function("1m gates", |b| { b.iter(|| { let mut simulator = diff --git a/source/simulators/benches/sim_time_noisy.rs b/source/simulators/benches/sim_time_noisy.rs index ed1e38dd047..60ad904ccba 100644 --- a/source/simulators/benches/sim_time_noisy.rs +++ b/source/simulators/benches/sim_time_noisy.rs @@ -11,7 +11,6 @@ use qdk_simulators::{ }, stabilizer_simulator::{ StabilizerSimulator, - noise::Fault, operation::{Operation, cz, h, id, mov, mz, s, x, y, z}, }, }; @@ -89,7 +88,7 @@ fn random_gates(num_gates: usize) -> Vec { fn sim_1k_gates(c: &mut Criterion) { const NUM_GATES: usize = 1_000; let gates = random_gates(NUM_GATES); - let noise: Arc> = Arc::new(NOISE_CONFIG.into()); + let noise: Arc = Arc::new(NOISE_CONFIG.into()); c.bench_function("1k gates", |b| { b.iter(|| { let mut simulator = @@ -102,7 +101,7 @@ fn sim_1k_gates(c: &mut Criterion) { fn sim_20k_gates(c: &mut Criterion) { const NUM_GATES: usize = 20_000; let gates = random_gates(NUM_GATES); - let noise: Arc> = Arc::new(NOISE_CONFIG.into()); + let noise: Arc = Arc::new(NOISE_CONFIG.into()); c.bench_function("20k gates", |b| { b.iter(|| { let mut simulator = @@ -115,7 +114,7 @@ fn sim_20k_gates(c: &mut Criterion) { fn sim_1m_gates(c: &mut Criterion) { const NUM_GATES: usize = 1_000_000; let gates = random_gates(NUM_GATES); - let noise: Arc> = Arc::new(NOISE_CONFIG.into()); + let noise: Arc = Arc::new(NOISE_CONFIG.into()); c.bench_function("1m gates", |b| { b.iter(|| { let mut simulator = diff --git a/source/simulators/src/cpu_full_state_simulator.rs b/source/simulators/src/cpu_full_state_simulator.rs index 7b117ef19ff..0790485245f 100644 --- a/source/simulators/src/cpu_full_state_simulator.rs +++ b/source/simulators/src/cpu_full_state_simulator.rs @@ -1,15 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -pub mod noise; - use crate::{ MeasurementResult, QubitID, Simulator, - noise_config::{CumulativeNoiseConfig, IntrinsicID}, + noise_config::{CumulativeNoiseConfig, Fault, FaultTerm, IntrinsicID}, }; use core::f64; use nalgebra::Complex; -use noise::Fault; use noisy_simulator::{ Instrument, NoisySimulator as _, Operation, StateVectorSimulator, operation, }; @@ -419,7 +416,7 @@ impl Simulator for NoiselessSimulator { /// A noisy state-vector simulator. pub struct NoisySimulator { /// The noise configuration for the simulation. - noise_config: Arc>, + noise_config: Arc, /// Random number generator used to sample from [`Self::noise_config`]. rng: StdRng, /// The current state of the simulation. @@ -462,49 +459,35 @@ impl NoisySimulator { fn apply_idle_noise(&mut self, target: QubitID) { let idle_time = self.time - self.last_operation_time[target]; self.last_operation_time[target] = self.time; - let fault = self.noise_config.gen_idle_fault(&mut self.rng, idle_time); - if !self.loss[target] && matches!(fault, Fault::S) { + let idle_fault = self.noise_config.gen_idle_fault(&mut self.rng, idle_time); + if idle_fault && !self.loss[target] { self.state .apply_operation(&S, &[target]) .expect("apply_operation should succeed"); } } - fn apply_fault(&mut self, fault: Fault, targets: &[QubitID]) { - match fault { - Fault::None => (), - Fault::Pauli(pauli_string) => { - for (pauli, target) in pauli_string.iter().zip(targets) { - // We don't apply faults on lost qubits. - if self.loss[*target] { - continue; - } - match pauli { - noise::PauliFault::I => (), - noise::PauliFault::X => self - .state - .apply_operation(&X, &[*target]) - .expect("apply_operation should succeed"), - noise::PauliFault::Y => self - .state - .apply_operation(&Y, &[*target]) - .expect("apply_operation should succeed"), - noise::PauliFault::Z => self - .state - .apply_operation(&Z, &[*target]) - .expect("apply_operation should succeed"), - } - } - } - Fault::S => { - if !self.loss[targets[0]] { - self.state - .apply_operation(&S, targets) - .expect("apply_operation should succeed"); - } + fn apply_fault(&mut self, fault: &Fault, targets: &[QubitID]) { + for (term, target) in fault.0.iter().zip(targets) { + // We don't apply faults on lost qubits. + if self.loss[*target] { + continue; } - Fault::Loss => { - for target in targets { + match term { + FaultTerm::I => (), + FaultTerm::X => self + .state + .apply_operation(&X, &[*target]) + .expect("apply_operation should succeed"), + FaultTerm::Y => self + .state + .apply_operation(&Y, &[*target]) + .expect("apply_operation should succeed"), + FaultTerm::Z => self + .state + .apply_operation(&Z, &[*target]) + .expect("apply_operation should succeed"), + FaultTerm::Loss => { self.mresetz_impl(*target); self.loss[*target] = true; } @@ -574,18 +557,17 @@ impl NoisySimulator { /// reference to `self` at the same time. So, the obvious way express /// this, /// ```ignore -/// fn apply_loss(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { +/// fn apply_noise(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { /// for target in targets { -/// if matches!(noise_table.sample_loss(&mut self.rng), Fault::Loss) { -/// self.mresetz_impl(*target); -/// self.loss[*target] = true; +/// if matches!(noise_table.sample_noise(&mut self.rng), Fault::Loss) { +/// ... /// } /// } /// } /// ``` /// and then doing, /// ```ignore -/// self.apply_loss(&self.noise_config.rxx, targets) +/// self.apply_noise(&self.noise_config.rxx, targets) /// ``` /// is not valid rust. /// @@ -594,7 +576,7 @@ impl NoisySimulator { /// that way rust doesn't see the cloned Arc as attached to self anymore. /// ```ignore /// let noise_config = Arc::clone(&self.noise_config); -/// self.apply_loss(&noise_config.rxx, targets); +/// self.apply_noise(&noise_config.rxx, targets); /// ``` /// However, this is not ideal. We don't want to be increasing and decreasing /// the reference count of an Arc in the hot-loop of the simulation. @@ -602,7 +584,7 @@ impl NoisySimulator { /// The other alternative is creating a function that takes all the necessary /// members of self as inputs independently, /// ```ignore -/// fn apply_loss( +/// fn apply_noise( /// state: &mut StateType, /// noise_table: &CumulativeNoiseTable, /// targets: &[QubitID], @@ -622,51 +604,17 @@ impl NoisySimulator { /// However, this is not very elegant. We would even need to re-implement mresetz. /// /// The remaining alternative is using a macro. -macro_rules! apply_loss { - ($slf:expr, $noise_table:ident, $targets:expr) => { - for target in $targets { - if !$slf.loss[*target] { - let fault = $slf.noise_config.$noise_table.sample_loss(&mut $slf.rng); - if matches!(fault, Fault::Loss) { - $slf.mresetz_impl(*target); - $slf.loss[*target] = true; - } - } - } - }; -} - macro_rules! apply_noise { ($slf:expr, $noise_table:ident, $targets:expr) => {{ let fault = $slf.noise_config.$noise_table.sample_noise(&mut $slf.rng); - if let Fault::Pauli(pauli_string) = fault { - for (pauli, target) in pauli_string.iter().zip($targets) { - // We don't apply faults on lost qubits. - if $slf.loss[*target] { - continue; - } - match pauli { - noise::PauliFault::I => (), - noise::PauliFault::X => $slf - .state - .apply_operation(&X, &[*target]) - .expect("apply_operation should succeed"), - noise::PauliFault::Y => $slf - .state - .apply_operation(&Y, &[*target]) - .expect("apply_operation should succeed"), - noise::PauliFault::Z => $slf - .state - .apply_operation(&Z, &[*target]) - .expect("apply_operation should succeed"), - } - } - }; + if let Some(fault) = fault { + $slf.apply_fault(&fault, $targets); + } }}; } impl Simulator for NoisySimulator { - type Noise = Arc>; + type Noise = Arc; type StateDumpData = noisy_simulator::StateVector; fn new(num_qubits: usize, num_results: usize, seed: u32, noise_config: Self::Noise) -> Self { @@ -687,7 +635,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&X, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, x, &[target]); apply_noise!(self, x, &[target]); } } @@ -698,7 +645,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&Y, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, y, &[target]); apply_noise!(self, y, &[target]); } } @@ -709,7 +655,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&Z, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, z, &[target]); apply_noise!(self, z, &[target]); } } @@ -720,7 +665,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&H, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, h, &[target]); apply_noise!(self, h, &[target]); } } @@ -731,7 +675,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&S, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, s, &[target]); apply_noise!(self, s, &[target]); } } @@ -742,7 +685,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&S_ADJ, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, s_adj, &[target]); apply_noise!(self, s_adj, &[target]); } } @@ -753,7 +695,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&SX, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, sx, &[target]); apply_noise!(self, sx, &[target]); } } @@ -764,7 +705,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&SX_ADJ, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, sx_adj, &[target]); apply_noise!(self, sx_adj, &[target]); } } @@ -775,7 +715,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&T, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, t, &[target]); apply_noise!(self, t, &[target]); } } @@ -786,7 +725,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&T_ADJ, &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, t_adj, &[target]); apply_noise!(self, t_adj, &[target]); } } @@ -797,7 +735,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&rx(angle), &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, rx, &[target]); apply_noise!(self, rx, &[target]); } } @@ -808,7 +745,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&ry(angle), &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, ry, &[target]); apply_noise!(self, ry, &[target]); } } @@ -819,7 +755,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&rz(angle), &[target]) .expect("apply_operation should succeed"); - apply_loss!(self, rz, &[target]); apply_noise!(self, rz, &[target]); } } @@ -833,7 +768,6 @@ impl Simulator for NoisySimulator { .expect("apply_operation should succeed"); } // We still apply operation faults to non-lost qubits. - apply_loss!(self, cx, &[control, target]); apply_noise!(self, cx, &[control, target]); } @@ -846,7 +780,6 @@ impl Simulator for NoisySimulator { .expect("apply_operation should succeed"); } // We still apply operation faults to non-lost qubits. - apply_loss!(self, cy, &[control, target]); apply_noise!(self, cy, &[control, target]); } @@ -859,7 +792,6 @@ impl Simulator for NoisySimulator { .expect("apply_operation should succeed"); } // We still apply operation faults to non-lost qubits. - apply_loss!(self, cz, &[control, target]); apply_noise!(self, cz, &[control, target]); } @@ -874,7 +806,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&rxx(angle), &[q1, q2]) .expect("apply_operation should succeed"); - apply_loss!(self, rxx, &[q1, q2]); apply_noise!(self, rxx, &[q1, q2]); } } @@ -891,7 +822,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&ryy(angle), &[q1, q2]) .expect("apply_operation should succeed"); - apply_loss!(self, ryy, &[q1, q2]); apply_noise!(self, ryy, &[q1, q2]); } } @@ -908,7 +838,6 @@ impl Simulator for NoisySimulator { self.state .apply_operation(&rzz(angle), &[q1, q2]) .expect("apply_operation should succeed"); - apply_loss!(self, rzz, &[q1, q2]); apply_noise!(self, rzz, &[q1, q2]); } } @@ -949,45 +878,42 @@ impl Simulator for NoisySimulator { // Is up to the user if swap is a virtual operation or not. // If they don't specify noise/loss probability for swap, then it is virtual. - apply_loss!(self, swap, &[q1, q2]); apply_noise!(self, swap, &[q1, q2]); } fn mz(&mut self, target: QubitID, result_id: QubitID) { self.apply_idle_noise(target); self.record_mz(target, result_id); - apply_loss!(self, mz, &[target]); apply_noise!(self, mz, &[target]); } fn mresetz(&mut self, target: QubitID, result_id: QubitID) { self.apply_idle_noise(target); self.record_mresetz(target, result_id); - apply_loss!(self, mresetz, &[target]); apply_noise!(self, mresetz, &[target]); } fn resetz(&mut self, target: QubitID) { self.apply_idle_noise(target); self.mresetz_impl(target); - apply_loss!(self, mresetz, &[target]); apply_noise!(self, mresetz, &[target]); } fn mov(&mut self, target: QubitID) { if !self.loss[target] { self.apply_idle_noise(target); - apply_loss!(self, mov, &[target]); apply_noise!(self, mov, &[target]); } } fn correlated_noise_intrinsic(&mut self, intrinsic_id: IntrinsicID, targets: &[usize]) { let fault = match self.noise_config.intrinsics.get(&intrinsic_id) { - Some(correlated_noise) => correlated_noise.sample(&mut self.rng), + Some(correlated_noise) => correlated_noise.sample(&mut self.rng).cloned(), None => return, }; - self.apply_fault(fault, targets); + if let Some(fault) = fault { + self.apply_fault(&fault, targets); + } } fn measurements(&self) -> &[MeasurementResult] { diff --git a/source/simulators/src/cpu_full_state_simulator/noise.rs b/source/simulators/src/cpu_full_state_simulator/noise.rs deleted file mode 100644 index 60ce7dad150..00000000000 --- a/source/simulators/src/cpu_full_state_simulator/noise.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::noise_config::{self, CumulativeNoiseConfig, decode_pauli, is_pauli_identity}; -use rand::RngExt; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum PauliFault { - I, - X, - Y, - Z, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum Fault { - /// No fault occurred. - #[default] - None, - /// A Pauli fault. - Pauli(Vec), - /// A gradual dephasing fault. Qubits are always slowly - /// rotating along the Z-axis with an unknown rate, - /// eventually resulting in an `S` gate. - S, - /// The qubit was lost. - Loss, -} - -impl noise_config::Fault for Fault { - fn none() -> Self { - Self::None - } - - fn loss() -> Self { - Self::Loss - } -} - -impl From<(u64, u32)> for Fault { - fn from((pauli, qubits): (u64, u32)) -> Self { - const MAP: [PauliFault; 4] = [PauliFault::I, PauliFault::X, PauliFault::Z, PauliFault::Y]; - assert!( - !is_pauli_identity(pauli), - "the NoiseTable input validation should ensure we don't insert the identity string" - ); - let pauli_product = decode_pauli(pauli, qubits, &MAP); - Self::Pauli(pauli_product) - } -} - -impl CumulativeNoiseConfig { - /// Samples a float in the range [0, 1] and picks one of the faults - /// `X`, `Y`, `Z`, `S` based on the provided noise table. - #[must_use] - pub fn gen_idle_fault(&self, rng: &mut impl rand::Rng, idle_steps: u32) -> Fault { - let sample: f32 = rng.random_range(0.0..1.0); - if sample < self.idle.s_probability(idle_steps) { - Fault::S - } else { - Fault::None - } - } -} diff --git a/source/simulators/src/gpu_full_state_simulator/correlated_noise.rs b/source/simulators/src/gpu_full_state_simulator/correlated_noise.rs index 9e7057f13e7..7448d88c485 100644 --- a/source/simulators/src/gpu_full_state_simulator/correlated_noise.rs +++ b/source/simulators/src/gpu_full_state_simulator/correlated_noise.rs @@ -14,8 +14,8 @@ use crate::noise_config::{NoiseConfig, NoiseTable, encode_pauli, uq1_63}; #[repr(C)] #[derive(Copy, Clone, Debug, Pod, Zeroable)] pub struct NoiseTableEntry { - /// The correlated pauli string as bits (2 bits per qubit). If bit 0 is set, then it has bit-flip - /// noise, and if bit 1 is set then it has phase-flip noise. e.g., `110001 == "YIX"` + /// The correlated pauli + loss string as bits (3 bits per qubit). If bit 0 is set, then it has bit-flip + /// noise, and if bit 1 is set then it has phase-flip noise. e.g., `100_011_000_001 == "LYIX"` paulis: u64, /// The probability of the noise occurring in `Q1_63` format. This is a float format where the high /// order bit (bit 63) has the value 1.0 (`2^0 / 1`), bit 62 has the value 0.5 (`2^1 / 1`), etc. diff --git a/source/simulators/src/noise_config.rs b/source/simulators/src/noise_config.rs index 6d8c5809451..7841909b457 100644 --- a/source/simulators/src/noise_config.rs +++ b/source/simulators/src/noise_config.rs @@ -5,16 +5,62 @@ mod tests; pub(crate) mod uq1_63; -use num_traits::{ConstZero, Float}; +use num_traits::ConstZero; use rand::RngExt; use rustc_hash::FxHashMap; use std::hash::BuildHasherDefault; +use crate::noise_config::uq1_63::UQ1_63; + pub(crate) type IntrinsicID = u32; -pub trait Fault { - fn none() -> Self; - fn loss() -> Self; +/// A [`u64`] where every 3-bits encodes one of I, X, Y, Z, and L +/// where IXYZ represent Pauli terms, and L represents loss. +pub type PauliAndLossString = u64; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FaultTerm { + /// An `I` Pauli. + I, + /// An `X` Pauli. + X, + /// A `Y` Pauli. + Y, + /// A `Z` Pauli. + Z, + /// The qubit was lost. + Loss, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// A string of [`FaultTerm`]. +pub struct Fault(pub Vec); + +impl From<(PauliAndLossString, u32)> for Fault { + fn from((pauli, qubits): (PauliAndLossString, u32)) -> Self { + const MAP: [FaultTerm; 5] = [ + FaultTerm::I, + FaultTerm::X, + FaultTerm::Z, + FaultTerm::Y, + FaultTerm::Loss, + ]; + assert!( + !is_pauli_identity(pauli), + "the NoiseTable input validation should ensure we don't insert the identity string" + ); + let fault_string = decode_pauli(pauli, qubits, &MAP); + Self(fault_string) + } +} + +impl CumulativeNoiseConfig { + /// Returns true if an idle fault has triggered. + #[must_use] + pub fn gen_idle_fault(&self, rng: &mut impl rand::Rng, idle_steps: u32) -> bool { + let sample: f32 = rng.random_range(0.0..1.0); + sample < self.idle.s_probability(idle_steps) + } } /// Noise description for each operation. @@ -22,7 +68,7 @@ pub trait Fault { /// This is the format in which the user config files are /// written. #[derive(Clone, Debug)] -pub struct NoiseConfig { +pub struct NoiseConfig { pub i: NoiseTable, pub x: NoiseTable, pub y: NoiseTable, @@ -61,7 +107,7 @@ pub const fn const_empty_hash_map() -> FxHashMap { } } -impl NoiseConfig { +impl NoiseConfig { pub const NOISELESS: Self = Self { i: NoiseTable::::noiseless(1), x: NoiseTable::::noiseless(1), @@ -130,34 +176,32 @@ impl IdleNoiseParams { /// strings are mutually exclusive. Therefore, their probabilities /// must add up to a number less or equal than `1.0`. #[derive(Clone, Debug)] -pub struct NoiseTable { +pub struct NoiseTable { pub qubits: u32, - pub pauli_strings: Vec, + pub pauli_strings: Vec, pub probabilities: Vec, - pub loss: T, } -impl NoiseTable { +impl NoiseTable { #[must_use] pub const fn noiseless(qubits: u32) -> Self { Self { qubits, pauli_strings: Vec::new(), probabilities: Vec::new(), - loss: num_traits::ConstZero::ZERO, } } -} -impl NoiseTable { #[must_use] - pub fn is_noiseless(&self) -> bool { - self.probabilities.is_empty() && self.loss == T::zero() + pub const fn is_noiseless(&self) -> bool { + self.probabilities.is_empty() } +} +impl NoiseTable { #[must_use] pub fn has_pauli_noise(&self) -> bool { - self.probabilities.iter().any(|p| *p > T::zero()) + self.probabilities.iter().any(|p| *p > T::ZERO) } } @@ -165,40 +209,37 @@ impl NoiseTable { /// /// This is the internal format used by the simulator. #[derive(Default)] -pub struct CumulativeNoiseConfig { - pub i: CumulativeNoiseTable, - pub x: CumulativeNoiseTable, - pub y: CumulativeNoiseTable, - pub z: CumulativeNoiseTable, - pub h: CumulativeNoiseTable, - pub s: CumulativeNoiseTable, - pub s_adj: CumulativeNoiseTable, - pub t: CumulativeNoiseTable, - pub t_adj: CumulativeNoiseTable, - pub sx: CumulativeNoiseTable, - pub sx_adj: CumulativeNoiseTable, - pub rx: CumulativeNoiseTable, - pub ry: CumulativeNoiseTable, - pub rz: CumulativeNoiseTable, - pub cx: CumulativeNoiseTable, - pub cy: CumulativeNoiseTable, - pub cz: CumulativeNoiseTable, - pub rxx: CumulativeNoiseTable, - pub ryy: CumulativeNoiseTable, - pub rzz: CumulativeNoiseTable, - pub swap: CumulativeNoiseTable, - pub ccx: CumulativeNoiseTable, - pub mov: CumulativeNoiseTable, - pub mz: CumulativeNoiseTable, - pub mresetz: CumulativeNoiseTable, +pub struct CumulativeNoiseConfig { + pub i: CumulativeNoiseTable, + pub x: CumulativeNoiseTable, + pub y: CumulativeNoiseTable, + pub z: CumulativeNoiseTable, + pub h: CumulativeNoiseTable, + pub s: CumulativeNoiseTable, + pub s_adj: CumulativeNoiseTable, + pub t: CumulativeNoiseTable, + pub t_adj: CumulativeNoiseTable, + pub sx: CumulativeNoiseTable, + pub sx_adj: CumulativeNoiseTable, + pub rx: CumulativeNoiseTable, + pub ry: CumulativeNoiseTable, + pub rz: CumulativeNoiseTable, + pub cx: CumulativeNoiseTable, + pub cy: CumulativeNoiseTable, + pub cz: CumulativeNoiseTable, + pub rxx: CumulativeNoiseTable, + pub ryy: CumulativeNoiseTable, + pub rzz: CumulativeNoiseTable, + pub swap: CumulativeNoiseTable, + pub ccx: CumulativeNoiseTable, + pub mov: CumulativeNoiseTable, + pub mz: CumulativeNoiseTable, + pub mresetz: CumulativeNoiseTable, pub idle: IdleNoiseParams, - pub intrinsics: FxHashMap>, + pub intrinsics: FxHashMap, } -impl From> for CumulativeNoiseConfig -where - F: Fault + Clone + From<(u64, u32)>, -{ +impl From> for CumulativeNoiseConfig { fn from(value: NoiseConfig) -> Self { let intrinsics = value .intrinsics @@ -243,64 +284,52 @@ where /// /// This is the internal format used by the simulator. #[derive(Default)] -pub struct CumulativeNoiseTable { - pub sampler: CorrelatedNoiseSampler, - pub loss: f64, +pub struct CumulativeNoiseTable { + pub sampler: Sampler, } -impl From> for CumulativeNoiseTable -where - F: Fault + Clone + From<(u64, u32)>, -{ +impl From> for CumulativeNoiseTable { fn from(value: NoiseTable) -> Self { let qubits = value.qubits; let choices = value .pauli_strings .into_iter() - .map(|p| F::from((p, qubits))); + .map(|p| Fault::from((p, qubits))); let probs = value.probabilities.into_iter().map(uq1_63::from_prob); Self { - sampler: CorrelatedNoiseSampler::new(choices, probs), - loss: value.loss, + sampler: Sampler::new(choices, probs), } } } -impl CumulativeNoiseTable -where - F: Fault + Clone, -{ - /// Samples loss using the loss probability in the noise table. - #[must_use] - pub fn sample_loss(&self, rng: &mut impl rand::Rng) -> F { - if rng.random_range(0.0..1.0) < self.loss { - F::loss() - } else { - F::none() - } - } - +impl CumulativeNoiseTable { /// Samples loss using the noise probabilities in the noise table. #[must_use] - pub fn sample_noise(&self, rng: &mut impl rand::Rng) -> F { - self.sampler.sample(rng) + pub fn sample_noise(&self, rng: &mut impl rand::Rng) -> Option { + self.sampler.sample(rng).cloned() } } -#[derive(Default)] -pub struct CorrelatedNoiseSampler { - /// The total probability of any noise. - noise_probability: u64, - /// The errors to choose from. +pub struct Sampler { + /// The total probability of any choice. + total_probability: UQ1_63, + /// The values to choose from. choices: Vec, - /// Cumulative probabilities in the [`uq1_63`] format. - cumulative_probabilities: Vec, + /// Cumulative probabilities in the [`UQ1_63`] format. + cumulative_probabilities: Vec, +} + +impl Default for Sampler { + fn default() -> Self { + Self { + total_probability: Default::default(), + choices: Default::default(), + cumulative_probabilities: Default::default(), + } + } } -impl From> for CorrelatedNoiseSampler -where - F: Fault + Clone + From<(u64, u32)>, -{ +impl From> for Sampler { fn from(value: NoiseTable) -> Self { assert!( !value.pauli_strings.is_empty(), @@ -310,41 +339,41 @@ where let choices = value .pauli_strings .into_iter() - .map(|p| F::from((p, qubits))); + .map(|p| Fault::from((p, qubits))); let probs = value.probabilities.into_iter().map(uq1_63::from_prob); Self::new(choices, probs) } } -impl CorrelatedNoiseSampler { +impl Sampler { #[must_use] pub fn new(choices: Choices, probs: Probabilities) -> Self where - Choices: IntoIterator, + Choices: IntoIterator, Probabilities: IntoIterator, { let probs = probs.into_iter(); let mut cumulative_probabilities: Vec = Vec::with_capacity(probs.size_hint().0); - let mut noise_probability: u64 = 0; + let mut total_probability: u64 = 0; for p in probs { - noise_probability += p; + total_probability += p; assert!( - noise_probability <= uq1_63::ONE, + total_probability <= uq1_63::ONE, "total probability should not exceed 1.0" ); - cumulative_probabilities.push(noise_probability); + cumulative_probabilities.push(total_probability); } Self { - noise_probability, + total_probability, choices: choices.into_iter().collect(), cumulative_probabilities, } } #[must_use] - pub fn sample(&self, rng: &mut impl rand::Rng) -> F { + pub fn sample(&self, rng: &mut impl rand::Rng) -> Option<&T> { let distr = rand::distr::Uniform::new(0, uq1_63::ONE).expect("valid range"); let random_sample: u64 = rng.sample(distr); self.sample_with_value(random_sample) @@ -353,57 +382,58 @@ impl CorrelatedNoiseSampler { /// Samples a fault given a pre-generated random value in the range `[0, uq1_63::ONE)`. /// This is useful for testing purposes. #[must_use] - pub fn sample_with_value(&self, random_sample: u64) -> F { + pub fn sample_with_value(&self, random_sample: u64) -> Option<&T> { // This codepath will be taken > 99.9% of times, since the total error probability // is usually very low. - if random_sample >= self.noise_probability { - return F::none(); + if random_sample >= self.total_probability { + return None; } // Find the index of the first cumulative probability greater than the chosen sample. let idx = self .cumulative_probabilities .partition_point(|p| *p <= random_sample); - self.choices[idx].clone() + Some(&self.choices[idx]) } } /// Checks if a pauli string is the identity. #[must_use] -pub fn is_pauli_identity(pauli_string: u64) -> bool { +pub fn is_pauli_identity(pauli_string: PauliAndLossString) -> bool { pauli_string == 0 } -/// Encode a validated Pauli string as a `u64` using 2 bits per character -/// (I=0, X=1, Z=2, Y=3). Supports up to 32 qubits. +/// Encode a validated Pauli string as a `u128` using 3 bits per character +/// (I=0, X=1, Z=2, Y=3, L=4). Supports up to 42 qubits. #[must_use] -pub fn encode_pauli(pauli: &str) -> u64 { +pub fn encode_pauli(pauli: &str) -> PauliAndLossString { let pauli = pauli.as_bytes(); - debug_assert!(pauli.len() <= 32); - let mut result: u64 = 0; + debug_assert!(pauli.len() <= 42); + let mut result: PauliAndLossString = 0; for &b in pauli { let bits = match b { - b'I' => 0u64, - b'X' => 1u64, - b'Z' => 2u64, - b'Y' => 3u64, + b'I' => 0, + b'X' => 1, + b'Z' => 2, + b'Y' => 3, + b'L' => 4, _ => unreachable!("pauli bytes must be validated before encoding"), }; - result = (result << 2) | bits; + result = (result << 3) | bits; } result } /// Decode a `u64`-encoded Pauli string back into a list of T, /// using the given `map` for decoding. -/// (I=0, X=1, Z=2, Y=3). Supports up to 32 qubits. +/// (I=0, X=1, Z=2, Y=3, L=4). Supports up to 21 qubits. #[must_use] -pub fn decode_pauli(mut pauli: u64, qubits: u32, map: &[T; 4]) -> Vec { +pub fn decode_pauli(mut pauli: PauliAndLossString, qubits: u32, map: &[T; 5]) -> Vec { let n = qubits as usize; let mut buf = vec![map[0].clone(); n]; for i in (0..n).rev() { - buf[i] = map[(pauli & 0b11) as usize].clone(); - pauli >>= 2; + buf[i] = map[(pauli & 0b111) as usize].clone(); + pauli >>= 3; } buf } diff --git a/source/simulators/src/noise_config/tests.rs b/source/simulators/src/noise_config/tests.rs index e147a925277..b42b41bca20 100644 --- a/source/simulators/src/noise_config/tests.rs +++ b/source/simulators/src/noise_config/tests.rs @@ -1,41 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::noise_config::{self, CorrelatedNoiseSampler, decode_pauli, encode_pauli, uq1_63}; - -#[derive(Debug, Clone, PartialEq)] -enum Fault { - None, - Value(u64), -} - -impl noise_config::Fault for Fault { - fn none() -> Self { - Self::None - } - - fn loss() -> Self { - unimplemented!() - } -} +use crate::noise_config::{Sampler, decode_pauli, encode_pauli, uq1_63}; #[test] fn sample_smallest_probability_element_at_start() { - let choices = vec![Fault::Value(0), Fault::Value(1)]; + let choices = vec![0, 1]; let probs = vec![1, 1]; - let sampler = CorrelatedNoiseSampler::new(choices, probs); - assert_eq!(Fault::Value(0), sampler.sample_with_value(0)); - assert_eq!(Fault::Value(1), sampler.sample_with_value(1)); - assert_eq!(Fault::None, sampler.sample_with_value(2)); + let sampler = Sampler::new(choices, probs); + assert_eq!(Some(&0), sampler.sample_with_value(0)); + assert_eq!(Some(&1), sampler.sample_with_value(1)); + assert_eq!(None, sampler.sample_with_value(2)); } #[test] fn sample_smallest_probability_element_at_end() { - let choices = vec![Fault::Value(0), Fault::Value(1)]; + let choices = vec![0, 1]; let probs = vec![uq1_63::ONE - 1, 1]; - let sampler = CorrelatedNoiseSampler::new(choices, probs); - assert_eq!(Fault::Value(0), sampler.sample_with_value(uq1_63::ONE - 2)); - assert_eq!(Fault::Value(1), sampler.sample_with_value(uq1_63::ONE - 1)); + let sampler = Sampler::new(choices, probs); + assert_eq!(Some(&0), sampler.sample_with_value(uq1_63::ONE - 2)); + assert_eq!(Some(&1), sampler.sample_with_value(uq1_63::ONE - 1)); } #[test] @@ -44,53 +28,79 @@ fn binary_search_works_as_expected() { let mut probs = Vec::new(); for i in 0..100 { - choices.push(Fault::Value(i)); + choices.push(i); probs.push(1); } - let sampler = CorrelatedNoiseSampler::new(choices, probs); + let sampler = Sampler::new(choices, probs); for i in 0..100 { - assert_eq!(Fault::Value(i), sampler.sample_with_value(i)); + assert_eq!(Some(&i), sampler.sample_with_value(i)); } } #[test] fn test_encode_pauli() { - assert_eq!(0b_00_00, encode_pauli("II")); - assert_eq!(0b_00_01, encode_pauli("IX")); - assert_eq!(0b_00_11, encode_pauli("IY")); - assert_eq!(0b_00_10, encode_pauli("IZ")); - assert_eq!(0b_01_00, encode_pauli("XI")); - assert_eq!(0b_01_01, encode_pauli("XX")); - assert_eq!(0b_01_11, encode_pauli("XY")); - assert_eq!(0b_01_10, encode_pauli("XZ")); - assert_eq!(0b_11_00, encode_pauli("YI")); - assert_eq!(0b_11_01, encode_pauli("YX")); - assert_eq!(0b_11_11, encode_pauli("YY")); - assert_eq!(0b_11_10, encode_pauli("YZ")); - assert_eq!(0b_10_00, encode_pauli("ZI")); - assert_eq!(0b_10_01, encode_pauli("ZX")); - assert_eq!(0b_10_11, encode_pauli("ZY")); - assert_eq!(0b_10_10, encode_pauli("ZZ")); + assert_eq!(0b_000_000, encode_pauli("II")); + assert_eq!(0b_000_001, encode_pauli("IX")); + assert_eq!(0b_000_011, encode_pauli("IY")); + assert_eq!(0b_000_010, encode_pauli("IZ")); + assert_eq!(0b_000_100, encode_pauli("IL")); + + assert_eq!(0b_001_000, encode_pauli("XI")); + assert_eq!(0b_001_001, encode_pauli("XX")); + assert_eq!(0b_001_011, encode_pauli("XY")); + assert_eq!(0b_001_010, encode_pauli("XZ")); + assert_eq!(0b_001_100, encode_pauli("XL")); + + assert_eq!(0b_011_000, encode_pauli("YI")); + assert_eq!(0b_011_001, encode_pauli("YX")); + assert_eq!(0b_011_011, encode_pauli("YY")); + assert_eq!(0b_011_010, encode_pauli("YZ")); + assert_eq!(0b_011_100, encode_pauli("YL")); + + assert_eq!(0b_010_000, encode_pauli("ZI")); + assert_eq!(0b_010_001, encode_pauli("ZX")); + assert_eq!(0b_010_011, encode_pauli("ZY")); + assert_eq!(0b_010_010, encode_pauli("ZZ")); + assert_eq!(0b_010_100, encode_pauli("ZL")); + + assert_eq!(0b_100_000, encode_pauli("LI")); + assert_eq!(0b_100_001, encode_pauli("LX")); + assert_eq!(0b_100_011, encode_pauli("LY")); + assert_eq!(0b_100_010, encode_pauli("LZ")); + assert_eq!(0b_100_100, encode_pauli("LL")); } #[test] fn test_decode_pauli() { - const MAP: [char; 4] = ['I', 'X', 'Z', 'Y']; - assert_eq!(vec!['I', 'I'], decode_pauli(0b_00_00, 2, &MAP)); - assert_eq!(vec!['I', 'X'], decode_pauli(0b_00_01, 2, &MAP)); - assert_eq!(vec!['I', 'Y'], decode_pauli(0b_00_11, 2, &MAP)); - assert_eq!(vec!['I', 'Z'], decode_pauli(0b_00_10, 2, &MAP)); - assert_eq!(vec!['X', 'I'], decode_pauli(0b_01_00, 2, &MAP)); - assert_eq!(vec!['X', 'X'], decode_pauli(0b_01_01, 2, &MAP)); - assert_eq!(vec!['X', 'Y'], decode_pauli(0b_01_11, 2, &MAP)); - assert_eq!(vec!['X', 'Z'], decode_pauli(0b_01_10, 2, &MAP)); - assert_eq!(vec!['Y', 'I'], decode_pauli(0b_11_00, 2, &MAP)); - assert_eq!(vec!['Y', 'X'], decode_pauli(0b_11_01, 2, &MAP)); - assert_eq!(vec!['Y', 'Y'], decode_pauli(0b_11_11, 2, &MAP)); - assert_eq!(vec!['Y', 'Z'], decode_pauli(0b_11_10, 2, &MAP)); - assert_eq!(vec!['Z', 'I'], decode_pauli(0b_10_00, 2, &MAP)); - assert_eq!(vec!['Z', 'X'], decode_pauli(0b_10_01, 2, &MAP)); - assert_eq!(vec!['Z', 'Y'], decode_pauli(0b_10_11, 2, &MAP)); - assert_eq!(vec!['Z', 'Z'], decode_pauli(0b_10_10, 2, &MAP)); + const MAP: [char; 5] = ['I', 'X', 'Z', 'Y', 'L']; + assert_eq!(vec!['I', 'I'], decode_pauli(0b_000_000, 2, &MAP)); + assert_eq!(vec!['I', 'X'], decode_pauli(0b_000_001, 2, &MAP)); + assert_eq!(vec!['I', 'Y'], decode_pauli(0b_000_011, 2, &MAP)); + assert_eq!(vec!['I', 'Z'], decode_pauli(0b_000_010, 2, &MAP)); + assert_eq!(vec!['I', 'L'], decode_pauli(0b_000_100, 2, &MAP)); + + assert_eq!(vec!['X', 'I'], decode_pauli(0b_001_000, 2, &MAP)); + assert_eq!(vec!['X', 'X'], decode_pauli(0b_001_001, 2, &MAP)); + assert_eq!(vec!['X', 'Y'], decode_pauli(0b_001_011, 2, &MAP)); + assert_eq!(vec!['X', 'Z'], decode_pauli(0b_001_010, 2, &MAP)); + assert_eq!(vec!['X', 'L'], decode_pauli(0b_001_100, 2, &MAP)); + + assert_eq!(vec!['Y', 'I'], decode_pauli(0b_011_000, 2, &MAP)); + assert_eq!(vec!['Y', 'X'], decode_pauli(0b_011_001, 2, &MAP)); + assert_eq!(vec!['Y', 'Y'], decode_pauli(0b_011_011, 2, &MAP)); + assert_eq!(vec!['Y', 'Z'], decode_pauli(0b_011_010, 2, &MAP)); + assert_eq!(vec!['Y', 'L'], decode_pauli(0b_011_100, 2, &MAP)); + + assert_eq!(vec!['Z', 'I'], decode_pauli(0b_010_000, 2, &MAP)); + assert_eq!(vec!['Z', 'X'], decode_pauli(0b_010_001, 2, &MAP)); + assert_eq!(vec!['Z', 'Y'], decode_pauli(0b_010_011, 2, &MAP)); + assert_eq!(vec!['Z', 'Z'], decode_pauli(0b_010_010, 2, &MAP)); + assert_eq!(vec!['Z', 'L'], decode_pauli(0b_010_100, 2, &MAP)); + + assert_eq!(vec!['L', 'I'], decode_pauli(0b_100_000, 2, &MAP)); + assert_eq!(vec!['L', 'X'], decode_pauli(0b_100_001, 2, &MAP)); + assert_eq!(vec!['L', 'Y'], decode_pauli(0b_100_011, 2, &MAP)); + assert_eq!(vec!['L', 'Z'], decode_pauli(0b_100_010, 2, &MAP)); + assert_eq!(vec!['L', 'L'], decode_pauli(0b_100_100, 2, &MAP)); } diff --git a/source/simulators/src/noise_config/uq1_63.rs b/source/simulators/src/noise_config/uq1_63.rs index e0d326f7a7e..527965d7765 100644 --- a/source/simulators/src/noise_config/uq1_63.rs +++ b/source/simulators/src/noise_config/uq1_63.rs @@ -4,13 +4,18 @@ #[cfg(test)] mod tests; +/// A `UQ1_63` encoding for a floating point number. +/// +/// You can learn more at: . +pub(crate) type UQ1_63 = u64; + /// This value is 1.0 in `UQ1.63` format (high order bit is 1, rest are 0). -pub(crate) const ONE: u64 = 1u64 << 63; +pub(crate) const ONE: UQ1_63 = 1u64 << 63; /// Maps an `f64` in the range`[0.0, 1.0]` to a `u64` in the `UQ1.63` format. /// /// You can learn more at: . -pub(crate) fn from_prob(p: f64) -> u64 { +pub(crate) fn from_prob(p: f64) -> UQ1_63 { // Only allow values from 0 to 1.0 for the incoming probability. assert!( (0.0..=1.0).contains(&p), diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 7e593178080..97e0d3df70e 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -3,19 +3,17 @@ //! This crate implements a stabilizer simulator for the QDK. -pub mod noise; pub mod operation; use crate::{ MeasurementResult, NearlyZero, QubitID, Simulator, - noise_config::{CumulativeNoiseConfig, IntrinsicID}, + noise_config::{CumulativeNoiseConfig, Fault, FaultTerm, IntrinsicID}, }; -pub use noise::Fault; use operation::Operation; use paulimer::{ Simulation, UnitaryOp, outcome_specific_simulation::{OutcomeSpecificSimulation, apply_hadamard}, - quantum_core, + quantum_core::{self, PauliObservable}, }; use rand::{SeedableRng as _, rngs::StdRng}; use std::{ @@ -26,7 +24,7 @@ use std::{ /// A stabilizer simulator with the ability to simulate atom loss. pub struct StabilizerSimulator { /// The noise configuration for the simulation. - noise_config: Arc>, + noise_config: Arc, /// Random number generator used to sample from [`Self::noise_config`]. rng: StdRng, /// The current inverse state of the simulation. @@ -46,18 +44,17 @@ pub struct StabilizerSimulator { /// reference to `self` at the same time. So, the obvious way express /// this, /// ```ignore -/// fn apply_loss(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { +/// fn apply_noise(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { /// for target in targets { -/// if matches!(noise_table.sample_loss(&mut self.rng), Fault::Loss) { -/// self.mresetz_impl(*target); -/// self.loss[*target] = true; +/// if matches!(noise_table.sample_noise(&mut self.rng), Fault::Loss) { +/// ... /// } /// } /// } /// ``` /// and then doing, /// ```ignore -/// self.apply_loss(&self.noise_config.rxx, targets) +/// self.apply_noise(&self.noise_config.rxx, targets) /// ``` /// is not valid rust. /// @@ -66,7 +63,7 @@ pub struct StabilizerSimulator { /// that way rust doesn't see the cloned Arc as attached to self anymore. /// ```ignore /// let noise_config = Arc::clone(&self.noise_config); -/// self.apply_loss(&noise_config.rxx, targets); +/// self.apply_noise(&noise_config.rxx, targets); /// ``` /// However, this is not ideal. We don't want to be increasing and decreasing /// the reference count of an Arc in the hot-loop of the simulation. @@ -74,7 +71,7 @@ pub struct StabilizerSimulator { /// The other alternative is creating a function that takes all the necessary /// members of self as inputs independently, /// ```ignore -/// fn apply_loss( +/// fn apply_noise( /// state: &mut StateType, /// noise_table: &CumulativeNoiseTable, /// targets: &[QubitID], @@ -94,35 +91,30 @@ pub struct StabilizerSimulator { /// However, this is not very elegant. We would even need to re-implement mresetz. /// /// The remaining alternative is using a macro. -macro_rules! apply_loss { - ($slf:expr, $noise_table:ident, $targets:expr) => { - for target in $targets { - if !$slf.loss[*target] { - let fault = $slf.noise_config.$noise_table.sample_loss(&mut $slf.rng); - if matches!(fault, Fault::Loss) { - $slf.mresetz_impl(*target); - $slf.loss[*target] = true; - } - } - } - }; -} - macro_rules! apply_noise { ($slf:expr, $noise_table:ident, $targets:expr) => {{ let fault = $slf.noise_config.$noise_table.sample_noise(&mut $slf.rng); - if let Fault::Pauli(pauli_observables) = fault { - let observable: Vec<_> = pauli_observables - .into_iter() - .zip($targets) - .filter(|(_, q)| !$slf.loss[**q]) // We don't apply faults on lost qubits. - .map(|(pauli, q)| (pauli, *q).into()) - .collect(); - $slf.state.pauli(&observable); - }; + if let Some(fault) = fault { + $slf.apply_fault(&fault, $targets); + } }}; } +// macro_rules! apply_noise { +// ($slf:expr, $noise_table:ident, $targets:expr) => {{ +// let fault = $slf.noise_config.$noise_table.sample_noise(&mut $slf.rng); +// if let Fault::Pauli(pauli_observables) = fault { +// let observable: Vec<_> = pauli_observables +// .into_iter() +// .zip($targets) +// .filter(|(_, q)| !$slf.loss[**q]) // We don't apply faults on lost qubits. +// .map(|(pauli, q)| (pauli, *q).into()) +// .collect(); +// $slf.state.pauli(&observable); +// }; +// }}; +// } + impl StabilizerSimulator { /// Sets the random seed of the simulator. pub fn set_seed(&mut self, seed: u64) { @@ -179,36 +171,39 @@ impl StabilizerSimulator { fn apply_idle_noise(&mut self, target: QubitID) { let idle_time = self.time - self.last_operation_time[target]; self.last_operation_time[target] = self.time; - let fault = self.noise_config.gen_idle_fault(&mut self.rng, idle_time); - if !self.loss[target] && matches!(fault, Fault::S) { + let idle_fault = self.noise_config.gen_idle_fault(&mut self.rng, idle_time); + if idle_fault && !self.loss[target] { self.state.apply_unitary(UnitaryOp::SqrtZ, &[target]); } } - fn apply_fault(&mut self, fault: Fault, targets: &[QubitID]) { - match fault { - Fault::None => (), - Fault::Pauli(pauli_observables) => { - let observable: Vec<_> = pauli_observables - .into_iter() - .zip(targets) - .filter(|(_, q)| !self.loss[**q]) // We don't apply faults on lost qubits. - .map(|(pauli, q)| (pauli, *q).into()) - .collect(); - self.state.pauli(&observable); - } - Fault::S => { - if !self.loss[targets[0]] { - self.state.apply_unitary(UnitaryOp::SqrtZ, targets); + fn apply_fault(&mut self, fault: &Fault, targets: &[QubitID]) { + let observable: Vec<_> = fault + .0 + .iter() + .zip(targets) + .filter(|(term, q)| { + if self.loss[**q] { + return false; } - } - Fault::Loss => { - for target in targets { - self.mresetz_impl(*target); - self.loss[*target] = true; + match term { + FaultTerm::I => false, + FaultTerm::X | FaultTerm::Y | FaultTerm::Z => true, + FaultTerm::Loss => { + self.mresetz_impl(**q); + self.loss[**q] = true; + false + } } - } - } + }) + .map(|(term, q)| match term { + FaultTerm::X => (PauliObservable::PlusX, *q).into(), + FaultTerm::Y => (PauliObservable::PlusY, *q).into(), + FaultTerm::Z => (PauliObservable::PlusZ, *q).into(), + FaultTerm::I | FaultTerm::Loss => unreachable!("these terms were filtered"), + }) + .collect(); + self.state.pauli(&observable); } /// Records a z-measurement on the given `target`. @@ -269,7 +264,7 @@ impl StabilizerSimulator { } impl Simulator for StabilizerSimulator { - type Noise = Arc>; + type Noise = Arc; type StateDumpData = paulimer::clifford::CliffordUnitary; fn new(num_qubits: usize, num_results: usize, seed: u32, noise_config: Self::Noise) -> Self { @@ -288,7 +283,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::X, &[target]); - apply_loss!(self, x, &[target]); apply_noise!(self, x, &[target]); } } @@ -297,7 +291,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::Y, &[target]); - apply_loss!(self, y, &[target]); apply_noise!(self, y, &[target]); } } @@ -306,7 +299,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::Z, &[target]); - apply_loss!(self, z, &[target]); apply_noise!(self, z, &[target]); } } @@ -315,7 +307,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); apply_hadamard(&mut self.state, target); - apply_loss!(self, h, &[target]); apply_noise!(self, h, &[target]); } } @@ -324,7 +315,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::SqrtZ, &[target]); - apply_loss!(self, s, &[target]); apply_noise!(self, s, &[target]); } } @@ -333,7 +323,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::SqrtZInv, &[target]); - apply_loss!(self, s_adj, &[target]); apply_noise!(self, s_adj, &[target]); } } @@ -342,7 +331,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::SqrtX, &[target]); - apply_loss!(self, sx, &[target]); apply_noise!(self, sx, &[target]); } } @@ -351,7 +339,6 @@ impl Simulator for StabilizerSimulator { if !self.loss[target] { self.apply_idle_noise(target); self.state.apply_unitary(UnitaryOp::SqrtXInv, &[target]); - apply_loss!(self, sx_adj, &[target]); apply_noise!(self, sx_adj, &[target]); } } @@ -364,7 +351,6 @@ impl Simulator for StabilizerSimulator { .apply_unitary(UnitaryOp::ControlledX, &[control, target]); } // We still apply operation faults to non-lost qubits. - apply_loss!(self, cx, &[control, target]); apply_noise!(self, cx, &[control, target]); } @@ -378,7 +364,6 @@ impl Simulator for StabilizerSimulator { self.state.apply_unitary(UnitaryOp::SqrtZ, &[target]); } // We still apply operation faults to non-lost qubits. - apply_loss!(self, cy, &[control, target]); apply_noise!(self, cy, &[control, target]); } @@ -390,7 +375,6 @@ impl Simulator for StabilizerSimulator { .apply_unitary(UnitaryOp::ControlledZ, &[control, target]); } // We still apply operation faults to non-lost qubits. - apply_loss!(self, cz, &[control, target]); apply_noise!(self, cz, &[control, target]); } @@ -408,7 +392,6 @@ impl Simulator for StabilizerSimulator { ); self.state.apply_unitary(unitary, &[target]); - apply_loss!(self, rx, &[target]); apply_noise!(self, rx, &[target]); } } @@ -427,7 +410,6 @@ impl Simulator for StabilizerSimulator { ); self.state.apply_unitary(unitary, &[target]); - apply_loss!(self, ry, &[target]); apply_noise!(self, ry, &[target]); } } @@ -446,7 +428,6 @@ impl Simulator for StabilizerSimulator { ); self.state.apply_unitary(unitary, &[target]); - apply_loss!(self, rz, &[target]); apply_noise!(self, rz, &[target]); } } @@ -477,7 +458,6 @@ impl Simulator for StabilizerSimulator { self.state.apply_unitary(UnitaryOp::Hadamard, &[q1]); self.state.apply_unitary(UnitaryOp::Hadamard, &[q2]); - apply_loss!(self, rxx, &[q1, q2]); apply_noise!(self, rxx, &[q1, q2]); } } @@ -509,7 +489,6 @@ impl Simulator for StabilizerSimulator { self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q1]); self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q2]); - apply_loss!(self, ryy, &[q1, q2]); apply_noise!(self, ryy, &[q1, q2]); } } @@ -536,7 +515,6 @@ impl Simulator for StabilizerSimulator { self.state.apply_unitary(unitary, &[q1]); self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); - apply_loss!(self, rzz, &[q1, q2]); apply_noise!(self, rzz, &[q1, q2]); } } @@ -571,45 +549,42 @@ impl Simulator for StabilizerSimulator { // Is up to the user if swap is a virtual operation or not. // If they don't specify noise/loss probability for swap, then it is virtual. - apply_loss!(self, swap, &[q1, q2]); apply_noise!(self, swap, &[q1, q2]); } fn mz(&mut self, target: QubitID, result_id: QubitID) { self.apply_idle_noise(target); self.record_mz(target, result_id); - apply_loss!(self, mz, &[target]); apply_noise!(self, mz, &[target]); } fn mresetz(&mut self, target: QubitID, result_id: QubitID) { self.apply_idle_noise(target); self.record_mresetz(target, result_id); - apply_loss!(self, mresetz, &[target]); apply_noise!(self, mresetz, &[target]); } fn resetz(&mut self, target: QubitID) { self.apply_idle_noise(target); self.mresetz_impl(target); - apply_loss!(self, mresetz, &[target]); apply_noise!(self, mresetz, &[target]); } fn mov(&mut self, target: QubitID) { if !self.loss[target] { self.apply_idle_noise(target); - apply_loss!(self, mov, &[target]); apply_noise!(self, mov, &[target]); } } fn correlated_noise_intrinsic(&mut self, intrinsic_id: IntrinsicID, targets: &[usize]) { let fault = match self.noise_config.intrinsics.get(&intrinsic_id) { - Some(correlated_noise) => correlated_noise.sample(&mut self.rng), + Some(correlated_noise) => correlated_noise.sample(&mut self.rng).cloned(), None => return, }; - self.apply_fault(fault, targets); + if let Some(fault) = fault { + self.apply_fault(&fault, targets); + } } fn measurements(&self) -> &[MeasurementResult] { diff --git a/source/simulators/src/stabilizer_simulator/noise.rs b/source/simulators/src/stabilizer_simulator/noise.rs deleted file mode 100644 index 00803290543..00000000000 --- a/source/simulators/src/stabilizer_simulator/noise.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::noise_config::{self, CumulativeNoiseConfig, decode_pauli, is_pauli_identity}; -use paulimer::quantum_core::{self, PauliObservable}; -use rand::RngExt; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum Fault { - /// No fault occurred. - #[default] - None, - /// A Pauli fault. - Pauli(Vec), - /// A gradual dephasing fault. Qubits are always slowly - /// rotating along the Z-axis with an unknown rate, - /// eventually resulting in an `S` gate. - S, - /// The qubit was lost. - Loss, -} - -impl noise_config::Fault for Fault { - fn none() -> Self { - Self::None - } - - fn loss() -> Self { - Self::Loss - } -} - -impl From<(u64, u32)> for Fault { - fn from((pauli, qubits): (u64, u32)) -> Self { - const MAP: [PauliObservable; 4] = [ - PauliObservable::PlusI, - PauliObservable::PlusX, - PauliObservable::PlusZ, - PauliObservable::PlusY, - ]; - assert!( - !is_pauli_identity(pauli), - "the NoiseTable input validation should ensure we don't insert the identity string" - ); - let pauli_product = decode_pauli(pauli, qubits, &MAP); - Self::Pauli(pauli_product) - } -} - -impl CumulativeNoiseConfig { - /// Samples a float in the range [0, 1] and picks one of the faults - /// `X`, `Y`, `Z`, `S` based on the provided noise table. - #[must_use] - pub fn gen_idle_fault(&self, rng: &mut impl rand::Rng, idle_steps: u32) -> Fault { - let sample: f32 = rng.random_range(0.0..1.0); - if sample < self.idle.s_probability(idle_steps) { - Fault::S - } else { - Fault::None - } - } -} From 8fb9f954df8d82f696b6ababe380d7b7019bee7a Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Fri, 12 Jun 2026 12:24:49 -0700 Subject: [PATCH 15/67] gpu changes --- .../tests/test_adaptive_gpu_noise.py | 4 +- .../qdk_package/tests/test_gpu_simulator.py | 6 +- source/simulators/src/bin/gpu-runner.rs | 19 +- .../src/gpu_full_state_simulator/common.wgsl | 239 ++++++++++-------- .../gpu_full_state_simulator/gpu_context.rs | 49 ++-- .../gpu_full_state_simulator/noise_mapping.rs | 153 ++++++----- .../gpu_full_state_simulator/shader_types.rs | 115 ++++----- .../simulator_adaptive.wgsl | 86 ++++--- .../simulator_base.wgsl | 52 ++-- 9 files changed, 416 insertions(+), 307 deletions(-) diff --git a/source/qdk_package/tests/test_adaptive_gpu_noise.py b/source/qdk_package/tests/test_adaptive_gpu_noise.py index 0b7eec71fd8..7cdd7e04f0e 100644 --- a/source/qdk_package/tests/test_adaptive_gpu_noise.py +++ b/source/qdk_package/tests/test_adaptive_gpu_noise.py @@ -275,7 +275,7 @@ def test_noise_intrinsics_load_csv_dir(): noise = NoiseConfig() noise.load_csv_dir("./csv_dir_test") output = run_qir(QIR_WITH_CORRELATED_NOISE, shots=1, noise=noise, type="gpu") - assert output == [[Result.One, Result.Zero, Result.One]] + assert output == [[Result.One, Result.Zero, Result.Loss]] @pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON) @@ -284,7 +284,7 @@ def test_noise_intrinsics_gpu_sim_class(): sim.load_noise_tables("./csv_dir_test") sim.set_program(QIR_WITH_CORRELATED_NOISE) output = sim.run_shots(shots=1)["shot_results"] - assert output == [[Result.One, Result.Zero, Result.One]] + assert output == [[Result.One, Result.Zero, Result.Loss]] NOISE_INTRINSICS_WITH_REGISTERS_QIR = r""" diff --git a/source/qdk_package/tests/test_gpu_simulator.py b/source/qdk_package/tests/test_gpu_simulator.py index 7f1a1169168..45e448f4302 100644 --- a/source/qdk_package/tests/test_gpu_simulator.py +++ b/source/qdk_package/tests/test_gpu_simulator.py @@ -158,9 +158,9 @@ def test_gpu_mixed_noise(): print(result) # Reasonable results obtained from manual run assert result == [ - "00000-00000000-0000000000", - "00100001000-0000000000-00", - "000000010000000-000000000", + "00000000000000-00000000-0", + "0000000-00000000000000000", + "0000000000000000-00000000", ] diff --git a/source/simulators/src/bin/gpu-runner.rs b/source/simulators/src/bin/gpu-runner.rs index 33ee48cf97d..7d85435bbed 100644 --- a/source/simulators/src/bin/gpu-runner.rs +++ b/source/simulators/src/bin/gpu-runner.rs @@ -69,7 +69,10 @@ fn assert_ratio(results: &[Vec], expected: &[u32], expected_ratio: f64, tol fn two_measurements() { let ops: Vec = vec![ Op::new_x_gate(0), - Op::new_loss_noise(0, 0.333), + // 33% chance the qubit is lost after the X gate, sampled by the noise op + // and committed by the following loss-commit op. + Op::new_pauli_noise_1q_with_loss(0, 0.0, 0.0, 0.0, 0.333), + Op::new_loss_commit(0), // Should be 33% chance of lost, 66% chance of 1 // If not using the noise model processing, need to turn pauli on measurement into Id with noise then mesurement @@ -178,9 +181,11 @@ fn gates_on_lost_qubits() { let ops: Vec = vec![ Op::new_x_gate(0), - Op::new_loss_noise(0, 0.1), + Op::new_pauli_noise_1q_with_loss(0, 0.0, 0.0, 0.0, 0.1), + Op::new_loss_commit(0), Op::new_x_gate(1), - Op::new_loss_noise(1, 0.1), + Op::new_pauli_noise_1q_with_loss(1, 0.0, 0.0, 0.0, 0.1), + Op::new_loss_commit(1), Op::new_cx_gate(0, 1), Op::new_rx_gate(angle, 2), Op::new_rx_gate(angle, 2), @@ -732,7 +737,9 @@ fn repeated_noise() { let mut noise: NoiseConfig = NoiseConfig::NOISELESS.clone(); noise.x.pauli_strings.push(encode_pauli("X")); noise.x.probabilities.push(0.001); - noise.x.loss = 0.001; + + noise.x.pauli_strings.push(encode_pauli("L")); + noise.x.probabilities.push(0.001); let start = Instant::now(); // Run for 20,000 shots @@ -827,7 +834,9 @@ fn noise_config() { let mut noise: NoiseConfig = NoiseConfig::NOISELESS.clone(); noise.x.pauli_strings.push(encode_pauli("X")); noise.x.probabilities.push(0.5); - noise.x.loss = 0.333_333; + + noise.x.pauli_strings.push(encode_pauli("L")); + noise.x.probabilities.push(0.333_333); let ops: Vec = vec![ Op::new_x_gate(0), diff --git a/source/simulators/src/gpu_full_state_simulator/common.wgsl b/source/simulators/src/gpu_full_state_simulator/common.wgsl index 907dae91d8d..708dac70378 100644 --- a/source/simulators/src/gpu_full_state_simulator/common.wgsl +++ b/source/simulators/src/gpu_full_state_simulator/common.wgsl @@ -90,7 +90,11 @@ fn shot_init_per_op(shot_idx: u32) { shot.rand_damping = next_rand_f32(shot_idx); shot.rand_dephase = next_rand_f32(shot_idx); shot.rand_measure = next_rand_f32(shot_idx); - shot.rand_loss = next_rand_f32(shot_idx); + // Reserved draw: qubit loss is now sampled from the combined `rand_pauli` + // distribution rather than its own value, but we still advance the RNG by + // one draw here to keep the per-op random stream (and thus seeded results) + // identical to the previous loss model. + next_rand_f32(shot_idx); } // Resets the entire shot state, including RNG, probabilities, and per-qubit tracking. @@ -126,6 +130,7 @@ fn reset_all(shot_idx: i32) { shot.qubit_is_0_mask = (1u << u32(QUBIT_COUNT)) - 1u; // All qubits are |0> shot.qubit_is_1_mask = 0u; shot.qubits_updated_last_op_mask = 0; + shot.pending_loss_mask = 0u; // Initialize all qubit probabilities to 100% |0> for (var i: i32 = 0; i < QUBIT_COUNT; i++) { @@ -221,44 +226,13 @@ fn update_qubit_state(shot_idx: u32) { } } -fn prep_measure_reset(shot_idx: u32, op_idx: u32, is_loss: bool, stores_result: bool, resets_to_zero: bool) { +// Build a measure-and-reset (or measure-only) instrument for `qubit` given a +// measured `result`, store it in the shot buffer, set up renormalization, and +// mark the qubit as no longer in a definite basis state so the execute stage +// recomputes its probabilities. Shared by `prep_measure_reset` and +// `prep_loss_commit`; the caller sets `shot.op_idx` and `shot.op_type`. +fn write_measure_reset_instrument(shot_idx: u32, qubit: u32, result: u32, resets_to_zero: bool) { let shot = &shots[shot_idx]; - let op = &ops[op_idx]; - - // Choose measurement result based on qubit probabilities and random number - let qubit = get_measure_qubit(shot_idx, op_idx); - let result = select(1u, 0u, shot.rand_measure < shot.qubit_state[qubit].zero_probability); - - // If this is being called due to loss noise, we don't write the result back to the results buffer - // Instead, mark the qubit as lost by setting the heat to -1.0 - if !is_loss { - if stores_result { - let result_id = get_measure_result(shot_idx, op_idx); // Result id to store the measurement result in is stored in q2 - - // If the qubit is already marked as lost, just report that and exit. It's already in the zero - // state so nothing to update or renormalize. The execute op should be a no-op (ID) - if shot.qubit_state[qubit].heat == -1.0 { - atomicStore(&results[(shot_idx * RESULT_COUNT) + result_id], 2u); - shot.op_type = OPID_ID; - shot.op_idx = op_idx; - // Qubit get reloaded after a Measurement, so set the heat back to 0.0 - shot.qubit_state[qubit].heat = 0.0; - return; - } else { - atomicStore(&results[(shot_idx * RESULT_COUNT) + result_id], result); - } - } else { - // No result to store (e.g. ResetZ). If the qubit is lost, it's already in the zero - // state so nothing to update. Just set to ID and return. - if shot.qubit_state[qubit].heat == -1.0 { - shot.op_type = OPID_ID; - shot.op_idx = op_idx; - return; - } - } - } else { - shot.qubit_state[qubit].heat = -1.0; - } // Construct the measurement/reset instrument based on the measured result // Put the instrument into the shot buffer for the execute_op stage to apply @@ -297,6 +271,48 @@ fn prep_measure_reset(shot_idx: u32, op_idx: u32, is_loss: bool, stores_result: ((1u << u32(QUBIT_COUNT)) - 1u) // Exclude qubits already in definite states & ~(shot.qubit_is_0_mask | shot.qubit_is_1_mask); +} + +fn prep_measure_reset(shot_idx: u32, op_idx: u32, is_loss: bool, stores_result: bool, resets_to_zero: bool) { + let shot = &shots[shot_idx]; + let op = &ops[op_idx]; + + // Choose measurement result based on qubit probabilities and random number + let qubit = get_measure_qubit(shot_idx, op_idx); + let result = select(1u, 0u, shot.rand_measure < shot.qubit_state[qubit].zero_probability); + + // If this is being called due to loss noise, we don't write the result back to the results buffer + // Instead, mark the qubit as lost by setting the heat to -1.0 + if !is_loss { + if stores_result { + let result_id = get_measure_result(shot_idx, op_idx); // Result id to store the measurement result in is stored in q2 + + // If the qubit is already marked as lost, just report that and exit. It's already in the zero + // state so nothing to update or renormalize. The execute op should be a no-op (ID) + if shot.qubit_state[qubit].heat == -1.0 { + atomicStore(&results[(shot_idx * RESULT_COUNT) + result_id], 2u); + shot.op_type = OPID_ID; + shot.op_idx = op_idx; + // Qubit get reloaded after a Measurement, so set the heat back to 0.0 + shot.qubit_state[qubit].heat = 0.0; + return; + } else { + atomicStore(&results[(shot_idx * RESULT_COUNT) + result_id], result); + } + } else { + // No result to store (e.g. ResetZ). If the qubit is lost, it's already in the zero + // state so nothing to update. Just set to ID and return. + if shot.qubit_state[qubit].heat == -1.0 { + shot.op_type = OPID_ID; + shot.op_idx = op_idx; + return; + } + } + } else { + shot.qubit_state[qubit].heat = -1.0; + } + + write_measure_reset_instrument(shot_idx, qubit, result, resets_to_zero); shot.op_idx = op_idx; // Use OPID_MRESETZ as the op_type for all three variants in execute stage @@ -315,17 +331,6 @@ fn get_pauli_noise_idx(op_idx: u32) -> u32 { return 0u; } -// From the starting index given, return the next index if loss noise, else 0 -fn get_loss_idx(op_idx: u32) -> u32 { - if (arrayLength(&ops) > (op_idx + 1)) { - let op = &ops[op_idx + 1]; - if (op.id == OPID_LOSS_NOISE) { - return op_idx + 1u; - } - } - return 0u; -} - fn apply_1q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { // NOTE: Assumes that whatever prepared the program ensured that noise_op.q1 matches op.q1 and @@ -334,11 +339,13 @@ fn apply_1q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { let op = &ops[op_idx]; let noise_op = &ops[noise_idx]; - // Apply 1-qubit Pauli noise based on the probabilities in the op data, which are stored in - // the real part (x) of the first 4 vec2 entries of the unitary array (ignore [0] which is "I") - let p_x = noise_op.unitary[1].x; - let p_y = noise_op.unitary[2].x; - let p_z = noise_op.unitary[3].x; + // Categorical outcome probabilities by 3-bit term (X=1, Z=2, Y=3, L=4), + // stored at flat slot k = term in `unitary[k / 2][k % 2]`. The identity + // outcome (slot 0) is implicit. + let p_x = noise_op.unitary[0].y; + let p_z = noise_op.unitary[1].x; + let p_y = noise_op.unitary[1].y; + let p_loss = noise_op.unitary[2].x; shot.op_type = OPID_SHOT_BUFF_1Q; // Indicate to use the matrix in the shot buffer @@ -349,19 +356,25 @@ fn apply_1q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { shot.unitary[1] = op.unitary[5]; shot.unitary[4] = op.unitary[0]; shot.unitary[5] = op.unitary[1]; - } else if (rand < (p_x + p_y)) { - // Apply the Y permutation (swap rows with negated |0> state) - shot.unitary[0] = cplxNeg(op.unitary[4]); - shot.unitary[1] = cplxNeg(op.unitary[5]); - shot.unitary[4] = op.unitary[0]; - shot.unitary[5] = op.unitary[1]; - } else if (rand < (p_x + p_y + p_z)) { + } else if (rand < (p_x + p_z)) { // Apply Z error (negate |1> state) shot.unitary[0] = op.unitary[0]; shot.unitary[1] = op.unitary[1]; shot.unitary[4] = cplxNeg(op.unitary[4]); shot.unitary[5] = cplxNeg(op.unitary[5]); + } else if (rand < (p_x + p_z + p_y)) { + // Apply the Y permutation (swap rows with negated |0> state) + shot.unitary[0] = cplxNeg(op.unitary[4]); + shot.unitary[1] = cplxNeg(op.unitary[5]); + shot.unitary[4] = op.unitary[0]; + shot.unitary[5] = op.unitary[1]; } else { + // Either loss or no noise: the gate executes unmodified. If loss was + // sampled, schedule a loss commit for this qubit; a following + // loss-commit op performs the measure + reset. + if (rand < (p_x + p_z + p_y + p_loss)) { + shot.pending_loss_mask |= (1u << op.q1); + } // No noise. Set the op_type back to the op.id value if it's Id, MResetZ, MZ, or ResetZ, as they get handled specially in execute_op if (op.id == OPID_ID || op.id == OPID_MRESETZ || op.id == OPID_MZ || op.id == OPID_RESETZ) { shot.op_type = op.id; @@ -385,34 +398,44 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { let op = &ops[op_idx]; let noise_op = &ops[noise_idx]; - // Correlated noise is stored in the real parts of the unitary. - // unitary[0] = II, unitary[1] = IX, unitary[2] = IY, unitary[3] = IZ - // unitary[4] = XI, unitary[5] = XX, unitary[6] = XY, unitary[7] = XZ - // unitary[8] = YI, unitary[9] = YX, unitary[10]= YY, unitary[11]= YZ - // unitary[12]= ZI, unitary[13]= ZX, unitary[14]= ZY, unitary[15]= ZZ - + // The categorical distribution over the 25 (q1_term, q2_term) outcomes is + // stored at flat slot k = q1_term * 5 + q2_term in `unitary[k / 2][k % 2]`. + // Terms use the 3-bit encoding: I=0, X=1, Z=2, Y=3, L=4. The II slot (0) is + // implicit and carries the remaining probability. var rand = shot.rand_pauli; - var q1_pauli = 0; - var q2_pauli = 0; - - // Find the paulis to apply based on the random number and the probabilities - for (var i = 0; i < 4; i = i + 1) { - for (var j = 0; j < 4; j = j + 1) { - let p_ij = noise_op.unitary[i * 4 + j].x; - if (rand < p_ij) { - q1_pauli = i; - q2_pauli = j; + var q1_term = 0; + var q2_term = 0; + + // Find the terms to apply based on the random number and the probabilities + for (var a = 0; a < 5; a = a + 1) { + for (var b = 0; b < 5; b = b + 1) { + let k = a * 5 + b; + if (k == 0) { continue; } // II carries no stored probability + let slot = noise_op.unitary[k / 2]; + let p_ab = select(slot.x, slot.y, (k & 1) == 1); + if (rand < p_ab) { + q1_term = a; + q2_term = b; // Break out of both loops - i = 4; - j = 4; + a = 5; + b = 5; } else { - rand = rand - p_ij; + rand = rand - p_ab; } } } - // Only apply noise if needed - if (q1_pauli != 0 || q2_pauli != 0) { + // Schedule loss commits for any qubit whose sampled term is loss (L = 4). + // A following loss-commit op performs the measure + reset. + if (q1_term == 4) { shot.pending_loss_mask |= (1u << op.q1); } + if (q2_term == 4) { shot.pending_loss_mask |= (1u << op.q2); } + + // A Pauli fault (X, Z, Y = 1, 2, 3) is fused into the gate by permuting its + // rows. Loss (4) and identity (0) leave the gate unmodified for that qubit. + let q1_pauli = q1_term >= 1 && q1_term <= 3; + let q2_pauli = q2_term >= 1 && q2_term <= 3; + + if (q1_pauli || q2_pauli) { // Get the rows of the 2 qubit unitary var op_row_0 = getOpRow(op_idx, 0); var op_row_1 = getOpRow(op_idx, 1); @@ -426,7 +449,7 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { // Z on q1 is -2 and -3, Z on q2 is -1 and -3 // Apply the q1 permutations as needed - if (q1_pauli == 1) { + if (q1_term == 1) { // Apply the X permutation let old_row_0 = op_row_0; let old_row_1 = op_row_1; @@ -434,7 +457,7 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { op_row_1 = op_row_3; op_row_2 = old_row_0; op_row_3 = old_row_1; - } else if (q1_pauli == 2) { + } else if (q1_term == 3) { // Apply the Y permutation let old_row_0 = op_row_0; let old_row_1 = op_row_1; @@ -442,13 +465,13 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { op_row_1 = rowNeg(op_row_3); op_row_2 = old_row_0; op_row_3 = old_row_1; - } else if (q1_pauli == 3) { + } else if (q1_term == 2) { // Apply Z permutation op_row_2 = rowNeg(op_row_2); op_row_3 = rowNeg(op_row_3); } // Apply the q2 permutations as needed - if (q2_pauli == 1) { + if (q2_term == 1) { // Apply the X permutation let old_row_0 = op_row_0; let old_row_2 = op_row_2; @@ -456,7 +479,7 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { op_row_2 = op_row_3; op_row_1 = old_row_0; op_row_3 = old_row_2; - } else if (q2_pauli == 2) { + } else if (q2_term == 3) { // Apply the Y permutation let old_row_0 = op_row_0; let old_row_2 = op_row_2; @@ -464,7 +487,7 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { op_row_2 = rowNeg(op_row_3); op_row_1 = old_row_0; op_row_3 = old_row_2; - } else if (q2_pauli == 3) { + } else if (q2_term == 2) { // Apply Z permutation op_row_1 = rowNeg(op_row_1); op_row_3 = rowNeg(op_row_3); @@ -476,7 +499,7 @@ fn apply_2q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { setUnitaryRow(shot_idx, 3u, op_row_3); shot.op_type = OPID_SHOT_BUFF_2Q; } else { - // No noise to apply. Leave if CX, CY, CZ, or RZZ as they get handled specially in execute_op + // No Pauli fault to fuse (identity or loss only). Leave if CX, CY, CZ, or RZZ as they get handled specially in execute_op if (op.id == OPID_CX || op.id == OPID_CY || op.id == OPID_CZ || op.id == OPID_RZZ) { shot.op_type = op.id; } else { @@ -579,8 +602,8 @@ fn apply_1q_op(workgroupId: u32, tid: u32, q1: u32) { stateVector[params.shot_state_vector_start + offset0] = new0; stateVector[params.shot_state_vector_start + offset1] = new1; - if shot.op_type == OPID_MRESETZ || scale != 1.0 { - // For MResetZ or renormalization, we need to update the probabilities for all qubits + if shot.op_type == OPID_MRESETZ || shot.op_type == OPID_LOSS_NOISE || scale != 1.0 { + // For MResetZ, loss-commit, or renormalization, update the probabilities for all qubits update_all_qubit_probs(u32(offset0), new0, tid); update_all_qubit_probs(u32(offset1), new1, tid); } else { @@ -592,7 +615,7 @@ fn apply_1q_op(workgroupId: u32, tid: u32, q1: u32) { entry_index += params.total_threads_per_shot; } - if scale == 1.0 && shot.op_type != OPID_RZ && shot.op_type != OPID_MRESETZ { + if scale == 1.0 && shot.op_type != OPID_RZ && shot.op_type != OPID_MRESETZ && shot.op_type != OPID_LOSS_NOISE { // Update this thread's totals for the two qubits in the workgroup storage qubitProbabilities[tid].zero[q1] = summed_probs[0]; qubitProbabilities[tid].one[q1] = summed_probs[1]; @@ -929,23 +952,37 @@ fn sample_correlated_noise(shot_idx: u32, op_idx: u32, noise_table_idx: u32) -> return CorrelatedNoiseSample(1u, entry.paulis_lo, entry.paulis_hi); } -// Extracts the 2-bit Pauli value for qubit position `i` from a Pauli string. -// The Rust parsing stores paulis with the rightmost (last) character at the lowest bits, -// so for position i we need bits at (qubit_count - 1 - i) * 2. +// Extracts the 3-bit term value for qubit position `i` from a Pauli + loss string. +// Terms use the encoding I=0, X=1, Z=2, Y=3, L=4. The low two bits double as the +// bit-flip (0x1) and phase-flip (0x2) indicators, and 0x4 marks loss. +// The Rust parsing stores terms with the rightmost (last) character at the lowest +// bits, so for position i we read the 3 bits at (qubit_count - 1 - i) * 3. fn get_pauli_bits(paulis_lo: u32, paulis_hi: u32, qubit_count: u32, i: u32) -> u32 { - let bit_position = qubit_count - 1u - i; - if (bit_position < 16u) { - return (paulis_lo >> (bit_position * 2u)) & 0x3u; + let bit_position = (qubit_count - 1u - i) * 3u; + if (bit_position + 3u <= 32u) { + return (paulis_lo >> bit_position) & 0x7u; + } else if (bit_position >= 32u) { + return (paulis_hi >> (bit_position - 32u)) & 0x7u; } else { - return (paulis_hi >> ((bit_position - 16u) * 2u)) & 0x3u; + // The 3-bit term straddles the boundary between the lo and hi words. + let low_part = paulis_lo >> bit_position; + let high_part = paulis_hi << (32u - bit_position); + return (low_part | high_part) & 0x7u; } } // Commits correlated noise masks into the shot state: stores the masks, swaps probabilities and -// tracking bits for bit-flipped qubits, and sets the shot up for the correlated noise execute stage. -fn commit_correlated_noise(shot_idx: u32, op_idx: u32, bit_flip_mask: u32, phase_flip_mask: u32) { +// tracking bits for bit-flipped qubits, records any loss, and sets the shot up for the correlated +// noise execute stage. Qubits in `loss_mask` are scheduled for loss; following loss-commit ops +// perform the measure + reset. +fn commit_correlated_noise(shot_idx: u32, op_idx: u32, bit_flip_mask: u32, phase_flip_mask: u32, loss_mask: u32) { let shot = &shots[shot_idx]; + // Schedule loss for any qubit whose sampled term was loss. The actual + // measure + reset is performed by the loss-commit ops emitted after the + // correlated-noise op. + shot.pending_loss_mask |= loss_mask; + // Store the masks in the shot buffer for the execute stage // We use the unitary entries to store these masks (reinterpreted as floats) shot.unitary[0] = vec2f(bitcast(bit_flip_mask), bitcast(phase_flip_mask)); diff --git a/source/simulators/src/gpu_full_state_simulator/gpu_context.rs b/source/simulators/src/gpu_full_state_simulator/gpu_context.rs index b77672e680c..56df1b87e73 100644 --- a/source/simulators/src/gpu_full_state_simulator/gpu_context.rs +++ b/source/simulators/src/gpu_full_state_simulator/gpu_context.rs @@ -10,7 +10,7 @@ use crate::bytecode::AdaptiveProgram; use crate::correlated_noise::NoiseTables; use crate::gpu_resources::GpuResources; use crate::noise_config::NoiseConfig; -use crate::noise_mapping::get_noise_ops; +use crate::noise_mapping::{expand_correlated_loss_commits, get_noise_ops}; use crate::shader_types::{ self, DiagnosticsData, InterpreterState, MAX_ALLOCA_SIZE, MAX_BUFFER_SIZE, MAX_QUBIT_COUNT, MAX_QUBITS_PER_WORKGROUP, MAX_REGISTERS, MAX_SHOTS_PER_BATCH, MIN_QUBIT_COUNT, MIN_REGISTERS, @@ -260,8 +260,9 @@ impl GpuContext { let mut non_noise_count = 0; for op in self.get_program() { - // Check if this is a noise op - let is_noise = matches!(op.id, ops::PAULI_NOISE_1Q..=ops::LOSS_NOISE); + // Check if this is a noise op. Pauli noise ops are consumed inline + // by the preceding gate; loss-commit ops are dispatched on their own. + let is_noise = matches!(op.id, ops::PAULI_NOISE_1Q..=ops::PAULI_NOISE_2Q); if !is_noise { non_noise_count += 1; @@ -308,15 +309,16 @@ impl GpuContext { | ops::CX..=ops::RZZ | ops::CY | ops::SWAP - | ops::CORRELATED_NOISE => { + | ops::CORRELATED_NOISE + | ops::LOSS_NOISE => { compute_pass.set_pipeline(&kernels.prepare_op); compute_pass.dispatch_workgroups(prepare_workgroup_count, 1, 1); compute_pass.set_pipeline(&kernels.execute_op); compute_pass.dispatch_workgroups(execute_workgroup_count, 1, 1); } - // Skip over simple noise ops - ops::PAULI_NOISE_1Q..=ops::LOSS_NOISE => {} + // Pauli noise ops are consumed inline by the preceding gate + ops::PAULI_NOISE_1Q..=ops::PAULI_NOISE_2Q => {} _ => { panic!("Unsupported op ID {}", op.id); } @@ -379,14 +381,16 @@ impl GpuContext { if self.program_is_dirty || self.noise_config_is_dirty { // Rebuild the program (with noise if needed) and upload to GPU - if let Some(noise) = &self.noise_config { - let ops = add_noise_config_to_ops(&self.program, noise); - self.resources.upload_ops_data(cast_slice(&ops))?; - self.program_with_noise = Some(ops); + let base_ops = if let Some(noise) = &self.noise_config { + &add_noise_config_to_ops(&self.program, noise) } else { - self.resources.upload_ops_data(cast_slice(&self.program))?; - self.program_with_noise = None; - } + &self.program + }; + // Insert loss-commit ops after correlated-noise ops so intrinsic + // tables containing loss strings can lose qubits on the GPU. + let ops = expand_correlated_loss_commits(base_ops); + self.resources.upload_ops_data(cast_slice(&ops))?; + self.program_with_noise = Some(ops); self.program_is_dirty = false; } @@ -928,8 +932,10 @@ fn add_noise_config_to_ops(ops: &[Op], noise: &NoiseConfig) -> Vec for op in ops { let mut add_ops: Vec = vec![*op]; - // If there's a NoiseConfig, and we get noise for this op, append it - if let Some(noise_ops) = get_noise_ops(op, noise) { + // If there's a NoiseConfig, and we get noise for this op, append it. + // The base path dispatches ops linearly, so it needs explicit + // loss-commit ops to perform any deferred qubit loss. + if let Some(noise_ops) = get_noise_ops(op, noise, true) { add_ops.extend(noise_ops); } // If it's an MResetZ, MZ, or ResetZ with noise, change to an Id with noise, followed by the original op @@ -957,12 +963,17 @@ fn add_noise_config_to_ops(ops: &[Op], noise: &NoiseConfig) -> Vec /// Expand the adaptive quantum op pool with noise ops. /// -/// For each original op, this emits the op itself and then any Pauli/loss -/// noise ops from the `NoiseConfig`. Unlike `add_noise_config_to_ops` used +/// For each original op, this emits the op itself and then the Pauli/loss +/// sampler op from the `NoiseConfig`. Unlike `add_noise_config_to_ops` used /// by the non-adaptive path, this does *not* reorder measure/reset ops or /// drop identity gates, because the adaptive bytecode interpreter references /// each op by index and handles measure/reset at the instruction level. /// +/// It also does *not* emit loss-commit ops: the adaptive interpreter commits +/// any sampled qubit loss by draining `pending_loss_mask` in its run loop, so +/// emitting per-gate loss-commit ops here would only bloat the op pool with +/// entries that are never dispatched. +/// /// Returns `(expanded_ops, index_map)` where `index_map[old_idx]` gives /// the new index of the original op in the expanded pool. fn add_noise_to_adaptive_ops( @@ -980,8 +991,8 @@ fn add_noise_to_adaptive_ops( noisy_ops.push(*op); - // Append any noise ops (pauli + loss) from the config - if let Some(noise_ops) = get_noise_ops(op, noise) { + // Append the Pauli/loss sampler op (no loss-commit ops; see above). + if let Some(noise_ops) = get_noise_ops(op, noise, false) { noisy_ops.extend(noise_ops); } } diff --git a/source/simulators/src/gpu_full_state_simulator/noise_mapping.rs b/source/simulators/src/gpu_full_state_simulator/noise_mapping.rs index 378f9716c36..03ddff440ec 100644 --- a/source/simulators/src/gpu_full_state_simulator/noise_mapping.rs +++ b/source/simulators/src/gpu_full_state_simulator/noise_mapping.rs @@ -2,76 +2,80 @@ // Licensed under the MIT License. use crate::{ - noise_config::{NoiseConfig, NoiseTable}, + noise_config::{NoiseConfig, NoiseTable, PauliAndLossString}, shader_types::{Op, ops}, }; -// Use the 'real' parts of the Op to store the pauli probabilities. -// For 1q ops, r00 = pI, r01 = pX, r02 = pY, r03 = pZ -// For 2q ops, r00 = pII, r01 = pIX, r02 = pIY, r03 = pIZ -// r10 = pXI, r11 = pXX, r12 = pXY, r13 = pXZ -// r20 = pYI, r21 = pYX, r22 = pYY, r23 = pYZ -// r30 = pZI, r31 = pZX, r32 = pZY, r33 = pZZ -fn set_noise_op_probabilities(noise_table: &NoiseTable, op: &mut Op) { +/// The 3-bit term value representing qubit loss (matches `encode_pauli`). +const LOSS_TERM: u64 = 4; + +/// Decodes the categorical-outcome flat slot for a fault string. Outcomes are +/// stored in the noise op's matrix floats; the host writes slot `k` and the +/// shader reads it as `unitary[k / 2][k % 2]`. +/// +/// Terms use the 3-bit encoding (I=0, X=1, Z=2, Y=3, L=4). For a 1-qubit table +/// the slot is the single qubit's term; for a 2-qubit table it is +/// `q1_term * 5 + q2_term` (5 possible terms per qubit). The identity outcome +/// (slot 0) is implicit and never stored. +fn outcome_slot(pauli: PauliAndLossString, qubits: u32) -> usize { + match qubits { + 1 => (pauli & 0b111) as usize, + 2 => { + let q1_term = ((pauli >> 3) & 0b111) as usize; + let q2_term = (pauli & 0b111) as usize; + q1_term * 5 + q2_term + } + _ => panic!("Unsupported qubit count in noise table: {qubits}"), + } +} + +/// Returns true if any fault string in the table loses a qubit. +fn table_has_loss(noise_table: &NoiseTable) -> bool { noise_table + .pauli_strings + .iter() + .any(|p| (0..noise_table.qubits).any(|i| (p >> (i * 3)) & 0b111 == LOSS_TERM)) +} + +fn set_noise_op_probabilities(noise_table: &NoiseTable, op: &mut Op) { + for (pauli, prob) in noise_table .pauli_strings .iter() .zip(&noise_table.probabilities) - .for_each(|(pauli, prob)| match noise_table.qubits { - 1 => match pauli { - 0b_00 => op.r00 = *prob, - 0b_01 => op.r01 = *prob, - 0b_11 => op.r02 = *prob, - 0b_10 => op.r03 = *prob, - _ => panic!("Invalid pauli string for 1 qubit: {pauli}"), - }, - 2 => match pauli { - 0b_00_00 => op.r00 = *prob, - 0b_00_01 => op.r01 = *prob, - 0b_00_11 => op.r02 = *prob, - 0b_00_10 => op.r03 = *prob, - 0b_01_00 => op.r10 = *prob, - 0b_01_01 => op.r11 = *prob, - 0b_01_11 => op.r12 = *prob, - 0b_01_10 => op.r13 = *prob, - 0b_11_00 => op.r20 = *prob, - 0b_11_01 => op.r21 = *prob, - 0b_11_11 => op.r22 = *prob, - 0b_11_10 => op.r23 = *prob, - 0b_10_00 => op.r30 = *prob, - 0b_10_01 => op.r31 = *prob, - 0b_10_11 => op.r32 = *prob, - 0b_10_10 => op.r33 = *prob, - _ => panic!("Invalid pauli string for 2 qubits: {pauli}"), - }, - _ => panic!( - "Unsupported qubit count in noise table: {}", - noise_table.qubits - ), - }); + { + op.set_noise_prob_slot(outcome_slot(*pauli, noise_table.qubits), *prob); + } } fn get_noise_op(op: &Op, noise_table: &NoiseTable) -> Op { - match noise_table.qubits { - 1 => { - let mut op = Op::new_1q_gate(ops::PAULI_NOISE_1Q, op.q1); - set_noise_op_probabilities(noise_table, &mut op); - op - } - 2 => { - let mut op = Op::new_2q_gate(ops::PAULI_NOISE_2Q, op.q1, op.q2); - set_noise_op_probabilities(noise_table, &mut op); - op - } + let mut noise_op = match noise_table.qubits { + 1 => Op::new_1q_gate(ops::PAULI_NOISE_1Q, op.q1), + 2 => Op::new_2q_gate(ops::PAULI_NOISE_2Q, op.q1, op.q2), _ => panic!( "Unsupported qubit count in noise table: {}", noise_table.qubits ), - } + }; + set_noise_op_probabilities(noise_table, &mut noise_op); + noise_op } +/// Builds the noise ops to insert after `op` for the given config, or `None` +/// if the gate is noiseless. +/// +/// `emit_loss_commits` controls whether loss-commit ops are appended after the +/// categorical sampler op. The base (non-adaptive) path dispatches ops linearly +/// and needs an explicit loss-commit op per qubit to perform the deferred +/// measure + reset, so it passes `true`. The adaptive path instead drains +/// `pending_loss_mask` inside the interpreter loop, so it passes `false` to +/// avoid emitting loss-commit ops that would never be dispatched (which would +/// otherwise roughly double the op pool for circuits with loss on every gate). #[must_use] -pub fn get_noise_ops(op: &Op, noise_config: &NoiseConfig) -> Option> { +pub fn get_noise_ops( + op: &Op, + noise_config: &NoiseConfig, + emit_loss_commits: bool, +) -> Option> { let noise_table = match op.id { ops::ID => &noise_config.i, ops::X => &noise_config.x, @@ -102,24 +106,43 @@ pub fn get_noise_ops(op: &Op, noise_config: &NoiseConfig) -> Option 0.0 { + if emit_loss_commits && table_has_loss(noise_table) { if ops::is_2q_op(op.id) { - // For two-qubit gates, doing loss inline is hard, so just append an Id gate with loss for each qubit - results.push(Op::new_id_gate(op.q1)); - results.push(Op::new_loss_noise(op.q1, noise_table.loss)); - results.push(Op::new_id_gate(op.q2)); - results.push(Op::new_loss_noise(op.q2, noise_table.loss)); + results.push(Op::new_loss_commit(op.q1)); + results.push(Op::new_loss_commit(op.q2)); } else if ops::is_1q_op(op.id) { - // For one-qubit gates, just add the loss noise on the one qubit operation - results.push(Op::new_loss_noise(op.q1, noise_table.loss)); + results.push(Op::new_loss_commit(op.q1)); } else { panic!("unsupported op for loss noise: {op:?}"); } } Some(results) } + +/// Expand a program by inserting a loss-commit op after each correlated-noise +/// op, one per targeted qubit. Correlated-noise (intrinsic) tables can contain +/// loss strings, and the shader records any sampled loss in `pending_loss_mask`; +/// these loss-commit ops perform the deferred measure + reset. Ops with no loss +/// sampled leave the mask clear, so the loss-commit acts as identity. +#[must_use] +pub fn expand_correlated_loss_commits(ops: &[Op]) -> Vec { + // Most programs have no correlated noise, so start from the existing length. + let mut out = Vec::with_capacity(ops.len()); + for op in ops { + out.push(*op); + if op.id == ops::CORRELATED_NOISE { + // q2 holds the number of qubits the correlated op targets. + for i in 0..op.q2 { + let q = op.correlated_noise_qubit(i); + out.push(Op::new_loss_commit(q)); + } + } + } + out +} diff --git a/source/simulators/src/gpu_full_state_simulator/shader_types.rs b/source/simulators/src/gpu_full_state_simulator/shader_types.rs index edf26ec1475..97a33cdc4f7 100644 --- a/source/simulators/src/gpu_full_state_simulator/shader_types.rs +++ b/source/simulators/src/gpu_full_state_simulator/shader_types.rs @@ -98,7 +98,9 @@ pub struct ShotData { pub rand_damping: f32, pub rand_dephase: f32, pub rand_measure: f32, - pub rand_loss: f32, + // Bitmask of qubits that the most recent noise sampler decided to lose. A + // following loss-commit op consumes (and clears) its qubit's bit. + pub pending_loss_mask: u32, pub op_type: u32, pub op_idx: u32, pub duration: f32, @@ -664,76 +666,45 @@ impl Op { #[must_use] pub fn new_pauli_noise_1q(qubit: u32, p_x: f32, p_y: f32, p_z: f32) -> Self { - let mut op = Self::new_1q_gate(ops::PAULI_NOISE_1Q, qubit); - op.r00 = 1.0 - (p_x + p_y + p_z); - op.r01 = p_x; - op.r02 = p_y; - op.r03 = p_z; - op + Self::new_pauli_noise_1q_with_loss(qubit, p_x, p_y, p_z, 0.0) } + /// Build a 1-qubit per-gate noise op whose categorical distribution covers + /// the Pauli faults and qubit loss. Outcome probabilities are stored at flat + /// slots indexed by the 3-bit term encoding (X=1, Z=2, Y=3, L=4); the implicit + /// identity slot (0) holds the remaining probability at sample time. #[must_use] - #[allow(clippy::similar_names)] - #[allow(clippy::too_many_arguments)] - pub fn new_pauli_noise_2q( - q1: u32, - q2: u32, - p_ix: f32, - p_iy: f32, - p_iz: f32, - p_xi: f32, - p_xx: f32, - p_xy: f32, - p_xz: f32, - p_yi: f32, - p_yx: f32, - p_yy: f32, - p_yz: f32, - p_zi: f32, - p_zx: f32, - p_zy: f32, - p_zz: f32, + pub fn new_pauli_noise_1q_with_loss( + qubit: u32, + p_x: f32, + p_y: f32, + p_z: f32, + p_loss: f32, ) -> Self { - let mut op = Self::new_2q_gate(ops::PAULI_NOISE_2Q, q1, q2); - op.r00 = 1.0 - - (p_ix - + p_iy - + p_iz - + p_xi - + p_xx - + p_xy - + p_xz - + p_yi - + p_yx - + p_yy - + p_yz - + p_zi - + p_zx - + p_zy - + p_zz); - op.r01 = p_ix; - op.r02 = p_iy; - op.r03 = p_iz; - op.r10 = p_xi; - op.r11 = p_xx; - op.r12 = p_xy; - op.r13 = p_xz; - op.r20 = p_yi; - op.r21 = p_yx; - op.r22 = p_yy; - op.r23 = p_yz; - op.r30 = p_zi; - op.r31 = p_zx; - op.r32 = p_zy; - op.r33 = p_zz; + let mut op = Self::new_1q_gate(ops::PAULI_NOISE_1Q, qubit); + op.set_noise_prob_slot(1, p_x); + op.set_noise_prob_slot(2, p_z); + op.set_noise_prob_slot(3, p_y); + op.set_noise_prob_slot(4, p_loss); op } + /// Store the probability `prob` for the categorical noise outcome `slot` + /// in the op's matrix-storage floats. The host and shader agree that flat + /// slot `k` maps to WGSL `unitary[k / 2][k % 2]`. + pub fn set_noise_prob_slot(&mut self, slot: usize, prob: f32) { + // Op is 4 leading u32 fields followed by 32 matrix floats + // (r00, i00, r01, i01, ...), so matrix flat slot `k` lives at index `4 + k`. + let floats: &mut [f32; 36] = bytemuck::cast_mut(self); + floats[4 + slot] = prob; + } + #[must_use] - pub fn new_loss_noise(qubit: u32, p_loss: f32) -> Self { - let mut op = Self::new_1q_gate(ops::LOSS_NOISE, qubit); - op.r00 = p_loss; - op + pub fn new_loss_commit(qubit: u32) -> Self { + // A loss-commit op carries no probability. It commits a loss (measure + + // reset) when the preceding noise sampler set this qubit's bit in + // `pending_loss_mask`, and otherwise acts as identity. + Self::new_1q_gate(ops::LOSS_NOISE, qubit) } /// Create a new 2-qubit gate Op with default values @@ -952,6 +923,24 @@ impl Op { op } + /// Read the qubit ID at `index` from a correlated-noise op, the inverse of + /// the qubit packing done in `new_correlated_noise_gate` (qubit `i` is stored + /// in matrix flat slot `i`). + #[must_use] + pub fn correlated_noise_qubit(&self, index: u32) -> u32 { + let floats: &[f32; 36] = bytemuck::cast_ref(self); + // The 4 leading u32 fields precede the 32 matrix floats. Qubit ids are + // stored as exact f32 values (range limited to 32), mirroring how the + // shader reads them back as u32. + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "qubit ids are small non-negative integers stored exactly as f32" + )] + let qubit = floats[4 + index as usize] as u32; + qubit + } + /// Custom 1-qubit operation with arbitrary matrix elements /// K = [[_00r + i*_00i, _01r + i*_01i], /// [_10r + i*_10i, _11r + i*_11i]] diff --git a/source/simulators/src/gpu_full_state_simulator/simulator_adaptive.wgsl b/source/simulators/src/gpu_full_state_simulator/simulator_adaptive.wgsl index 4f317a598bf..44ab524befc 100644 --- a/source/simulators/src/gpu_full_state_simulator/simulator_adaptive.wgsl +++ b/source/simulators/src/gpu_full_state_simulator/simulator_adaptive.wgsl @@ -39,7 +39,8 @@ struct ShotData { rand_damping: f32, rand_dephase: f32, rand_measure: f32, - rand_loss: f32, + // Bitmask of qubits the most recent noise sampler chose to lose. + pending_loss_mask: u32, // The type of the next operation to execute. This will be OPID_SHOT_BUFF_* if it should use the unitary from the op buffer op_type: u32, @@ -288,6 +289,12 @@ const STATUS_TERMINATED: u32 = 2u; const STATUS_ERROR: u32 = 3u; const STATUS_YIELD: u32 = 4u; +// pending_op_type values: 0 = gate, 1 = measure, 2 = reset, 3 = loss commit. +// A loss-commit pending op carries the lost qubit in pending_op_idx (not an +// ops-pool index) and is produced while draining pending_loss_mask. Its value +// must not collide with the gate/measure/reset types resolved in prepare_op. +const PENDING_OP_LOSS_COMMIT: u32 = 3u; + // ----------------------------------------------------------------------------- // Adaptive interpreter — opcodes // ----------------------------------------------------------------------------- @@ -547,6 +554,19 @@ struct QubitProbabilityPerThread { var qubitProbabilities: array; // Workgroup memory size: THREADS_PER_WORKGROUP (32) * 216 = 6,912 bytes. +// Commit a sampled qubit loss on an explicitly given qubit (measure + reset to +// |0> and mark the qubit lost). The lost qubit is carried to the execute stage +// in `op_idx`, and `op_type` is set to OPID_LOSS_NOISE so execute applies the +// reset matrix to that explicit qubit. +fn prep_loss_commit(shot_idx: u32, qubit: u32) { + let shot = &shots[shot_idx]; + let result = select(1u, 0u, shot.rand_measure < shot.qubit_state[qubit].zero_probability); + shot.qubit_state[qubit].heat = -1.0; + write_measure_reset_instrument(shot_idx, qubit, result, true /* resets_to_zero */); + shot.op_idx = qubit; // execute reads the lost qubit from op_idx + shot.op_type = OPID_LOSS_NOISE; +} + // Prepare correlated noise for the adaptive path. // Qubit IDs are read from call_arg_table (register indices), following the same // pattern as OP_CALL argument passing. @@ -556,18 +576,24 @@ fn prep_correlated_noise(shot_idx: u32, op_idx: u32, qubit_count: u32, arg_offse let sample = sample_correlated_noise(shot_idx, op_idx, noise_table_idx); if (sample.should_apply == 0u) { return; } - // Build bit-flip and phase-flip masks using qubit IDs from registers via call_arg_table + // Build bit-flip, phase-flip, and loss masks using qubit IDs from registers via call_arg_table var bit_flip_mask: u32 = 0u; var phase_flip_mask: u32 = 0u; + var loss_mask: u32 = 0u; for (var i: u32 = 0u; i < qubit_count; i++) { let pauli_bits = get_pauli_bits(sample.paulis_lo, sample.paulis_hi, qubit_count, i); let arg_reg = batch_data.program.call_arg_table[arg_offset + i]; let qubit_mask = 1u << read_reg(shot_idx, arg_reg); - if ((pauli_bits & 0x1u) != 0u) { bit_flip_mask |= qubit_mask; } - if ((pauli_bits & 0x2u) != 0u) { phase_flip_mask |= qubit_mask; } + if ((pauli_bits & 0x4u) != 0u) { + // Loss term (L = 4): the qubit is lost, no Pauli is applied to it. + loss_mask |= qubit_mask; + } else { + if ((pauli_bits & 0x1u) != 0u) { bit_flip_mask |= qubit_mask; } + if ((pauli_bits & 0x2u) != 0u) { phase_flip_mask |= qubit_mask; } + } } - commit_correlated_noise(shot_idx, op_idx, bit_flip_mask, phase_flip_mask); + commit_correlated_noise(shot_idx, op_idx, bit_flip_mask, phase_flip_mask, loss_mask); } @compute @workgroup_size(THREADS_PER_WORKGROUP) @@ -667,6 +693,20 @@ fn interpret_classical(@builtin(global_invocation_id) gid: vec3) { return; } + // -- Drain pending qubit losses before resuming classical execution -- + // The most recent noise op (per-gate Pauli/loss or correlated) may have + // sampled one or more qubits as lost, recorded in pending_loss_mask. Commit + // each as its own measure+reset quantum op (one per round) before running + // any more bytecode, so loss is applied with the correct correlation. + if shots[shot_idx].pending_loss_mask != 0u { + let q = firstTrailingBit(shots[shot_idx].pending_loss_mask); + shots[shot_idx].pending_loss_mask &= ~(1u << q); + shots[shot_idx].interp.pending_op_idx = q; + shots[shot_idx].interp.pending_op_type = PENDING_OP_LOSS_COMMIT; + shots[shot_idx].interp.status = STATUS_QUANTUM_PENDING; + return; + } + // If we were paused (QUANTUM_PENDING after a quantum op, or YIELD after // hitting the step limit), transition back to RUNNING so the main loop // resumes executing instructions from where it left off. @@ -1479,6 +1519,14 @@ fn prepare_op(@builtin(global_invocation_id) globalId: vec3) { let op_idx = state.pending_op_idx; let op_type = state.pending_op_type; + + // Loss commit: pending_op_idx holds the lost qubit (not an ops-pool index). + // Measure + reset that qubit; the execute stage applies it via op_idx. + if op_type == PENDING_OP_LOSS_COMMIT { + prep_loss_commit(shot_idx, op_idx); + return; + } + let op = &ops[op_idx]; // Correlated noise: qubit IDs are stored as register indices in @@ -1571,20 +1619,8 @@ fn prepare_op(@builtin(global_invocation_id) globalId: vec3) { // Check for noise ops after this gate in the ops pool let pauli_op_idx = get_pauli_noise_idx(op_idx); - let loss_op_idx = get_loss_idx(select(op_idx, pauli_op_idx, pauli_op_idx != 0u)); - - // Handle loss noise first (if qubit is lost, gate doesn't matter) - if loss_op_idx != 0u { - let loss_op = &ops[loss_op_idx]; - let p_loss = loss_op.unitary[0].x; - if shot.rand_loss < p_loss { - prep_measure_reset(shot_idx, op_idx, true, false, true); - shots[shot_idx].interp.status = STATUS_RUNNING; - return; - } - } - // Handle Pauli noise + // Handle Pauli noise (loss, if sampled, is recorded in pending_loss_mask) if pauli_op_idx != 0u { if ops[pauli_op_idx].id == OPID_PAULI_NOISE_1Q { apply_1q_pauli_noise(shot_idx, op_idx, pauli_op_idx); @@ -1630,17 +1666,6 @@ fn prepare_op(@builtin(global_invocation_id) globalId: vec3) { // Check for noise ops before the measure op // (noise is applied as Id+noise, then original measure, matching non-adaptive pattern) let pauli_op_idx = get_pauli_noise_idx(op_idx); - let loss_op_idx = get_loss_idx(select(op_idx, pauli_op_idx, pauli_op_idx != 0u)); - - if loss_op_idx != 0u { - let loss_op = &ops[loss_op_idx]; - let p_loss = loss_op.unitary[0].x; - if shot.rand_loss < p_loss { - prep_measure_reset(shot_idx, op_idx, true, false, true); - shots[shot_idx].interp.status = STATUS_RUNNING; - return; - } - } if pauli_op_idx != 0u { // Apply noise to the Id gate before measure, then the measure itself @@ -1688,6 +1713,9 @@ fn execute( // IGNORE } else if (shot.op_type == OPID_CORRELATED_NOISE) { apply_correlated_noise(workgroupId.x, tid); + } else if (shot.op_type == OPID_LOSS_NOISE) { + // Loss commit: the lost qubit is carried in op_idx (set by prep_loss_commit). + apply_1q_op(workgroupId.x, tid, shot.op_idx); } else if (is_1q_op(shot.op_type)) { let q1: u32 = resolve_q1(shot_idx_u32); apply_1q_op(workgroupId.x, tid, q1); diff --git a/source/simulators/src/gpu_full_state_simulator/simulator_base.wgsl b/source/simulators/src/gpu_full_state_simulator/simulator_base.wgsl index 54ac64d4eff..e1f966630da 100644 --- a/source/simulators/src/gpu_full_state_simulator/simulator_base.wgsl +++ b/source/simulators/src/gpu_full_state_simulator/simulator_base.wgsl @@ -33,7 +33,9 @@ struct ShotData { rand_damping: f32, rand_dephase: f32, rand_measure: f32, - rand_loss: f32, + // Bitmask of qubits the most recent noise sampler chose to lose. A following + // loss-commit op consumes (and clears) its qubit's bit. + pending_loss_mask: u32, // The type of the next operation to execute. This will be OPID_SHOT_BUFF_* if it should use the unitary from the op buffer op_type: u32, @@ -193,17 +195,23 @@ fn prep_correlated_noise(shot_idx: u32, op_idx: u32) { let sample = sample_correlated_noise(shot_idx, op_idx, noise_table_idx); if (sample.should_apply == 0u) { return; } - // Build bit-flip and phase-flip masks using qubit IDs from the op's unitary matrix + // Build bit-flip, phase-flip, and loss masks using qubit IDs from the op's unitary matrix var bit_flip_mask: u32 = 0u; var phase_flip_mask: u32 = 0u; + var loss_mask: u32 = 0u; for (var i: u32 = 0u; i < qubit_count; i++) { let pauli_bits = get_pauli_bits(sample.paulis_lo, sample.paulis_hi, qubit_count, i); let qubit_mask = 1u << get_correlated_noise_qubit(op_idx, i); - if ((pauli_bits & 0x1u) != 0u) { bit_flip_mask |= qubit_mask; } - if ((pauli_bits & 0x2u) != 0u) { phase_flip_mask |= qubit_mask; } + if ((pauli_bits & 0x4u) != 0u) { + // Loss term (L = 4): the qubit is lost, no Pauli is applied to it. + loss_mask |= qubit_mask; + } else { + if ((pauli_bits & 0x1u) != 0u) { bit_flip_mask |= qubit_mask; } + if ((pauli_bits & 0x2u) != 0u) { phase_flip_mask |= qubit_mask; } + } } - commit_correlated_noise(shot_idx, op_idx, bit_flip_mask, phase_flip_mask); + commit_correlated_noise(shot_idx, op_idx, bit_flip_mask, phase_flip_mask, loss_mask); } @@ -268,6 +276,22 @@ fn prepare_op(@builtin(global_invocation_id) globalId: vec3) { return; } + // Loss-commit op: lose this qubit if and only if the preceding noise sampler + // set its bit in pending_loss_mask; otherwise act as identity. + if (op.id == OPID_LOSS_NOISE) { + shot.next_op_idx = op_idx + 1u; + let loss_bit = 1u << op.q1; + if ((shot.pending_loss_mask & loss_bit) != 0u) { + shot.pending_loss_mask &= ~loss_bit; + prep_measure_reset(shot_idx, op_idx, true /* is_loss */, false /* stores_result */, true /* resets_to_zero */); + } else { + shot.op_type = OPID_ID; + shot.op_idx = op_idx; + shot.qubits_updated_last_op_mask = 0u; + } + return; + } + /* Handle noise: - For the 1-qubit op case, there could be pauli and loss noise after the op itself. We want to check for loss first and only apply pauli noise if the qubit wasn't lost. (If lost, the pauli noise and even the gate itself don't matter). @@ -276,8 +300,9 @@ fn prepare_op(@builtin(global_invocation_id) globalId: vec3) { */ let pauli_op_idx = get_pauli_noise_idx(op_idx); - let loss_op_idx = get_loss_idx(select(op_idx, pauli_op_idx, pauli_op_idx != 0u)); - shot.next_op_idx = max(op_idx, max(pauli_op_idx, loss_op_idx)) + 1u; + // Advance past this gate and its (optional) inline Pauli/loss noise op. Any + // loss-commit ops that follow are separate ops handled on later iterations. + shot.next_op_idx = max(op_idx, pauli_op_idx) + 1u; // Handle correlated noise operations if (op.id == OPID_CORRELATED_NOISE) { @@ -294,19 +319,6 @@ fn prepare_op(@builtin(global_invocation_id) globalId: vec3) { return; } - // If there is loss noise to apply, do that now - if (loss_op_idx != 0u) { - let loss_op = &ops[loss_op_idx]; - let p_loss = loss_op.unitary[0].x; // Loss probability is stored in the x part of first vec2 - if (shot.rand_loss < p_loss) { - // Qubit is lost - perform MResetZ with is_loss = true - // (stores_result is irrelevant here since the is_loss path never stores a result) - prep_measure_reset(shot_idx, op_idx, true /* is_loss */, false /* stores_result */, true /* resets_to_zero */); - // There is no further noise of gate to apply, just the loss execution. - return; - } - } - if pauli_op_idx != 0 { if ops[pauli_op_idx].id == OPID_PAULI_NOISE_1Q { apply_1q_pauli_noise(shot_idx, op_idx, pauli_op_idx); From 4ff3d45ff8e8a7f2ce70203d905af14dad849ecb Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Fri, 12 Jun 2026 12:32:29 -0700 Subject: [PATCH 16/67] delete commented out code --- source/simulators/src/stabilizer_simulator.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 97e0d3df70e..4131659f8a2 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -100,21 +100,6 @@ macro_rules! apply_noise { }}; } -// macro_rules! apply_noise { -// ($slf:expr, $noise_table:ident, $targets:expr) => {{ -// let fault = $slf.noise_config.$noise_table.sample_noise(&mut $slf.rng); -// if let Fault::Pauli(pauli_observables) = fault { -// let observable: Vec<_> = pauli_observables -// .into_iter() -// .zip($targets) -// .filter(|(_, q)| !$slf.loss[**q]) // We don't apply faults on lost qubits. -// .map(|(pauli, q)| (pauli, *q).into()) -// .collect(); -// $slf.state.pauli(&observable); -// }; -// }}; -// } - impl StabilizerSimulator { /// Sets the random seed of the simulator. pub fn set_seed(&mut self, seed: u64) { From 5c1cc104c5b61b1d3c24966fb9e5b9926dd55ce3 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Fri, 12 Jun 2026 12:37:54 -0700 Subject: [PATCH 17/67] fix cargo clippy warning --- source/simulators/benches/sim_mem.rs | 3 +-- source/simulators/src/cpu_full_state_simulator.rs | 4 ++-- source/simulators/src/stabilizer_simulator.rs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/source/simulators/benches/sim_mem.rs b/source/simulators/benches/sim_mem.rs index 4d5bd6f38b4..a3697949cfa 100644 --- a/source/simulators/benches/sim_mem.rs +++ b/source/simulators/benches/sim_mem.rs @@ -22,8 +22,7 @@ mod bench { fn setup(gates: Vec) -> (StabilizerSimulator, Vec) { const NUM_QUBITS: usize = 1224; const NUM_RESULTS: usize = NUM_QUBITS; - let noise: Arc> = - Arc::new(>::NOISELESS.into()); + let noise: Arc = Arc::new(>::NOISELESS.into()); let simulator = StabilizerSimulator::new(NUM_QUBITS, NUM_RESULTS, SEED, noise); (simulator, gates) } diff --git a/source/simulators/src/cpu_full_state_simulator.rs b/source/simulators/src/cpu_full_state_simulator.rs index 0790485245f..1ca4f5260c7 100644 --- a/source/simulators/src/cpu_full_state_simulator.rs +++ b/source/simulators/src/cpu_full_state_simulator.rs @@ -557,7 +557,7 @@ impl NoisySimulator { /// reference to `self` at the same time. So, the obvious way express /// this, /// ```ignore -/// fn apply_noise(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { +/// fn apply_noise(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { /// for target in targets { /// if matches!(noise_table.sample_noise(&mut self.rng), Fault::Loss) { /// ... @@ -586,7 +586,7 @@ impl NoisySimulator { /// ```ignore /// fn apply_noise( /// state: &mut StateType, -/// noise_table: &CumulativeNoiseTable, +/// noise_table: &CumulativeNoiseTable, /// targets: &[QubitID], /// rng: &mut Rng, /// loss: &mut Vec diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 4131659f8a2..de5be2a7c9f 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -44,7 +44,7 @@ pub struct StabilizerSimulator { /// reference to `self` at the same time. So, the obvious way express /// this, /// ```ignore -/// fn apply_noise(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { +/// fn apply_noise(&mut self, noise_table: &CumulativeNoiseTable, targets: &[QubitID]) { /// for target in targets { /// if matches!(noise_table.sample_noise(&mut self.rng), Fault::Loss) { /// ... @@ -73,7 +73,7 @@ pub struct StabilizerSimulator { /// ```ignore /// fn apply_noise( /// state: &mut StateType, -/// noise_table: &CumulativeNoiseTable, +/// noise_table: &CumulativeNoiseTable, /// targets: &[QubitID], /// rng: &mut Rng, /// loss: &mut Vec From c5663764c3ccef905bb4a81e0ee8947667826972 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Fri, 12 Jun 2026 12:42:22 -0700 Subject: [PATCH 18/67] fix issue in benchmark --- source/simulators/benches/sim_mem.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/source/simulators/benches/sim_mem.rs b/source/simulators/benches/sim_mem.rs index a3697949cfa..55a540aee65 100644 --- a/source/simulators/benches/sim_mem.rs +++ b/source/simulators/benches/sim_mem.rs @@ -11,7 +11,6 @@ mod bench { noise_config::{CumulativeNoiseConfig, NoiseConfig}, stabilizer_simulator::{ StabilizerSimulator, - noise::Fault, operation::{Operation, cz, h, id, mz, s, x, y, z}, }, }; From 3a30469dd64ed1eeb8dc0d87dda29251ecd768bd Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 12 Jun 2026 17:45:49 -0700 Subject: [PATCH 19/67] add qirWriter for separating responsibilities --- source/compiler/qsc_stim_parser/src/qir.rs | 315 ++++++++++++--------- 1 file changed, 175 insertions(+), 140 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index cc02e04e43f..f8dca9a32a0 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -1,12 +1,80 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - -use qsc_data_structures::target; - use crate::parser::*; +use std::collections::HashMap; use std::collections::HashSet; use std::fmt::Write; +#[derive(Clone, Copy)] +enum Operand { + /// A qubit operand, carrying the raw Stim qubit index. + Qubit(u32), + /// A result operand — the writer allocates the next result ID. + Result, +} + +struct QirWriter { + output: String, + qubit_map: HashMap, + num_results: u32, + used_intrinsics: HashSet, +} + +impl QirWriter { + fn new() -> Self { + Self { + output: String::new(), + qubit_map: HashMap::new(), + num_results: 0, + used_intrinsics: HashSet::new(), + } + } + + // Writes: ` call void @__quantum__qis__{intrinsic}__body(ptr inttoptr (i64 N to ptr), ...)` + // Resolves qubit indices via the qubit map and allocates result IDs internally. + fn write_call(&mut self, intrinsic: &str, operands: &[Operand]) { + write!( + self.output, + " call void @__quantum__qis__{intrinsic}__body(" + ) + .unwrap(); + for (i, &operand) in operands.iter().enumerate() { + if i > 0 { + write!(self.output, ", ").unwrap(); + } + self.write_operand(operand); + } + writeln!(self.output, ")").unwrap(); + self.used_intrinsics.insert(intrinsic.to_string()); + } + + // Resolves an Operand to its QIR ID and writes: `ptr inttoptr (i64 N to ptr)` + fn write_operand(&mut self, operand: Operand) { + let id = match operand { + Operand::Qubit(stim_index) => self.map_qubit(stim_index), + Operand::Result => self.next_result(), + }; + write!(self.output, "ptr inttoptr (i64 {id} to ptr)").unwrap(); + } + + fn write_header(&mut self) {} + + fn write_footer(&mut self) {} + + // Maps a Stim qubit index to a dense 0-based QIR qubit ID. + fn map_qubit(&mut self, stim_index: u32) -> u32 { + let next_id = self.qubit_map.len() as u32; + *self.qubit_map.entry(stim_index).or_insert(next_id) + } + + // Allocates the next result ID. + fn next_result(&mut self) -> u32 { + let id = self.num_results; + self.num_results += 1; + id + } +} + enum InstructionKind { PauliGate, SingleQubitCliffordGate, @@ -20,245 +88,216 @@ enum InstructionKind { CustomInstruction, } -struct Emitter { - qir_output: String, - num_qubits: u32, - num_results: u32, +struct Compiler { + writer: QirWriter, last_preselect_begin: Option, num_preselect_expects: u32, - used_intrinsics: HashSet, - labels: Vec, } -impl Emitter { +impl Compiler { fn new() -> Self { Self { - qir_output: String::new(), - num_qubits: 0, - num_results: 0, + writer: QirWriter::new(), last_preselect_begin: None, num_preselect_expects: 0, - used_intrinsics: HashSet::new(), - labels: Vec::new(), } } - fn emit_circuit(&mut self, circuit: &Circuit) { - let items = &circuit.items; - for item in items { - self.emit_item(&item); + fn compile_circuit(&mut self, circuit: &Circuit) { + for item in &circuit.items { + self.compile_item(item); } } - fn emit_item(&mut self, item: &Item) { + fn compile_item(&mut self, item: &Item) { match item { - Item::Block(block) => self.emit_block(block), - Item::Line(line) => self.emit_line(line), + Item::Block(block) => self.compile_block(block), + Item::Line(line) => self.compile_line(line), } } - fn emit_block(&mut self, block: &Block) { + fn compile_block(&mut self, block: &Block) { let Block { block_instruction, items, .. } = block; - self.emit_instruction(block_instruction); + self.compile_instruction(block_instruction); for item in items { - self.emit_item(item); + self.compile_item(item); } } - fn emit_line(&mut self, line: &Line) { + fn compile_line(&mut self, line: &Line) { let Line { instruction, .. } = line; - self.emit_instruction(instruction); + self.compile_instruction(instruction); } - fn emit_instruction(&mut self, instruction: &Instruction) { - match self.instruction_kind(instruction.name) { + fn compile_instruction(&mut self, instruction: &Instruction) { + match Self::instruction_kind(&instruction.name) { InstructionKind::PauliGate => { - self.emit_pauli_gate(instruction); + self.compile_pauli_gate(instruction); } InstructionKind::SingleQubitCliffordGate => { - self.emit_single_qubit_clifford_gate(instruction); + self.compile_single_qubit_clifford_gate(instruction); } InstructionKind::TwoQubitCliffordGate => { - self.emit_two_qubit_clifford_gate(instruction); + self.compile_two_qubit_clifford_gate(instruction); } InstructionKind::NoiseChannel => { - self.emit_noise_channel(instruction); + self.compile_noise_channel(instruction); } InstructionKind::CollapsingGate => { - self.emit_collapsing_gate(instruction); + self.compile_collapsing_gate(instruction); } InstructionKind::PairMeasurementGate => { - self.emit_pair_measurement_gate(instruction); + self.compile_pair_measurement_gate(instruction); } InstructionKind::GeneralizedPauliProductGate => { - self.emit_generalized_pauli_product_gate(instruction); + self.compile_generalized_pauli_product_gate(instruction); } InstructionKind::ControlFlow => { - self.emit_control_flow(instruction); + self.compile_control_flow(instruction); } InstructionKind::Annotations => { - self.emit_annotations(instruction); + self.compile_annotations(instruction); } InstructionKind::CustomInstruction => { - self.emit_custom_instruction(instruction); + self.compile_custom_instruction(instruction); } } } - fn emit_pauli_gate(&mut self, instruction: &Instruction) { + fn compile_pauli_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); for target in &instruction.targets { - write!(self.qir_output, "__quantum__qis__{}__body", gate).unwrap(); - self.emit_target(&target); + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer.write_call(&gate, &[Operand::Qubit(value)]); } } - fn emit_single_qubit_clifford_gate(&mut self, instruction: &Instruction) { + fn compile_single_qubit_clifford_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); if gate == "h" || gate == "s" { for target in &instruction.targets { - write!(self.qir_output, "__quantum__qis__{}__body", gate).unwrap(); - self.emit_target(&target); + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer.write_call(&gate, &[Operand::Qubit(value)]); } } else if gate == "sqrt_x" { // decomposed into H S H for target in &instruction.targets { - write!(self.qir_output, "__quantum__qis__h__body").unwrap(); - self.emit_target(&target); - write!(self.qir_output, "__quantum__qis__s__body").unwrap(); - self.emit_target(&target); - write!(self.qir_output, "__quantum__qis__h__body").unwrap(); - self.emit_target(&target); + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + let q = Operand::Qubit(value); + self.writer.write_call("h", &[q]); + self.writer.write_call("s", &[q]); + self.writer.write_call("h", &[q]); } } } - fn emit_two_qubit_clifford_gate(&mut self, instruction: &Instruction) { + fn compile_two_qubit_clifford_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); if gate == "cz" { let targets = &instruction.targets; for pair in targets.chunks(2) { - let [control, target] = pair else { - unreachable!() + let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { + continue; }; - write!(self.qir_output, "__quantum__qis__cz__body").unwrap(); - self.emit_target(&control); - self.emit_target(&target); + let TargetKind::Qubit { value: v1, .. } = pair[1].kind else { + continue; + }; + self.writer + .write_call(&gate, &[Operand::Qubit(v0), Operand::Qubit(v1)]); } } } - fn emit_noise_channel(&mut self, instruction: &Instruction) {} + fn compile_noise_channel(&mut self, instruction: &Instruction) {} - fn emit_collapsing_gate(&mut self, instruction: &Instruction) { + fn compile_collapsing_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); if gate == "r" { for target in &instruction.targets { - write!(self.qir_output, "__quantum__qis__reset__body").unwrap(); - self.emit_target(&target); + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer.write_call("reset", &[Operand::Qubit(value)]); } } else if gate == "mr" { for target in &instruction.targets { - write!(self.qir_output, "__quantum__qis__mresetz__body").unwrap(); - self.emit_target(&target); + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer + .write_call("mresetz", &[Operand::Qubit(value), Operand::Result]); } } else if gate == "mrx" { // decomposed into H MRZ H for target in &instruction.targets { - write!(self.qir_output, "__quantum__qis__h__body").unwrap(); - self.emit_target(&target); - write!(self.qir_output, "__quantum__qis__mresetz__body").unwrap(); - self.emit_target(&target); - write!(self.qir_output, "__quantum__qis__h__body").unwrap(); - self.emit_target(&target); + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + let q = Operand::Qubit(value); + self.writer.write_call("h", &[q]); + self.writer.write_call("mresetz", &[q, Operand::Result]); + self.writer.write_call("h", &[q]); } } } - fn emit_pair_measurement_gate(&mut self, instruction: &Instruction) {} + fn compile_pair_measurement_gate(&mut self, instruction: &Instruction) {} - fn emit_generalized_pauli_product_gate(&mut self, instruction: &Instruction) {} + fn compile_generalized_pauli_product_gate(&mut self, instruction: &Instruction) {} - fn emit_control_flow(&mut self, instruction: &Instruction) {} + fn compile_control_flow(&mut self, instruction: &Instruction) {} - fn emit_annotations(&mut self, instruction: &Instruction) {} + fn compile_annotations(&mut self, instruction: &Instruction) {} - fn emit_custom_instruction(&mut self, instruction: &Instruction) { - let instructionName = instruction.name.to_lowercase(); - if instructionName == "#!preselect_begin" { + fn compile_custom_instruction(&mut self, instruction: &Instruction) { + let instruction_name = instruction.name.to_lowercase(); + if instruction_name == "#!preselect_begin" { self.last_preselect_begin = match self.last_preselect_begin { None => Some(0), Some(n) => Some(n + 1), }; - write!( - self.qir_output, + writeln!( + self.writer.output, "preselect_begin_{}:", - self.last_preselect_begin.unwrap() // It shouldn't be none! + self.last_preselect_begin.unwrap() + ) + .unwrap(); + } else if instruction_name == "#!preselect_expect" { + write!( + self.writer.output, + "preselect_r{}", + self.num_preselect_expects ) .unwrap(); - } else if instructionName == "#!preselect_expect" { - write!(self.qir_output, "preselect_r{}", self.num_preselect_expects).unwrap(); self.num_preselect_expects += 1; - write!(self.qir_output, " ").unwrap(); // whitespace write!( - self.qir_output, - "= call i1 @__quantum__qis__read_result__body" + self.writer.output, + " = call i1 @__quantum__qis__read_result__body(" ) .unwrap(); - self.emit_target(&instruction.targets[0]); + let TargetKind::Qubit { value, .. } = instruction.targets[0].kind else { + return; + }; + // read_result takes a result operand + let result_id = self.writer.map_qubit(value); + write!(self.writer.output, "ptr inttoptr (i64 {result_id} to ptr)").unwrap(); + writeln!(self.writer.output, ")").unwrap(); // EMIT BREAK, br i1 %preselect_r1, label %preselect_fail_1, label %continue_1 - // HAVE IT THE OTHER WAY AROUDN IF TARGETS[1] IS 1 + // HAVE IT THE OTHER WAY AROUND IF TARGETS[1] IS 1 } } - fn emit_targets(&mut self, targets: &[Target]) { - write!(self.qir_output, "(").unwrap(); - for target in targets { - self.emit_target(target); - write!(self.qir_output, ", ").unwrap(); - } - self.qir_output.pop(); // remove trailing whitespace - self.qir_output.pop(); // remove trailing comma - write!(self.qir_output, ")\n").unwrap(); - } - - // fn emit_target(&mut self, target: &Target) { - // match target.kind { - // TargetKind::Qubit { negated, value } => { - // if negated { - // write!(self.qir_output, "!q{}", value).unwrap(); - // } else { - // write!(self.qir_output, "q{}", value).unwrap(); - // } - // } - // TargetKind::MeasurementRecord { value } => { - // write!(self.qir_output, "m{}", value).unwrap(); - // } - // TargetKind::SweepBit { value } => { - // write!(self.qir_output, "s{}", value).unwrap(); - // } - // TargetKind::Pauli { negated, pauli, value } => { - // if negated { - // write!(self.qir_output, "!").unwrap(); - // } - // match pauli { - // Pauli::X => write!(self.qir_output, "X").unwrap(), - // Pauli::Y => write!(self.qir_output, "Y").unwrap(), - // Pauli::Z => write!(self.qir_output, "Z").unwrap(), - // } - // write!(self.qir_output, "{}", value).unwrap(); - // } - // TargetKind::Combiner => { - // write!(self.qir_output, "combiner").unwrap(); - // } - // } - // } - fn instruction_kind(name: &str) -> InstructionKind { match name { // Pauli Gates @@ -316,18 +355,14 @@ impl Emitter { } } - fn append_header(&mut self) {} - - fn append_footer(&mut self) {} - - fn into_qir(&mut self, circuit: &Circuit) -> String { - self.emit_circuit(circuit); - self.append_header(); - self.append_footer(); - self.qir_output + fn into_qir(mut self, circuit: &Circuit) -> String { + self.compile_circuit(circuit); + self.writer.write_header(); + self.writer.write_footer(); + self.writer.output } } -pub fn emit_qir(circuit: &Circuit) -> String { - Emitter::new().into_qir(circuit) +pub fn compile_to_qir(circuit: &Circuit) -> String { + Compiler::new().into_qir(circuit) } From ff29da3ae981d543caf00b3a9a2d0cf9b7a468b8 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 12 Jun 2026 17:55:44 -0700 Subject: [PATCH 20/67] file for manual testing e2e compilation --- .../qsc_stim_parser/examples/compile_stim.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 source/compiler/qsc_stim_parser/examples/compile_stim.rs diff --git a/source/compiler/qsc_stim_parser/examples/compile_stim.rs b/source/compiler/qsc_stim_parser/examples/compile_stim.rs new file mode 100644 index 00000000000..3c070abd6e5 --- /dev/null +++ b/source/compiler/qsc_stim_parser/examples/compile_stim.rs @@ -0,0 +1,15 @@ +use qsc_stim_parser::parser::parse; +use qsc_stim_parser::qir::compile_to_qir; +use std::fs; + +fn main() { + let stim_code = + fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); + + let circuit = parse(&stim_code); + let qir = compile_to_qir(&circuit); + + fs::write("examples/example.qir", &qir).expect("Failed to write examples/example.qir"); + + println!("Wrote examples/example.qir"); +} From 8bcb3e7892acae6dad6124a134144d59f7d36d55 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 12 Jun 2026 19:00:26 -0700 Subject: [PATCH 21/67] output header, footer, and declarations --- source/compiler/qsc_stim_parser/src/qir.rs | 111 +++++++++++++++++++-- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index f8dca9a32a0..057aef471a7 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -2,7 +2,6 @@ // Licensed under the MIT License. use crate::parser::*; use std::collections::HashMap; -use std::collections::HashSet; use std::fmt::Write; #[derive(Clone, Copy)] @@ -17,7 +16,7 @@ struct QirWriter { output: String, qubit_map: HashMap, num_results: u32, - used_intrinsics: HashSet, + used_intrinsics: HashMap, } impl QirWriter { @@ -26,7 +25,7 @@ impl QirWriter { output: String::new(), qubit_map: HashMap::new(), num_results: 0, - used_intrinsics: HashSet::new(), + used_intrinsics: HashMap::new(), } } @@ -45,7 +44,8 @@ impl QirWriter { self.write_operand(operand); } writeln!(self.output, ")").unwrap(); - self.used_intrinsics.insert(intrinsic.to_string()); + self.used_intrinsics + .insert(intrinsic.to_string(), operands.len()); } // Resolves an Operand to its QIR ID and writes: `ptr inttoptr (i64 N to ptr)` @@ -57,9 +57,106 @@ impl QirWriter { write!(self.output, "ptr inttoptr (i64 {id} to ptr)").unwrap(); } - fn write_header(&mut self) {} + fn write_header(&mut self) { + writeln!(self.output, "define i64 @ENTRYPOINT__main() #0 {{").unwrap(); + writeln!( + self.output, + " call void @__quantum__rt__initialize(ptr null)" + ) + .unwrap(); + } + + fn write_record_output(&mut self) { + let num_results = self.num_results; + writeln!( + self.output, + " call void @__quantum__rt__array_record_output(i64 {num_results}, ptr null)" + ) + .unwrap(); + for i in 0..num_results { + writeln!( + self.output, + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 {i} to ptr), ptr null)" + ) + .unwrap(); + } + } + + fn write_declarations(&mut self) { + writeln!(self.output).unwrap(); + writeln!(self.output, "declare void @__quantum__rt__initialize(ptr)").unwrap(); + writeln!(self.output, "declare void @__quantum__rt__array_record_output(i64, ptr)").unwrap(); + writeln!(self.output, "declare void @__quantum__rt__result_record_output(ptr, ptr)").unwrap(); + for (intrinsic, arity) in &self.used_intrinsics { + let params = (0..*arity).map(|_| "ptr").collect::>().join(", "); + writeln!( + self.output, + "declare void @__quantum__qis__{intrinsic}__body({params})" + ) + .unwrap(); + } + } + + fn write_footer(&mut self) { + self.write_record_output(); + writeln!(self.output, " ret i64 0").unwrap(); + writeln!(self.output, "}}").unwrap(); + self.write_declarations(); - fn write_footer(&mut self) {} + let num_qubits = self.qubit_map.len(); + let num_results = self.num_results; + writeln!(self.output).unwrap(); + writeln!( + self.output, + "attributes #0 = {{ \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"{num_qubits}\" \"required_num_results\"=\"{num_results}\" }}" + ).unwrap(); + writeln!(self.output, "attributes #1 = {{ \"irreversible\" }}").unwrap(); + writeln!(self.output).unwrap(); + writeln!(self.output, "; module flags").unwrap(); + writeln!(self.output).unwrap(); + writeln!( + self.output, + "!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}" + ) + .unwrap(); + writeln!(self.output).unwrap(); + writeln!( + self.output, + "!0 = !{{i32 1, !\"qir_major_version\", i32 2}}" + ) + .unwrap(); + writeln!( + self.output, + "!1 = !{{i32 7, !\"qir_minor_version\", i32 1}}" + ) + .unwrap(); + writeln!( + self.output, + "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}" + ) + .unwrap(); + writeln!( + self.output, + "!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}" + ) + .unwrap(); + writeln!( + self.output, + "!4 = !{{i32 5, !\"int_computations\", !{{!\"i64\"}}}}" + ) + .unwrap(); + writeln!( + self.output, + "!5 = !{{i32 5, !\"float_computations\", !{{!\"double\"}}}}" + ) + .unwrap(); + writeln!( + self.output, + "!6 = !{{i32 7, !\"backwards_branching\", i2 3}}" + ) + .unwrap(); + writeln!(self.output, "!7 = !{{i32 1, !\"arrays\", i1 true}}").unwrap(); + } // Maps a Stim qubit index to a dense 0-based QIR qubit ID. fn map_qubit(&mut self, stim_index: u32) -> u32 { @@ -356,8 +453,8 @@ impl Compiler { } fn into_qir(mut self, circuit: &Circuit) -> String { - self.compile_circuit(circuit); self.writer.write_header(); + self.compile_circuit(circuit); self.writer.write_footer(); self.writer.output } From fa9973dab70b9272efe3d4e21c1fc4b4a8889b89 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Sat, 13 Jun 2026 20:26:37 -0700 Subject: [PATCH 22/67] fix clippy warnings --- .../qsc_stim_parser/examples/lex_stim.rs | 2 +- source/compiler/qsc_stim_parser/src/lex.rs | 2 +- source/compiler/qsc_stim_parser/src/parser.rs | 44 ++++++++++--------- source/compiler/qsc_stim_parser/src/qir.rs | 32 +++++++++----- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/source/compiler/qsc_stim_parser/examples/lex_stim.rs b/source/compiler/qsc_stim_parser/examples/lex_stim.rs index ef1371cc151..ddbcf8f3a67 100644 --- a/source/compiler/qsc_stim_parser/examples/lex_stim.rs +++ b/source/compiler/qsc_stim_parser/examples/lex_stim.rs @@ -9,7 +9,7 @@ fn main() { let mut out = fs::File::create("examples/lex_output.txt").expect("Failed to create output file"); - writeln!(out, "{:<20} {:<10} {:}", "TOKEN KIND", "SPAN", "TEXT").unwrap(); + writeln!(out, "{:<20} {:<10} TEXT", "TOKEN KIND", "SPAN").unwrap(); writeln!(out, "{:-<50}", "").unwrap(); let lexer = Lexer::new(&stim_code); diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index cd25f25a4f6..0d752d4caf0 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -82,7 +82,7 @@ impl<'a> Lexer<'a> { } } - fn eat_while(&mut self, mut f: impl Fn(char) -> bool) { + fn eat_while(&mut self, f: impl Fn(char) -> bool) { while self.chars.next_if(|i| f(i.1)).is_some() {} } diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index f1788c1a0b6..71335fc2c5b 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -144,17 +144,25 @@ impl<'a> Parser<'a> { } fn parse_item(&mut self) -> Option { - // TODO WHAT IF IT STARTS WITH A NEWLINE? + // Skip any leading newlines + while self + .tokens + .peek() + .is_some_and(|t| t.kind == TokenKind::Newline) + { + self.tokens.next(); + } if let TokenKind::InstructionName = self.tokens.peek()?.kind { // Could be the start of a block or of a line let instruction = self.parse_instruction(); - if let Some(token) = self.tokens.peek() { - if token.kind == TokenKind::Open(Brace) { - return Some(Item::Block(self.parse_block(instruction))); - } + if let Some(token) = self.tokens.peek() + && token.kind == TokenKind::Open(Brace) + { + return Some(Item::Block(self.parse_block(instruction))); } - return Some(Item::Line(self.parse_line(instruction))); + + Some(Item::Line(self.parse_line(instruction))) } else { // TODO error! The start of every item should be an instruction; None @@ -203,21 +211,15 @@ impl<'a> Parser<'a> { let name = self.extract_string(name_token, None); let tag_token = self.tokens.next_if(|t| t.kind == TokenKind::Tag); - let tag: Option; - match tag_token { - Some(tag_token) => { - tag = Some(self.extract_string( - tag_token, - Some(Span { - lo: tag_token.span.lo + 1, - hi: tag_token.span.hi - 1, - }), - )); // Remove the surrounding brackets - } - None => { - tag = None; - } - } + let tag: Option = tag_token.map(|tag_token| { + self.extract_string( + tag_token, + Some(Span { + lo: tag_token.span.lo + 1, + hi: tag_token.span.hi - 1, + }), + ) + }); let mut args = Vec::new(); let mut targets = Vec::new(); diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 057aef471a7..1c6d9cc8cf5 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use crate::parser::*; -use std::collections::HashMap; +use rustc_hash::FxHashMap; use std::fmt::Write; #[derive(Clone, Copy)] @@ -14,18 +14,18 @@ enum Operand { struct QirWriter { output: String, - qubit_map: HashMap, + qubit_map: FxHashMap, num_results: u32, - used_intrinsics: HashMap, + used_intrinsics: FxHashMap, } impl QirWriter { fn new() -> Self { Self { output: String::new(), - qubit_map: HashMap::new(), + qubit_map: FxHashMap::default(), num_results: 0, - used_intrinsics: HashMap::new(), + used_intrinsics: FxHashMap::default(), } } @@ -85,8 +85,16 @@ impl QirWriter { fn write_declarations(&mut self) { writeln!(self.output).unwrap(); writeln!(self.output, "declare void @__quantum__rt__initialize(ptr)").unwrap(); - writeln!(self.output, "declare void @__quantum__rt__array_record_output(i64, ptr)").unwrap(); - writeln!(self.output, "declare void @__quantum__rt__result_record_output(ptr, ptr)").unwrap(); + writeln!( + self.output, + "declare void @__quantum__rt__array_record_output(i64, ptr)" + ) + .unwrap(); + writeln!( + self.output, + "declare void @__quantum__rt__result_record_output(ptr, ptr)" + ) + .unwrap(); for (intrinsic, arity) in &self.used_intrinsics { let params = (0..*arity).map(|_| "ptr").collect::>().join(", "); writeln!( @@ -316,7 +324,7 @@ impl Compiler { } } - fn compile_noise_channel(&mut self, instruction: &Instruction) {} + fn compile_noise_channel(&mut self, _instruction: &Instruction) {} fn compile_collapsing_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); @@ -349,13 +357,13 @@ impl Compiler { } } - fn compile_pair_measurement_gate(&mut self, instruction: &Instruction) {} + fn compile_pair_measurement_gate(&mut self, _instruction: &Instruction) {} - fn compile_generalized_pauli_product_gate(&mut self, instruction: &Instruction) {} + fn compile_generalized_pauli_product_gate(&mut self, _instruction: &Instruction) {} - fn compile_control_flow(&mut self, instruction: &Instruction) {} + fn compile_control_flow(&mut self, _instruction: &Instruction) {} - fn compile_annotations(&mut self, instruction: &Instruction) {} + fn compile_annotations(&mut self, _instruction: &Instruction) {} fn compile_custom_instruction(&mut self, instruction: &Instruction) { let instruction_name = instruction.name.to_lowercase(); From c0bdb22deb31c8bf96e3a045403dad85629a48f8 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Sat, 13 Jun 2026 20:29:24 -0700 Subject: [PATCH 23/67] add run_qir api to python --- source/qdk_package/Cargo.toml | 1 + source/qdk_package/qdk/_native.pyi | 10 ++++++++ source/qdk_package/qdk/simulation/__init__.py | 2 +- .../qdk_package/qdk/simulation/_simulation.py | 12 ++++++++++ source/qdk_package/src/interop.rs | 24 ++++++++++++------- source/qdk_package/src/interpreter.rs | 5 ++-- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/source/qdk_package/Cargo.toml b/source/qdk_package/Cargo.toml index 9a36669c4c2..97b0658b3e1 100644 --- a/source/qdk_package/Cargo.toml +++ b/source/qdk_package/Cargo.toml @@ -16,6 +16,7 @@ num-bigint = { workspace = true } num-complex = { workspace = true } num-traits = { workspace = true } qsc = { path = "../compiler/qsc" } +qsc_stim_parser = { path = "../compiler/qsc_stim_parser" } qdk_simulators = { path = "../simulators" } resource_estimator = { path = "../resource_estimator" } qre = { path = "../qre" } diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index d1aab18ad8e..2afa53d30a3 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -629,6 +629,16 @@ def compile_qasm_to_qsharp( """ ... +def compile_stim_to_qir(source: str) -> str: + """ + Converts a Stim program to QIR. + + :param source: The Stim source code to convert. + :return: The converted QIR code as a string. + :rtype: str + """ + ... + def resource_estimate_qasm_program( source: str, job_params: str, diff --git a/source/qdk_package/qdk/simulation/__init__.py b/source/qdk_package/qdk/simulation/__init__.py index 8e5701332c4..62515d0c4d0 100644 --- a/source/qdk_package/qdk/simulation/__init__.py +++ b/source/qdk_package/qdk/simulation/__init__.py @@ -26,7 +26,7 @@ """ from .._device._atom import NeutralAtomDevice -from ._simulation import NoiseConfig, run_qir +from ._simulation import NoiseConfig, run_qir, run_stim from ._noisy_simulator import ( NoisySimulatorError, DensityMatrixSimulator, diff --git a/source/qdk_package/qdk/simulation/_simulation.py b/source/qdk_package/qdk/simulation/_simulation.py index 4aa6bf367d7..e90b5afc4c9 100644 --- a/source/qdk_package/qdk/simulation/_simulation.py +++ b/source/qdk_package/qdk/simulation/_simulation.py @@ -18,6 +18,7 @@ GpuContext, try_create_gpu_adapter, Result, + compile_stim_to_qir, ) from pyqir import ( Function, @@ -743,3 +744,14 @@ def run_qir( return run_qir_gpu(input, shots, noise, seed) case _: raise ValueError(f"Invalid simulator type: {type}") + + +def run_stim( + src: str, + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, + type: Optional[Literal["clifford", "cpu", "gpu"]] = None, +) -> List: + qir = compile_stim_to_qir(src) + return run_qir(qir, shots, noise, seed, type) diff --git a/source/qdk_package/src/interop.rs b/source/qdk_package/src/interop.rs index 3d353210c3a..f3c555705f9 100644 --- a/source/qdk_package/src/interop.rs +++ b/source/qdk_package/src/interop.rs @@ -11,6 +11,13 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; +use crate::fs::file_system; +use crate::interpreter::data_interop::value_to_pyobj; +use crate::interpreter::{ + CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError, + TargetProfile, format_error, format_errors, +}; +use crate::qir_simulation::{NoiseConfig, unbind_noise_config}; use pyo3::IntoPyObjectExt; use pyo3::exceptions::PyException; use pyo3::prelude::*; @@ -28,14 +35,8 @@ use qsc::{Backend, CliffordSim, PackageType, PauliNoise, SparseSim}; use qsc::{ LanguageFeatures, SourceMap, ast::Package, error::WithSource, interpret, project::FileSystem, }; - -use crate::fs::file_system; -use crate::interpreter::data_interop::value_to_pyobj; -use crate::interpreter::{ - CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError, - TargetProfile, format_error, format_errors, -}; -use crate::qir_simulation::{NoiseConfig, unbind_noise_config}; +use qsc_stim_parser::parser::parse; +use qsc_stim_parser::qir::compile_to_qir; use resource_estimator as re; @@ -463,6 +464,13 @@ pub(crate) fn compile_qasm_to_qsharp( Ok(qsharp) } +#[pyfunction] +#[pyo3(signature = (source))] +pub(crate) fn compile_stim_to_qir(source: &str) -> PyResult { + let circuit = parse(source); + Ok(compile_to_qir(&circuit)) +} + /// Enriches the compilation errors to provide more helpful messages /// as we know that we are compiling the entry expression. pub(crate) fn map_entry_compilation_errors( diff --git a/source/qdk_package/src/interpreter.rs b/source/qdk_package/src/interpreter.rs index 4cc65de9c85..877f47a313e 100644 --- a/source/qdk_package/src/interpreter.rs +++ b/source/qdk_package/src/interpreter.rs @@ -14,8 +14,8 @@ use crate::{ generic_estimator::register_generic_estimator_submodule, interop::{ circuit_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp, - create_filesystem_from_py, get_operation_name, get_output_semantics, get_program_type, - get_search_path, resource_estimate_qasm_program, run_qasm_program, + compile_stim_to_qir, create_filesystem_from_py, get_operation_name, get_output_semantics, + get_program_type, get_search_path, resource_estimate_qasm_program, run_qasm_program, }, interpreter::data_interop::{ PrimitiveKind, TypeIR, TypeKind, UdtFields, UdtIR, UdtValue, collect_udt_fields, @@ -152,6 +152,7 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(circuit_qasm_program, m)?)?; m.add_function(wrap_pyfunction!(compile_qasm_program_to_qir, m)?)?; m.add_function(wrap_pyfunction!(compile_qasm_to_qsharp, m)?)?; + m.add_function(wrap_pyfunction!(compile_stim_to_qir, m)?)?; Ok(()) } From c0d7155f7e6e288a15262aad538cae96c099f0b1 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Sat, 13 Jun 2026 20:30:31 -0700 Subject: [PATCH 24/67] use FxHashMap from rustc_hash --- Cargo.lock | 2 ++ source/compiler/qsc_stim_parser/Cargo.toml | 1 + 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e7cea171726..d67ec0e1441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,6 +2106,7 @@ dependencies = [ "qdk_simulators", "qre", "qsc", + "qsc_stim_parser", "rand 0.10.1", "rayon", "resource_estimator", @@ -2524,6 +2525,7 @@ version = "0.0.0" dependencies = [ "enum-iterator", "qsc_data_structures", + "rustc-hash", ] [[package]] diff --git a/source/compiler/qsc_stim_parser/Cargo.toml b/source/compiler/qsc_stim_parser/Cargo.toml index 6f3e59d207c..630e7a206c3 100644 --- a/source/compiler/qsc_stim_parser/Cargo.toml +++ b/source/compiler/qsc_stim_parser/Cargo.toml @@ -6,5 +6,6 @@ version.workspace = true [dependencies] enum-iterator.workspace = true qsc_data_structures = { path = "../qsc_data_structures" } +rustc-hash = { workspace = true } [dev-dependencies] From fc18cc36fceb28438976110616dfee7b8b01bd85 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Sat, 13 Jun 2026 21:31:10 -0700 Subject: [PATCH 25/67] finish implementing preselect --- source/compiler/qsc_stim_parser/src/qir.rs | 141 ++++++++++++++------- 1 file changed, 96 insertions(+), 45 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 1c6d9cc8cf5..c0606070f62 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -16,7 +16,7 @@ struct QirWriter { output: String, qubit_map: FxHashMap, num_results: u32, - used_intrinsics: FxHashMap, + used_intrinsics: FxHashMap, } impl QirWriter { @@ -44,8 +44,14 @@ impl QirWriter { self.write_operand(operand); } writeln!(self.output, ")").unwrap(); + let name = format!("__quantum__qis__{intrinsic}__body"); + let params = (0..operands.len()) + .map(|_| "ptr") + .collect::>() + .join(", "); self.used_intrinsics - .insert(intrinsic.to_string(), operands.len()); + .entry(name.clone()) + .or_insert_with(|| format!("declare void @{name}({params})")); } // Resolves an Operand to its QIR ID and writes: `ptr inttoptr (i64 N to ptr)` @@ -57,6 +63,39 @@ impl QirWriter { write!(self.output, "ptr inttoptr (i64 {id} to ptr)").unwrap(); } + // Writes a label: `{name}:` + fn write_label(&mut self, name: &str) { + writeln!(self.output, "{name}:").unwrap(); + } + + // Writes: ` br i1 %{cond}, label %{true_label}, label %{false_label}` + fn write_branch(&mut self, cond: &str, true_label: &str, false_label: &str) { + writeln!( + self.output, + " br i1 %{cond}, label %{true_label}, label %{false_label}" + ) + .unwrap(); + } + + // Writes: ` br label %{label}` + fn write_jump(&mut self, label: &str) { + writeln!(self.output, " br label %{label}").unwrap(); + } + + // Writes: ` %{dest} = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 N to ptr))` + fn write_read_result(&mut self, dest: &str, operand: Operand) { + write!( + self.output, + " %{dest} = call i1 @__quantum__rt__read_result(" + ) + .unwrap(); + self.write_operand(operand); + writeln!(self.output, ")").unwrap(); + self.used_intrinsics + .entry("__quantum__rt__read_result".to_string()) + .or_insert_with(|| "declare i1 @__quantum__rt__read_result(ptr)".to_string()); + } + fn write_header(&mut self) { writeln!(self.output, "define i64 @ENTRYPOINT__main() #0 {{").unwrap(); writeln!( @@ -64,6 +103,9 @@ impl QirWriter { " call void @__quantum__rt__initialize(ptr null)" ) .unwrap(); + self.used_intrinsics + .entry("__quantum__rt__initialize".to_string()) + .or_insert_with(|| "declare void @__quantum__rt__initialize(ptr)".to_string()); } fn write_record_output(&mut self) { @@ -73,6 +115,11 @@ impl QirWriter { " call void @__quantum__rt__array_record_output(i64 {num_results}, ptr null)" ) .unwrap(); + self.used_intrinsics + .entry("__quantum__rt__array_record_output".to_string()) + .or_insert_with(|| { + "declare void @__quantum__rt__array_record_output(i64, ptr)".to_string() + }); for i in 0..num_results { writeln!( self.output, @@ -80,28 +127,17 @@ impl QirWriter { ) .unwrap(); } + self.used_intrinsics + .entry("__quantum__rt__result_record_output".to_string()) + .or_insert_with(|| { + "declare void @__quantum__rt__result_record_output(ptr, ptr)".to_string() + }); } fn write_declarations(&mut self) { writeln!(self.output).unwrap(); - writeln!(self.output, "declare void @__quantum__rt__initialize(ptr)").unwrap(); - writeln!( - self.output, - "declare void @__quantum__rt__array_record_output(i64, ptr)" - ) - .unwrap(); - writeln!( - self.output, - "declare void @__quantum__rt__result_record_output(ptr, ptr)" - ) - .unwrap(); - for (intrinsic, arity) in &self.used_intrinsics { - let params = (0..*arity).map(|_| "ptr").collect::>().join(", "); - writeln!( - self.output, - "declare void @__quantum__qis__{intrinsic}__body({params})" - ) - .unwrap(); + for decl in self.used_intrinsics.values() { + writeln!(self.output, "{decl}").unwrap(); } } @@ -166,7 +202,7 @@ impl QirWriter { writeln!(self.output, "!7 = !{{i32 1, !\"arrays\", i1 true}}").unwrap(); } - // Maps a Stim qubit index to a dense 0-based QIR qubit ID. + // Maps a Stim qubit index to a 0-based QIR qubit ID. fn map_qubit(&mut self, stim_index: u32) -> u32 { let next_id = self.qubit_map.len() as u32; *self.qubit_map.entry(stim_index).or_insert(next_id) @@ -372,34 +408,49 @@ impl Compiler { None => Some(0), Some(n) => Some(n + 1), }; - writeln!( - self.writer.output, - "preselect_begin_{}:", - self.last_preselect_begin.unwrap() - ) - .unwrap(); + let id = self.last_preselect_begin.unwrap(); + let label = format!("preselect_begin_{id}"); + self.writer.write_jump(&label); // terminate the previous block + self.writer.write_label(&label); // start the new block } else if instruction_name == "#!preselect_expect" { - write!( - self.writer.output, - "preselect_r{}", - self.num_preselect_expects - ) - .unwrap(); + let id = self.last_preselect_begin.unwrap(); + let reg = format!("preselect_r{}", self.num_preselect_expects); self.num_preselect_expects += 1; - write!( - self.writer.output, - " = call i1 @__quantum__qis__read_result__body(" - ) - .unwrap(); - let TargetKind::Qubit { value, .. } = instruction.targets[0].kind else { + + // First target: which result to read + let TargetKind::Qubit { + value: result_id, .. + } = instruction.targets[0].kind + else { return; }; - // read_result takes a result operand - let result_id = self.writer.map_qubit(value); - write!(self.writer.output, "ptr inttoptr (i64 {result_id} to ptr)").unwrap(); - writeln!(self.writer.output, ")").unwrap(); - // EMIT BREAK, br i1 %preselect_r1, label %preselect_fail_1, label %continue_1 - // HAVE IT THE OTHER WAY AROUND IF TARGETS[1] IS 1 + // Second target: expected value (0 or 1) + let TargetKind::Qubit { + value: expected, .. + } = instruction.targets[1].kind + else { + return; + }; + + // Read the result into %reg + self.writer + .write_read_result(®, Operand::Qubit(result_id)); + + let begin_label = format!("preselect_begin_{id}"); + let continue_label = format!("preselect_continue_{id}"); + + // Branch: if result matches expected → continue, else → retry + if expected == 0 { + // expected 0: if read is true (1) → mismatch → retry + self.writer + .write_branch(®, &begin_label, &continue_label); + } else { + // expected 1: if read is true (1) → match → continue + self.writer + .write_branch(®, &continue_label, &begin_label); + } + + self.writer.write_label(&continue_label); } } From 92ab4a37482879805eeb9523ff29545e4fb4ae4e Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 15:06:08 -0700 Subject: [PATCH 26/67] wire noiseconfig into qir.rs --- source/compiler/qsc_stim_parser/src/lib.rs | 7 +++++++ source/compiler/qsc_stim_parser/src/qir.rs | 11 ++++++++--- source/qdk_package/qdk/_native.pyi | 9 ++++++--- source/qdk_package/qdk/stim/__init__.py | 18 ++++++++++++++++++ source/qdk_package/src/interop.rs | 19 ++++++++++++++----- source/qdk_package/src/qir_simulation.rs | 2 +- 6 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 source/qdk_package/qdk/stim/__init__.py diff --git a/source/compiler/qsc_stim_parser/src/lib.rs b/source/compiler/qsc_stim_parser/src/lib.rs index c59408e5bce..97d7b555b52 100644 --- a/source/compiler/qsc_stim_parser/src/lib.rs +++ b/source/compiler/qsc_stim_parser/src/lib.rs @@ -1,6 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use qdk_simulators::noise_config::NoiseConfig; + pub mod lex; pub mod parser; pub mod qir; + +pub fn compile(src: &str, noise: &mut NoiseConfig) -> String { + let circuit = parser::parse(src); + qir::compile_to_qir(&circuit, noise) +} diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index c0606070f62..25b1114c4cd 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. + +use qdk_simulators::noise_config::NoiseConfig + use crate::parser::*; use rustc_hash::FxHashMap; use std::fmt::Write; @@ -229,10 +232,11 @@ enum InstructionKind { CustomInstruction, } -struct Compiler { +struct Compiler<'noise> { writer: QirWriter, last_preselect_begin: Option, num_preselect_expects: u32, + noise: &'noise mut NoiseConfig, } impl Compiler { @@ -241,6 +245,7 @@ impl Compiler { writer: QirWriter::new(), last_preselect_begin: None, num_preselect_expects: 0, + noise, } } @@ -519,6 +524,6 @@ impl Compiler { } } -pub fn compile_to_qir(circuit: &Circuit) -> String { - Compiler::new().into_qir(circuit) +pub fn compile_to_qir(circuit: &Circuit, noise: &mut NoiseConfig) -> String { + Compiler::new(noise).into_qir(circuit) } diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index f59039e615e..3c3edce4d05 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -629,13 +629,16 @@ def compile_qasm_to_qsharp( """ ... -def compile_stim_to_qir(source: str) -> str: +def compile_stim_to_qir( + source: str, noise: Optional[NoiseConfig] +) -> Tuple[str, NoiseConfig]: """ Converts a Stim program to QIR. :param source: The Stim source code to convert. - :return: The converted QIR code as a string. - :rtype: str + :param noise: The noise configuration to use. + :return: The converted QIR code as a string and the noise configuration. + :rtype: Tuple[str, NoiseConfig] """ ... diff --git a/source/qdk_package/qdk/stim/__init__.py b/source/qdk_package/qdk/stim/__init__.py new file mode 100644 index 00000000000..a9d165e98b1 --- /dev/null +++ b/source/qdk_package/qdk/stim/__init__.py @@ -0,0 +1,18 @@ +from ..simulation import run_qir +from .._native import NoiseConfig, compile_stim_to_qir +from typing import List, Literal, Optional, Tuple + + +def compile(src: str, noise: Optional[NoiseConfig]) -> Tuple[str, NoiseConfig]: + return compile_stim_to_qir(src, noise) + + +def run( + src: str, + shots: Optional[int] = 1, + noise: Optional[NoiseConfig] = None, + seed: Optional[int] = None, + type: Optional[Literal["clifford", "cpu", "gpu"]] = None, +) -> List: + qir, noise = compile(src, noise) + return run_qir(qir, shots, noise, seed, type) diff --git a/source/qdk_package/src/interop.rs b/source/qdk_package/src/interop.rs index f3c555705f9..6be53d77693 100644 --- a/source/qdk_package/src/interop.rs +++ b/source/qdk_package/src/interop.rs @@ -17,7 +17,7 @@ use crate::interpreter::{ CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError, TargetProfile, format_error, format_errors, }; -use crate::qir_simulation::{NoiseConfig, unbind_noise_config}; +use crate::qir_simulation::{NoiseConfig, bind_noise_config, unbind_noise_config}; use pyo3::IntoPyObjectExt; use pyo3::exceptions::PyException; use pyo3::prelude::*; @@ -465,10 +465,19 @@ pub(crate) fn compile_qasm_to_qsharp( } #[pyfunction] -#[pyo3(signature = (source))] -pub(crate) fn compile_stim_to_qir(source: &str) -> PyResult { - let circuit = parse(source); - Ok(compile_to_qir(&circuit)) +#[pyo3(signature = (source, noise))] +pub(crate) fn compile_stim_to_qir( + py: Python, + source: &str, + noise: Option<&Bound>, +) -> PyResult<(String, NoiseConfig)> { + let mut noise_config: qdk_simulators::noise_config::NoiseConfig = noise.map_or( + qdk_simulators::noise_config::NoiseConfig::NOISELESS, + |noise_config| unbind_noise_config(py, noise_config), + ); + + let qir = qsc_stim_parser::compile(source, &mut noise_config); + Ok((qir, bind_noise_config(py, noise_config)?)) } /// Enriches the compilation errors to provide more helpful messages diff --git a/source/qdk_package/src/qir_simulation.rs b/source/qdk_package/src/qir_simulation.rs index 7b819b63cae..8670bb2219c 100644 --- a/source/qdk_package/src/qir_simulation.rs +++ b/source/qdk_package/src/qir_simulation.rs @@ -240,7 +240,7 @@ fn generic_float_cast(value: T) -> Q { num_traits::NumCast::from(value).expect("casting f64 to f32 should succeed") } -fn bind_noise_config( +pub(crate) fn bind_noise_config( py: Python, value: &qdk_simulators::noise_config::NoiseConfig, ) -> PyResult { From 96c4f2433cfd2b9eec60b0904410d78d58f48ff6 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:49:52 -0700 Subject: [PATCH 27/67] add qdk_simulators as dependency to qsc_stim_parser --- source/compiler/qsc_stim_parser/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/source/compiler/qsc_stim_parser/Cargo.toml b/source/compiler/qsc_stim_parser/Cargo.toml index 630e7a206c3..c71c2151720 100644 --- a/source/compiler/qsc_stim_parser/Cargo.toml +++ b/source/compiler/qsc_stim_parser/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true [dependencies] enum-iterator.workspace = true +qdk_simulators = { path = "../../simulators" } qsc_data_structures = { path = "../qsc_data_structures" } rustc-hash = { workspace = true } From 43a3624b03f84f3eaae8cf76d42f89a4d40b4e58 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:50:07 -0700 Subject: [PATCH 28/67] delete temporary testing files --- .../qsc_stim_parser/examples/compile_stim.rs | 15 ---- .../qsc_stim_parser/examples/lex_stim.rs | 33 ------- .../qsc_stim_parser/examples/parse_stim.rs | 89 ------------------- 3 files changed, 137 deletions(-) delete mode 100644 source/compiler/qsc_stim_parser/examples/compile_stim.rs delete mode 100644 source/compiler/qsc_stim_parser/examples/lex_stim.rs delete mode 100644 source/compiler/qsc_stim_parser/examples/parse_stim.rs diff --git a/source/compiler/qsc_stim_parser/examples/compile_stim.rs b/source/compiler/qsc_stim_parser/examples/compile_stim.rs deleted file mode 100644 index 3c070abd6e5..00000000000 --- a/source/compiler/qsc_stim_parser/examples/compile_stim.rs +++ /dev/null @@ -1,15 +0,0 @@ -use qsc_stim_parser::parser::parse; -use qsc_stim_parser::qir::compile_to_qir; -use std::fs; - -fn main() { - let stim_code = - fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); - - let circuit = parse(&stim_code); - let qir = compile_to_qir(&circuit); - - fs::write("examples/example.qir", &qir).expect("Failed to write examples/example.qir"); - - println!("Wrote examples/example.qir"); -} diff --git a/source/compiler/qsc_stim_parser/examples/lex_stim.rs b/source/compiler/qsc_stim_parser/examples/lex_stim.rs deleted file mode 100644 index ddbcf8f3a67..00000000000 --- a/source/compiler/qsc_stim_parser/examples/lex_stim.rs +++ /dev/null @@ -1,33 +0,0 @@ -use qsc_stim_parser::lex::Lexer; -use std::fs; -use std::io::Write; - -fn main() { - let stim_code = - fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); - - let mut out = - fs::File::create("examples/lex_output.txt").expect("Failed to create output file"); - - writeln!(out, "{:<20} {:<10} TEXT", "TOKEN KIND", "SPAN").unwrap(); - writeln!(out, "{:-<50}", "").unwrap(); - - let lexer = Lexer::new(&stim_code); - for token in lexer { - let text = &stim_code[token.span.lo as usize..token.span.hi as usize]; - let text_display = match token.kind { - qsc_stim_parser::lex::TokenKind::Newline => "\\n".to_string(), - _ => format!("{:?}", text), - }; - writeln!( - out, - "{:<20} {:<10} {}", - token.kind.to_string(), - format!("{}..{}", token.span.lo, token.span.hi), - text_display - ) - .unwrap(); - } - - println!("Wrote examples/lex_output.txt"); -} diff --git a/source/compiler/qsc_stim_parser/examples/parse_stim.rs b/source/compiler/qsc_stim_parser/examples/parse_stim.rs deleted file mode 100644 index 59f95b9dbea..00000000000 --- a/source/compiler/qsc_stim_parser/examples/parse_stim.rs +++ /dev/null @@ -1,89 +0,0 @@ -use qsc_stim_parser::parser::{Circuit, Instruction, Item, Pauli, Target, TargetKind, parse}; -use std::fs; -use std::io::Write; - -fn write_circuit(out: &mut impl Write, circuit: &Circuit) { - writeln!(out, "(circuit").unwrap(); - for item in &circuit.items { - write_item(out, item, 1); - } - writeln!(out, ")").unwrap(); -} - -fn write_item(out: &mut impl Write, item: &Item, indent: usize) { - let pad = " ".repeat(indent); - match item { - Item::Line(line) => { - write!(out, "{pad}(").unwrap(); - write_instruction(out, &line.instruction); - writeln!(out, ")").unwrap(); - } - Item::Block(block) => { - write!(out, "{pad}(").unwrap(); - write_instruction(out, &block.block_instruction); - writeln!(out).unwrap(); - for item in &block.items { - write_item(out, item, indent + 1); - } - writeln!(out, "{pad})").unwrap(); - } - } -} - -fn write_instruction(out: &mut impl Write, instr: &Instruction) { - write!(out, "{}", instr.name).unwrap(); - if let Some(tag) = &instr.tag { - write!(out, "[{}]", tag).unwrap(); - } - if !instr.args.is_empty() { - for arg in &instr.args { - write!(out, " {}", arg).unwrap(); - } - } - for target in &instr.targets { - write!(out, " ").unwrap(); - write_target(out, target); - } -} - -fn write_target(out: &mut impl Write, target: &Target) { - match &target.kind { - TargetKind::Qubit { negated, value } => { - if *negated { - write!(out, "!").unwrap(); - } - write!(out, "{}", value).unwrap(); - } - TargetKind::MeasurementRecord { value } => write!(out, "rec[-{}]", value).unwrap(), - TargetKind::SweepBit { value } => write!(out, "sweep[{}]", value).unwrap(), - TargetKind::Pauli { - negated, - pauli, - value, - } => { - if *negated { - write!(out, "!").unwrap(); - } - let p = match pauli { - Pauli::X => "X", - Pauli::Y => "Y", - Pauli::Z => "Z", - }; - write!(out, "{}{}", p, value).unwrap(); - } - TargetKind::Combiner => write!(out, "*").unwrap(), - } -} - -fn main() { - let stim_code = - fs::read_to_string("examples/example.stim").expect("Failed to read examples/example.stim"); - - let circuit = parse(&stim_code); - - let mut out = - fs::File::create("examples/parse_output.txt").expect("Failed to create output file"); - write_circuit(&mut out, &circuit); - - println!("Wrote examples/parse_output.txt"); -} From f90c416216abebf7c4024013fc2b49f536c3155e Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:50:40 -0700 Subject: [PATCH 29/67] fixed errors in interop.rs --- source/qdk_package/src/interop.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/qdk_package/src/interop.rs b/source/qdk_package/src/interop.rs index 6be53d77693..3dfc055aa1b 100644 --- a/source/qdk_package/src/interop.rs +++ b/source/qdk_package/src/interop.rs @@ -35,8 +35,6 @@ use qsc::{Backend, CliffordSim, PackageType, PauliNoise, SparseSim}; use qsc::{ LanguageFeatures, SourceMap, ast::Package, error::WithSource, interpret, project::FileSystem, }; -use qsc_stim_parser::parser::parse; -use qsc_stim_parser::qir::compile_to_qir; use resource_estimator as re; @@ -477,7 +475,7 @@ pub(crate) fn compile_stim_to_qir( ); let qir = qsc_stim_parser::compile(source, &mut noise_config); - Ok((qir, bind_noise_config(py, noise_config)?)) + Ok((qir, bind_noise_config(py, &noise_config)?)) } /// Enriches the compilation errors to provide more helpful messages From c2b0b99333a9f241cd3b5ad3d738100c3862b636 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:51:04 -0700 Subject: [PATCH 30/67] add loss to parser --- source/compiler/qsc_stim_parser/src/parser.rs | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 71335fc2c5b..d191ba7ec81 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -66,6 +66,9 @@ pub enum TargetKind { pauli: Pauli, value: u32, }, + Loss { + value: u32, + }, Combiner, } @@ -282,7 +285,10 @@ impl<'a> Parser<'a> { | TokenKind::Star => true, TokenKind::InstructionName => { let text = self.extract_string(*token, None); - text.starts_with('X') || text.starts_with('Y') || text.starts_with('Z') // STARTS WITH PAULI + text.starts_with('X') + || text.starts_with('Y') + || text.starts_with('Z') + || text.starts_with('L') // STARTS WITH PAULI OR LOSS } _ => false, } @@ -306,29 +312,37 @@ impl<'a> Parser<'a> { value: self.extract_uint(first_token, None), }, }, - TokenKind::InstructionName => Target { - span, - kind: TargetKind::Pauli { - negated, - pauli: self - .extract_string( - first_token, - Some(Span { - lo: span.lo, - hi: span.lo + 1, - }), - ) - .parse::() - .unwrap(), - value: self.extract_uint( - first_token, - Some(Span { - lo: span.lo + 1, - hi: span.hi, - }), - ), - }, - }, // Already validated + TokenKind::InstructionName => { + let head = self.extract_string( + first_token, + Some(Span { + lo: span.lo, + hi: span.lo + 1, + }), + ); + let value = self.extract_uint( + first_token, + Some(Span { + lo: span.lo + 1, + hi: span.hi, + }), + ); + if head == "L" { + Target { + span, + kind: TargetKind::Loss { value }, + } + } else { + Target { + span, + kind: TargetKind::Pauli { + negated, + pauli: head.parse::().unwrap(), + value, + }, + } + } + } // Already validated TokenKind::Rec => Target { span, kind: TargetKind::MeasurementRecord { From 201d3b41fef7aac728f387678f81783f956deb77 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:51:41 -0700 Subject: [PATCH 31/67] add noise instructions to stim compiler --- source/compiler/qsc_stim_parser/src/qir.rs | 246 ++++++++++++++++++++- 1 file changed, 241 insertions(+), 5 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 25b1114c4cd..6a6196fb546 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qdk_simulators::noise_config::NoiseConfig +use qdk_simulators::noise_config::{NoiseConfig, NoiseTable, encode_pauli}; use crate::parser::*; use rustc_hash::FxHashMap; @@ -20,6 +20,7 @@ struct QirWriter { qubit_map: FxHashMap, num_results: u32, used_intrinsics: FxHashMap, + has_noise_intrinsic: bool, } impl QirWriter { @@ -29,6 +30,7 @@ impl QirWriter { qubit_map: FxHashMap::default(), num_results: 0, used_intrinsics: FxHashMap::default(), + has_noise_intrinsic: false, } } @@ -57,6 +59,28 @@ impl QirWriter { .or_insert_with(|| format!("declare void @{name}({params})")); } + fn write_noise_intrinsic(&mut self, name: &str, qubits: &[u32]) { + write!(self.output, " call void @{name}(").unwrap(); + for (i, &qubit) in qubits.iter().enumerate() { + if i > 0 { + write!(self.output, ", ").unwrap(); + } + // Register the qubit so it is reflected in `required_num_qubits`, but emit + // the raw Stim index as the operand. + let id = self.map_qubit(qubit); + write!(self.output, "ptr inttoptr (i64 {id} to ptr)").unwrap(); + } + writeln!(self.output, ")").unwrap(); + let params = (0..qubits.len()) + .map(|_| "ptr") + .collect::>() + .join(", "); + self.used_intrinsics + .entry(name.to_string()) + .or_insert_with(|| format!("declare void @{name}({params}) #2")); + self.has_noise_intrinsic = true; + } + // Resolves an Operand to its QIR ID and writes: `ptr inttoptr (i64 N to ptr)` fn write_operand(&mut self, operand: Operand) { let id = match operand { @@ -161,6 +185,10 @@ impl QirWriter { writeln!(self.output).unwrap(); writeln!(self.output, "; module flags").unwrap(); writeln!(self.output).unwrap(); + if self.has_noise_intrinsic { + writeln!(self.output, "attributes #2 = {{ \"qdk_noise\" }}").unwrap(); + writeln!(self.output).unwrap(); + } writeln!( self.output, "!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}" @@ -232,20 +260,55 @@ enum InstructionKind { CustomInstruction, } +/// A single fault term (`X`, `Y`, `Z`, or `L`) applied to a qubit. +#[derive(Clone, Copy)] +enum FaultChar { + X, + Y, + Z, + Loss, +} + +impl FaultChar { + fn as_char(self) -> char { + match self { + FaultChar::X => 'X', + FaultChar::Y => 'Y', + FaultChar::Z => 'Z', + FaultChar::Loss => 'L', + } + } +} + +struct CorrelatedRow { + terms: Vec<(u32, FaultChar)>, + probability: f64, +} + +/// An accumulating `CORRELATED_ERROR` / `ELSE_CORRELATED_ERROR` group. +#[derive(Default)] +struct CorrelatedGroup { + rows: Vec, +} + struct Compiler<'noise> { writer: QirWriter, last_preselect_begin: Option, num_preselect_expects: u32, noise: &'noise mut NoiseConfig, + current_correlated_group: Option, + num_noise_intrinsics: u32, } -impl Compiler { - fn new() -> Self { +impl<'noise> Compiler<'noise> { + fn new(noise: &'noise mut NoiseConfig) -> Self { Self { writer: QirWriter::new(), last_preselect_begin: None, num_preselect_expects: 0, noise, + current_correlated_group: None, + num_noise_intrinsics: 0, } } @@ -281,6 +344,10 @@ impl Compiler { } fn compile_instruction(&mut self, instruction: &Instruction) { + if self.current_correlated_group.is_some() && instruction.name != "ELSE_CORRELATED_ERROR" { + self.finish_correlated_group(); + } + match Self::instruction_kind(&instruction.name) { InstructionKind::PauliGate => { self.compile_pauli_gate(instruction); @@ -365,7 +432,174 @@ impl Compiler { } } - fn compile_noise_channel(&mut self, _instruction: &Instruction) {} + fn compile_noise_channel(&mut self, instruction: &Instruction) { + let gate = instruction.name.to_lowercase(); + if gate == "correlated_error" || gate == "else_correlated_error" { + self.accumulate_correlated_error(instruction); + } else if gate == "x_error" + || gate == "y_error" + || gate == "z_error" + || gate == "loss_error" + { + let fault = match gate.as_str() { + "x_error" => FaultChar::X, + "y_error" => FaultChar::Y, + "z_error" => FaultChar::Z, + _ => FaultChar::Loss, + }; + let probability = instruction.args[0]; + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.current_correlated_group + .get_or_insert_with(CorrelatedGroup::default) + .rows + .push(CorrelatedRow { + terms: vec![(value, fault)], + probability, + }); + self.finish_correlated_group(); // one independent table per qubit + } + } else if gate == "depolarize1" { + let each = instruction.args[0] / 3.0; + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + { + let group = self + .current_correlated_group + .get_or_insert_with(CorrelatedGroup::default); + for fault in [FaultChar::X, FaultChar::Y, FaultChar::Z] { + group.rows.push(CorrelatedRow { + terms: vec![(value, fault)], + probability: each, + }); + } + } + self.finish_correlated_group(); // one independent 1-qubit table per target + } + } else if gate == "depolarize2" { + let each = instruction.args[0] / 15.0; + for pair in instruction.targets.chunks(2) { + let TargetKind::Qubit { value: q0, .. } = pair[0].kind else { + continue; + }; + let TargetKind::Qubit { value: q1, .. } = pair[1].kind else { + continue; + }; + { + let group = self + .current_correlated_group + .get_or_insert_with(CorrelatedGroup::default); + // All 16 (p0, p1) combos except (I, I); None means identity on that qubit. + let options = [ + None, + Some(FaultChar::X), + Some(FaultChar::Y), + Some(FaultChar::Z), + ]; + for p0 in options { + for p1 in options { + if p0.is_none() && p1.is_none() { + continue; // skip identity + } + let mut terms = Vec::new(); + if let Some(f) = p0 { + terms.push((q0, f)); + } + if let Some(f) = p1 { + terms.push((q1, f)); + } + group.rows.push(CorrelatedRow { + terms, + probability: each, + }); + } + } + } + self.finish_correlated_group(); // one independent 2-qubit table per pair + } + } + } + + fn accumulate_correlated_error(&mut self, instruction: &Instruction) { + let probability = instruction.args[0]; + let mut terms = Vec::new(); + for target in &instruction.targets { + match &target.kind { + TargetKind::Pauli { pauli, value, .. } => { + let fault = match pauli { + Pauli::X => FaultChar::X, + Pauli::Y => FaultChar::Y, + Pauli::Z => FaultChar::Z, + }; + terms.push((*value, fault)); + } + TargetKind::Loss { value } => { + terms.push((*value, FaultChar::Loss)); + } + _ => {} + } + } + + self.current_correlated_group + .get_or_insert_with(CorrelatedGroup::default) + .rows + .push(CorrelatedRow { terms, probability }) + } + + fn finish_correlated_group(&mut self) { + let Some(group) = self.current_correlated_group.take() else { + return; + }; + if group.rows.is_empty() { + return; + } + + let id = self.num_noise_intrinsics; + self.num_noise_intrinsics += 1; + let name = format!("noise_intrinsic_{id}"); + + // Columns are the sorted union of every qubit touched by the group. + let mut columns: Vec = group + .rows + .iter() + .flat_map(|row| row.terms.iter().map(|(qubit, _)| *qubit)) + .collect(); + columns.sort_unstable(); + columns.dedup(); + + let column_index: FxHashMap = columns + .iter() + .enumerate() + .map(|(i, &qubit)| (qubit, i)) + .collect(); + + let mut pauli_strings = Vec::with_capacity(group.rows.len()); + let mut probabilities = Vec::with_capacity(group.rows.len()); + for row in &group.rows { + // Build the fault string over the group columns; untouched qubits are `I`. + let mut chars = vec!['I'; columns.len()]; + for &(qubit, fault) in &row.terms { + let idx = column_index[&qubit]; + chars[idx] = fault.as_char(); + } + let pauli: String = chars.into_iter().collect(); + pauli_strings.push(encode_pauli(&pauli)); + probabilities.push(row.probability); + } + + let table = NoiseTable { + qubits: columns.len() as u32, + pauli_strings, + probabilities, + }; + self.noise.intrinsics.insert(id, table); + + self.writer.write_noise_intrinsic(&name, &columns); + } fn compile_collapsing_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); @@ -491,7 +725,8 @@ impl Compiler { | "PAULI_CHANNEL_2" | "X_ERROR" | "Y_ERROR" - | "Z_ERROR" => InstructionKind::NoiseChannel, + | "Z_ERROR" + | "LOSS_ERROR" => InstructionKind::NoiseChannel, // Collapsing Gates "M" | "MR" | "MRX" | "MRY" | "MRZ" | "MX" | "MY" | "MZ" | "R" | "RX" | "RY" | "RZ" => { @@ -519,6 +754,7 @@ impl Compiler { fn into_qir(mut self, circuit: &Circuit) -> String { self.writer.write_header(); self.compile_circuit(circuit); + self.finish_correlated_group(); self.writer.write_footer(); self.writer.output } From 1caf53c9560e47f54f89e6bffcbe37bf85c53170 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:52:06 -0700 Subject: [PATCH 32/67] fix bind_noise_config bug where intrinsics was always created empty --- source/qdk_package/src/qir_simulation.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/source/qdk_package/src/qir_simulation.rs b/source/qdk_package/src/qir_simulation.rs index 8670bb2219c..8d83984d91c 100644 --- a/source/qdk_package/src/qir_simulation.rs +++ b/source/qdk_package/src/qir_simulation.rs @@ -271,10 +271,30 @@ pub(crate) fn bind_noise_config( mz: Py::new(py, NoiseTable::from(value.mz.clone()))?, mresetz: Py::new(py, NoiseTable::from(value.mresetz.clone()))?, // idle: Py::new(py, IdleNoiseParams::from(value.idle))?, - intrinsics: Py::new(py, NoiseIntrinsicsTable::default())?, + intrinsics: Py::new(py, to_intrinsics_table(py, &value.intrinsics)?)?, }) } +/// Builds a Python [`NoiseIntrinsicsTable`] from the Rust intrinsics map, +/// preserving each intrinsic's id and naming it `noise_intrinsic_{id}` so the +/// name matches the call emitted by the Stim-to-QIR compiler. +fn to_intrinsics_table( + py: Python, + intrinsics: &FxHashMap>, +) -> PyResult { + let mut table = FxHashMap::default(); + let mut next_id = 0u32; + for (id, noise_table) in intrinsics { + let name = format!("noise_intrinsic_{id}"); + table.insert( + name, + (*id, Py::new(py, NoiseTable::from(noise_table.clone()))?), + ); + next_id = next_id.max(*id + 1); + } + Ok(NoiseIntrinsicsTable { next_id, table }) +} + pub(crate) fn unbind_noise_config( py: Python, value: &Bound, From a6d2538210ecd52354ffa01f12c66d0dff1bb6dc Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 17:52:20 -0700 Subject: [PATCH 33/67] create stim compilation and simulation notebook --- samples/notebooks/stim_to_qir.ipynb | 591 ++++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 samples/notebooks/stim_to_qir.ipynb diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb new file mode 100644 index 00000000000..25e563ebfc8 --- /dev/null +++ b/samples/notebooks/stim_to_qir.ipynb @@ -0,0 +1,591 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "772299d0", + "metadata": {}, + "source": [ + "# Stim to QIR Compilation & Simulation\n", + "\n", + "This notebook demonstrates compiling a [Stim](https://github.com/quantumlib/Stim) circuit to QIR and running it on the simulator using the `qdk.stim` module.\n", + "\n", + "The two entry points are:\n", + "\n", + "- `qdk.stim.compile(src, noise)` → returns a `(qir, noise)` tuple. The QIR is a string; `noise` is a `NoiseConfig` (auto-built from the circuit's correlated-error chains when you pass `None`).\n", + "- `qdk.stim.run(src, shots=..., noise=..., seed=..., type=...)` → compiles and simulates in one step, returning a list of per-shot measurement results." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6c9df231", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk import stim" + ] + }, + { + "cell_type": "markdown", + "id": "39a2e4f8", + "metadata": {}, + "source": [ + "## Basics\n", + "\n", + "Compile a plain Clifford circuit to QIR, then run it on the simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a41fe64d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "define i64 @ENTRYPOINT__main() #0 {\n", + " call void @__quantum__rt__initialize(ptr null)\n", + " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__cz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", + " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr))\n", + " call void @__quantum__rt__array_record_output(i64 2, ptr null)\n", + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null)\n", + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null)\n", + " ret i64 0\n", + "}\n", + "\n", + "declare void @__quantum__qis__mresetz__body(ptr, ptr)\n", + "declare void @__quantum__rt__result_record_output(ptr, ptr)\n", + "declare void @__quantum__rt__array_record_output(i64, ptr)\n", + "declare void @__quantum__qis__cz__body(ptr, ptr)\n", + "declare void @__quantum__rt__initialize(ptr)\n", + "declare void @__quantum__qis__h__body(ptr)\n", + "\n", + "attributes #0 = { \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"2\" \"required_num_results\"=\"2\" }\n", + "attributes #1 = { \"irreversible\" }\n", + "\n", + "; module flags\n", + "\n", + "!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}\n", + "\n", + "!0 = !{i32 1, !\"qir_major_version\", i32 2}\n", + "!1 = !{i32 7, !\"qir_minor_version\", i32 1}\n", + "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 false}\n", + "!3 = !{i32 1, !\"dynamic_result_management\", i1 false}\n", + "!4 = !{i32 5, !\"int_computations\", !{!\"i64\"}}\n", + "!5 = !{i32 5, !\"float_computations\", !{!\"double\"}}\n", + "!6 = !{i32 7, !\"backwards_branching\", i2 3}\n", + "!7 = !{i32 1, !\"arrays\", i1 true}\n", + "\n" + ] + } + ], + "source": [ + "# Start simple: a Clifford circuit with no noise.\n", + "# compile() returns a (qir, noise) tuple. Pass noise=None to auto-build a\n", + "# NoiseConfig (noiseless here, since there are no error channels).\n", + "stim_circuit = \"\"\"H 0\n", + "CZ 0 1\n", + "H 0\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "qir, noise = stim.compile(stim_circuit, None)\n", + "print(qir)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d859fe1a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got 100 shots\n", + "[One, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n" + ] + } + ], + "source": [ + "stim_circuit = \"\"\"H 0\n", + "CX 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "# run() compiles and simulates in one step.\n", + "results = stim.run(stim_circuit, shots=100, type=\"cpu\")\n", + "print(f\"Got {len(results)} shots\")\n", + "for r in results[:10]:\n", + " print(r)" + ] + }, + { + "cell_type": "markdown", + "id": "ad69889c", + "metadata": {}, + "source": [ + "## Preselection\n", + "\n", + "The custom `#!preselect_begin` / `#!preselect_expect` annotations compile to a\n", + "retry loop in the QIR: the block is repeated until the measured result matches\n", + "the expected value. This is useful for post-selecting on a desired measurement\n", + "outcome." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "452e15ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "define i64 @ENTRYPOINT__main() #0 {\n", + " call void @__quantum__rt__initialize(ptr null)\n", + " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))\n", + " br label %preselect_begin_0\n", + "preselect_begin_0:\n", + " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", + " %preselect_r0 = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 0 to ptr))\n", + " br i1 %preselect_r0, label %preselect_continue_0, label %preselect_begin_0\n", + "preselect_continue_0:\n", + " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__cz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", + " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 2 to ptr))\n", + " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 3 to ptr))\n", + " call void @__quantum__rt__array_record_output(i64 4, ptr null)\n", + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null)\n", + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null)\n", + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 2 to ptr), ptr null)\n", + " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 3 to ptr), ptr null)\n", + " ret i64 0\n", + "}\n", + "\n", + "declare void @__quantum__qis__h__body(ptr)\n", + "declare void @__quantum__qis__cz__body(ptr, ptr)\n", + "declare void @__quantum__rt__result_record_output(ptr, ptr)\n", + "declare void @__quantum__rt__array_record_output(i64, ptr)\n", + "declare i1 @__quantum__rt__read_result(ptr)\n", + "declare void @__quantum__qis__mresetz__body(ptr, ptr)\n", + "declare void @__quantum__rt__initialize(ptr)\n", + "\n", + "attributes #0 = { \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"2\" \"required_num_results\"=\"4\" }\n", + "attributes #1 = { \"irreversible\" }\n", + "\n", + "; module flags\n", + "\n", + "!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}\n", + "\n", + "!0 = !{i32 1, !\"qir_major_version\", i32 2}\n", + "!1 = !{i32 7, !\"qir_minor_version\", i32 1}\n", + "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 false}\n", + "!3 = !{i32 1, !\"dynamic_result_management\", i1 false}\n", + "!4 = !{i32 5, !\"int_computations\", !{!\"i64\"}}\n", + "!5 = !{i32 5, !\"float_computations\", !{!\"double\"}}\n", + "!6 = !{i32 7, !\"backwards_branching\", i2 3}\n", + "!7 = !{i32 1, !\"arrays\", i1 true}\n", + "\n", + "[Zero, One, Zero, Zero]\n", + "[Zero, Zero, Zero, Zero]\n", + "[Zero, Zero, Zero, Zero]\n", + "[Zero, One, Zero, Zero]\n", + "[One, Zero, One, Zero]\n", + "[Zero, One, Zero, Zero]\n", + "[One, Zero, Zero, Zero]\n", + "[One, One, One, Zero]\n", + "[One, One, Zero, Zero]\n", + "[Zero, Zero, Zero, Zero]\n" + ] + } + ], + "source": [ + "preselect_circuit = \"\"\"H 0\n", + "MR 0\n", + "#!preselect_begin\n", + "H 0\n", + "MR 0\n", + "#!preselect_expect 0 1\n", + "H 0\n", + "CZ 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "qir, noise = stim.compile(preselect_circuit, None)\n", + "print(qir)\n", + "\n", + "results = stim.run(preselect_circuit, shots=50, type=\"cpu\")\n", + "for r in results[:10]:\n", + " print(r)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca62aa9f", + "metadata": { + "vscode": { + "languageId": "markdown" + } + }, + "outputs": [], + "source": [ + "## Noise channels\n", + "\n", + "From here on we exercise the noise channels. Each one compiles to a noise\n", + "intrinsic plus a `NoiseTable` that maps Pauli/loss strings to probabilities.\n", + "Passing `noise=None` to `stim.compile` auto-builds the `NoiseConfig` from the\n", + "circuit and returns it, and `stim.run` wires it into the simulator.\n", + "\n", + "### Correlated error\n", + "\n", + "A `CORRELATED_ERROR` / `ELSE_CORRELATED_ERROR` chain is a set of mutually\n", + "exclusive faults applied together as one correlated event." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c2b7a13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, One]\n", + "[Zero, Zero]\n", + "[One, One]\n", + "[One, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n", + "[One, One]\n", + "[One, One]\n", + "[One, One]\n", + "[One, One]\n" + ] + } + ], + "source": [ + "# A correlated error on qubits 0 and 1: X on both, together, with probability 0.2.\n", + "correlated_circuit = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", + "ELSE_CORRELATED_ERROR(0.1) X1 X2\n", + "ELSE_CORRELATED_ERROR(0.1) X0 X2\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "results = stim.run(correlated_circuit, shots=20, type=\"cpu\")\n", + "for r in results:\n", + " print(r)" + ] + }, + { + "cell_type": "markdown", + "id": "08be56a0", + "metadata": {}, + "source": [ + "### Single-qubit Pauli errors: `X_ERROR`, `Y_ERROR`, `Z_ERROR`\n", + "\n", + "Unlike `CORRELATED_ERROR`, these apply an **independent** Pauli to each target\n", + "qubit with the given probability. Each target becomes its own one-qubit noise\n", + "table. An `X_ERROR` before a `Z`-basis measurement flips the result." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "57943533", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, One]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[Zero, Zero]\n" + ] + } + ], + "source": [ + "# Independent X errors on qubits 0 and 1, each with probability 0.1.\n", + "# Expect ~10% of each qubit to flip to 1, independently.\n", + "xyz_circuit = \"\"\"X_ERROR(0.1) 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "results = stim.run(xyz_circuit, shots=20, type=\"cpu\")\n", + "\n", + "for r in results: print(r)" + ] + }, + { + "cell_type": "markdown", + "id": "8a236142", + "metadata": {}, + "source": [ + "### Qubit loss: `LOSS_ERROR`\n", + "\n", + "`LOSS_ERROR` independently loses each target qubit with the given probability.\n", + "A lost qubit is reported as `L` in the measurement results (rather than `0`/`1`)." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "a10524be", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n" + ] + } + ], + "source": [ + "# Independent loss on qubits 0 and 1, each with probability 0.15.\n", + "# Lost qubits show up as `L`; otherwise the qubit measures 0.\n", + "loss_circuit = \"\"\"LOSS_ERROR(0.15) 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "results = stim.run(loss_circuit, shots=20, type=\"cpu\")\n", + "\n", + "for r in results: print(r)" + ] + }, + { + "cell_type": "markdown", + "id": "0d45640f", + "metadata": {}, + "source": [ + "### Loss inside a correlated error\n", + "\n", + "A `CORRELATED_ERROR` chain can mix Pauli terms (`X`/`Y`/`Z`) and loss terms\n", + "(`L`) in the same fault. Here the mutually-exclusive branches lose qubit 0,\n", + "lose qubit 1, or lose qubit 0 while applying `Z` to qubit 1." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "5aadceb8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Loss, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Loss, Zero]\n", + "[Zero, Loss]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Loss]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Loss, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n" + ] + } + ], + "source": [ + "# Mutually-exclusive correlated branches mixing loss and Pauli terms.\n", + "mixed_circuit = \"\"\"CORRELATED_ERROR(0.1) L0\n", + "ELSE_CORRELATED_ERROR(0.1) L1\n", + "ELSE_CORRELATED_ERROR(0.1) L0 Z1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "results = stim.run(mixed_circuit, shots=20, type=\"cpu\")\n", + "\n", + "for r in results: print(r)" + ] + }, + { + "cell_type": "markdown", + "id": "1c40106e", + "metadata": {}, + "source": [ + "### Depolarizing noise: `DEPOLARIZE1`, `DEPOLARIZE2`\n", + "\n", + "`DEPOLARIZE1(p)` applies one of the 3 single-qubit Paulis (each with\n", + "probability `p/3`) independently to each target. `DEPOLARIZE2(p)` applies one\n", + "of the 15 non-identity two-qubit Paulis (each with probability `p/15`) to each\n", + "qubit pair." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10a92131", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEPOLARIZE1(0.3):\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, One]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, Zero]\n", + "[Zero, One]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, One]\n", + "[Zero, One]\n", + "[One, One]\n", + "[Zero, Zero]\n", + "[Zero, One]\n", + "[Zero, Zero]\n", + "\n", + "DEPOLARIZE2(0.3):\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, One]\n", + "[Zero, Zero]\n", + "[Zero, One]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[Zero, Zero]\n", + "[One, One]\n", + "[One, One]\n", + "[One, Zero]\n", + "[One, Zero]\n" + ] + } + ], + "source": [ + "# DEPOLARIZE1: single-qubit depolarizing on qubits 0 and 1.\n", + "# In the Z basis, X and Y components flip the measurement, so ~2/3 * p flip.\n", + "print(\"DEPOLARIZE1(0.3):\")\n", + "depolarize1_circuit = \"\"\"DEPOLARIZE1(0.3) 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "for r in stim.run(depolarize1_circuit, shots=20, type=\"cpu\"):\n", + " print(r)\n", + "\n", + "print(\"\\nDEPOLARIZE2(0.3):\")\n", + "depolarize2_circuit = \"\"\"DEPOLARIZE2(0.3) 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "for r in stim.run(depolarize2_circuit, shots=20, type=\"cpu\"): print(r)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.12.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From d326df4e90a8f95c5ae5b0315631fc4f511c5613 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 22:57:55 -0700 Subject: [PATCH 34/67] support cx, cy --- source/compiler/qsc_stim_parser/src/qir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 6a6196fb546..445823b6041 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -417,7 +417,7 @@ impl<'noise> Compiler<'noise> { fn compile_two_qubit_clifford_gate(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); - if gate == "cz" { + if gate == "cz" || gate == "cx" || gate == "cy" { let targets = &instruction.targets; for pair in targets.chunks(2) { let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { From b346f5f3272f13db6e1b5d3c5ccba33a35b33f38 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 22:59:28 -0700 Subject: [PATCH 35/67] remove redundant api --- source/qdk_package/qdk/simulation/_simulation.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/source/qdk_package/qdk/simulation/_simulation.py b/source/qdk_package/qdk/simulation/_simulation.py index e90b5afc4c9..4aa6bf367d7 100644 --- a/source/qdk_package/qdk/simulation/_simulation.py +++ b/source/qdk_package/qdk/simulation/_simulation.py @@ -18,7 +18,6 @@ GpuContext, try_create_gpu_adapter, Result, - compile_stim_to_qir, ) from pyqir import ( Function, @@ -744,14 +743,3 @@ def run_qir( return run_qir_gpu(input, shots, noise, seed) case _: raise ValueError(f"Invalid simulator type: {type}") - - -def run_stim( - src: str, - shots: Optional[int] = 1, - noise: Optional[NoiseConfig] = None, - seed: Optional[int] = None, - type: Optional[Literal["clifford", "cpu", "gpu"]] = None, -) -> List: - qir = compile_stim_to_qir(src) - return run_qir(qir, shots, noise, seed, type) From 993f68005b2292b8aa6452d5505c923902781958 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 23:00:29 -0700 Subject: [PATCH 36/67] add license header to lex.rs --- source/compiler/qsc_stim_parser/src/lex.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 0d752d4caf0..b98458176e2 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use enum_iterator::Sequence; use qsc_data_structures::span::Span; use std::str::CharIndices; From 1a448156b3ed5093cadb09658f7558bfeeb03860 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 23:03:47 -0700 Subject: [PATCH 37/67] emit sx directly --- source/compiler/qsc_stim_parser/src/qir.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 445823b6041..a38e2af228b 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -408,9 +408,7 @@ impl<'noise> Compiler<'noise> { continue; }; let q = Operand::Qubit(value); - self.writer.write_call("h", &[q]); - self.writer.write_call("s", &[q]); - self.writer.write_call("h", &[q]); + self.writer.write_call("sx", &[q]); } } } From 8eab3601dd9fa4e91cdffa5dac340b7da375d6b3 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 23:30:00 -0700 Subject: [PATCH 38/67] remove redundant api --- source/qdk_package/qdk/simulation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/qdk_package/qdk/simulation/__init__.py b/source/qdk_package/qdk/simulation/__init__.py index 62515d0c4d0..8e5701332c4 100644 --- a/source/qdk_package/qdk/simulation/__init__.py +++ b/source/qdk_package/qdk/simulation/__init__.py @@ -26,7 +26,7 @@ """ from .._device._atom import NeutralAtomDevice -from ._simulation import NoiseConfig, run_qir, run_stim +from ._simulation import NoiseConfig, run_qir from ._noisy_simulator import ( NoisySimulatorError, DensityMatrixSimulator, From 48925528f280dd39d715461f4d8f5d8c669cf27d Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 15 Jun 2026 23:30:17 -0700 Subject: [PATCH 39/67] improve notebook for greater visibility --- samples/notebooks/stim_to_qir.ipynb | 495 +++++++++------------------- 1 file changed, 162 insertions(+), 333 deletions(-) diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb index 25e563ebfc8..3e36e8ea899 100644 --- a/samples/notebooks/stim_to_qir.ipynb +++ b/samples/notebooks/stim_to_qir.ipynb @@ -5,14 +5,12 @@ "id": "772299d0", "metadata": {}, "source": [ - "# Stim to QIR Compilation & Simulation\n", + "# Stim → QIR\n", "\n", - "This notebook demonstrates compiling a [Stim](https://github.com/quantumlib/Stim) circuit to QIR and running it on the simulator using the `qdk.stim` module.\n", + "Compile [Stim](https://github.com/quantumlib/Stim) circuits to QIR and simulate them with `qdk.stim`.\n", "\n", - "The two entry points are:\n", - "\n", - "- `qdk.stim.compile(src, noise)` → returns a `(qir, noise)` tuple. The QIR is a string; `noise` is a `NoiseConfig` (auto-built from the circuit's correlated-error chains when you pass `None`).\n", - "- `qdk.stim.run(src, shots=..., noise=..., seed=..., type=...)` → compiles and simulates in one step, returning a list of per-shot measurement results." + "- `stim.compile(src, None)` → `(qir, noise)`\n", + "- `stim.run(src, shots=..., type=\"clifford\")` → per-shot results" ] }, { @@ -22,7 +20,8 @@ "metadata": {}, "outputs": [], "source": [ - "from qdk import stim" + "from qdk import stim\n", + "from qsharp_widgets import Histogram" ] }, { @@ -32,7 +31,7 @@ "source": [ "## Basics\n", "\n", - "Compile a plain Clifford circuit to QIR, then run it on the simulator." + "Compile a Bell pair to QIR." ] }, { @@ -48,8 +47,7 @@ "define i64 @ENTRYPOINT__main() #0 {\n", " call void @__quantum__rt__initialize(ptr null)\n", " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", - " call void @__quantum__qis__cz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", - " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", + " call void @__quantum__qis__cx__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))\n", " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr))\n", " call void @__quantum__rt__array_record_output(i64 2, ptr null)\n", @@ -58,10 +56,10 @@ " ret i64 0\n", "}\n", "\n", + "declare void @__quantum__qis__cx__body(ptr, ptr)\n", "declare void @__quantum__qis__mresetz__body(ptr, ptr)\n", "declare void @__quantum__rt__result_record_output(ptr, ptr)\n", "declare void @__quantum__rt__array_record_output(i64, ptr)\n", - "declare void @__quantum__qis__cz__body(ptr, ptr)\n", "declare void @__quantum__rt__initialize(ptr)\n", "declare void @__quantum__qis__h__body(ptr)\n", "\n", @@ -85,16 +83,12 @@ } ], "source": [ - "# Start simple: a Clifford circuit with no noise.\n", - "# compile() returns a (qir, noise) tuple. Pass noise=None to auto-build a\n", - "# NoiseConfig (noiseless here, since there are no error channels).\n", - "stim_circuit = \"\"\"H 0\n", - "CZ 0 1\n", - "H 0\n", + "bell = \"\"\"H 0\n", + "CX 0 1\n", "MR 0 1\n", "\"\"\"\n", "\n", - "qir, noise = stim.compile(stim_circuit, None)\n", + "qir, _ = stim.compile(bell, None)\n", "print(qir)" ] }, @@ -105,34 +99,24 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Got 100 shots\n", - "[One, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8aec9a404d224ea3b6e6fa15311cbccb", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "stim_circuit = \"\"\"H 0\n", - "CX 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "\n", - "# run() compiles and simulates in one step.\n", - "results = stim.run(stim_circuit, shots=100, type=\"cpu\")\n", - "print(f\"Got {len(results)} shots\")\n", - "for r in results[:10]:\n", - " print(r)" + "# Entangled: only 00 and 11 appear.\n", + "Histogram([\"\".join(map(str, s)) for s in stim.run(bell, shots=2000, type=\"clifford\")])" ] }, { @@ -142,100 +126,39 @@ "source": [ "## Preselection\n", "\n", - "The custom `#!preselect_begin` / `#!preselect_expect` annotations compile to a\n", - "retry loop in the QIR: the block is repeated until the measured result matches\n", - "the expected value. This is useful for post-selecting on a desired measurement\n", - "outcome." + "`#!preselect_expect` retries until the measurement matches, so the recorded result is always that value — a single bar." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "452e15ec", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "define i64 @ENTRYPOINT__main() #0 {\n", - " call void @__quantum__rt__initialize(ptr null)\n", - " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", - " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))\n", - " br label %preselect_begin_0\n", - "preselect_begin_0:\n", - " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", - " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", - " %preselect_r0 = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 0 to ptr))\n", - " br i1 %preselect_r0, label %preselect_continue_0, label %preselect_begin_0\n", - "preselect_continue_0:\n", - " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", - " call void @__quantum__qis__cz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", - " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 2 to ptr))\n", - " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 3 to ptr))\n", - " call void @__quantum__rt__array_record_output(i64 4, ptr null)\n", - " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null)\n", - " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null)\n", - " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 2 to ptr), ptr null)\n", - " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 3 to ptr), ptr null)\n", - " ret i64 0\n", - "}\n", - "\n", - "declare void @__quantum__qis__h__body(ptr)\n", - "declare void @__quantum__qis__cz__body(ptr, ptr)\n", - "declare void @__quantum__rt__result_record_output(ptr, ptr)\n", - "declare void @__quantum__rt__array_record_output(i64, ptr)\n", - "declare i1 @__quantum__rt__read_result(ptr)\n", - "declare void @__quantum__qis__mresetz__body(ptr, ptr)\n", - "declare void @__quantum__rt__initialize(ptr)\n", - "\n", - "attributes #0 = { \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"2\" \"required_num_results\"=\"4\" }\n", - "attributes #1 = { \"irreversible\" }\n", - "\n", - "; module flags\n", - "\n", - "!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}\n", - "\n", - "!0 = !{i32 1, !\"qir_major_version\", i32 2}\n", - "!1 = !{i32 7, !\"qir_minor_version\", i32 1}\n", - "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 false}\n", - "!3 = !{i32 1, !\"dynamic_result_management\", i1 false}\n", - "!4 = !{i32 5, !\"int_computations\", !{!\"i64\"}}\n", - "!5 = !{i32 5, !\"float_computations\", !{!\"double\"}}\n", - "!6 = !{i32 7, !\"backwards_branching\", i2 3}\n", - "!7 = !{i32 1, !\"arrays\", i1 true}\n", - "\n", - "[Zero, One, Zero, Zero]\n", - "[Zero, Zero, Zero, Zero]\n", - "[Zero, Zero, Zero, Zero]\n", - "[Zero, One, Zero, Zero]\n", - "[One, Zero, One, Zero]\n", - "[Zero, One, Zero, Zero]\n", - "[One, Zero, Zero, Zero]\n", - "[One, One, One, Zero]\n", - "[One, One, Zero, Zero]\n", - "[Zero, Zero, Zero, Zero]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "530e5e728ace4abbbf749aa38862749a", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "preselect_circuit = \"\"\"H 0\n", - "MR 0\n", - "#!preselect_begin\n", + "# Force the measurement to 1: every shot reports 1.\n", + "preselect = \"\"\"#!preselect_begin\n", "H 0\n", "MR 0\n", "#!preselect_expect 0 1\n", - "H 0\n", - "CZ 0 1\n", - "MR 0 1\n", "\"\"\"\n", - "\n", - "qir, noise = stim.compile(preselect_circuit, None)\n", - "print(qir)\n", - "\n", - "results = stim.run(preselect_circuit, shots=50, type=\"cpu\")\n", - "for r in results[:10]:\n", - " print(r)" + "Histogram([\"\".join(map(str, s)) for s in stim.run(preselect, shots=2000, type=\"clifford\")])" ] }, { @@ -251,61 +174,38 @@ "source": [ "## Noise channels\n", "\n", - "From here on we exercise the noise channels. Each one compiles to a noise\n", - "intrinsic plus a `NoiseTable` that maps Pauli/loss strings to probabilities.\n", - "Passing `noise=None` to `stim.compile` auto-builds the `NoiseConfig` from the\n", - "circuit and returns it, and `stim.run` wires it into the simulator.\n", - "\n", "### Correlated error\n", "\n", - "A `CORRELATED_ERROR` / `ELSE_CORRELATED_ERROR` chain is a set of mutually\n", - "exclusive faults applied together as one correlated event." + "`X0 X1` fire together → only `00` and `11`." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "0c2b7a13", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, One]\n", - "[Zero, Zero]\n", - "[One, One]\n", - "[One, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n", - "[One, One]\n", - "[One, One]\n", - "[One, One]\n", - "[One, One]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "85dfea9eb96f45a3a661fd6ee1b9f768", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# A correlated error on qubits 0 and 1: X on both, together, with probability 0.2.\n", - "correlated_circuit = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", - "ELSE_CORRELATED_ERROR(0.1) X1 X2\n", - "ELSE_CORRELATED_ERROR(0.1) X0 X2\n", + "correlated = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", "MR 0 1\n", "\"\"\"\n", - "\n", - "results = stim.run(correlated_circuit, shots=20, type=\"cpu\")\n", - "for r in results:\n", - " print(r)" + "Histogram([\"\".join(map(str, s)) for s in stim.run(correlated, shots=5000, type=\"clifford\")])" ] }, { @@ -313,56 +213,38 @@ "id": "08be56a0", "metadata": {}, "source": [ - "### Single-qubit Pauli errors: `X_ERROR`, `Y_ERROR`, `Z_ERROR`\n", + "### Pauli error\n", "\n", - "Unlike `CORRELATED_ERROR`, these apply an **independent** Pauli to each target\n", - "qubit with the given probability. Each target becomes its own one-qubit noise\n", - "table. An `X_ERROR` before a `Z`-basis measurement flips the result." + "Independent `X` on each qubit (p = 0.1)." ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 13, "id": "57943533", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, One]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[Zero, Zero]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "73c0f33ec76c45a8868d90c62b14309f", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# Independent X errors on qubits 0 and 1, each with probability 0.1.\n", - "# Expect ~10% of each qubit to flip to 1, independently.\n", - "xyz_circuit = \"\"\"X_ERROR(0.1) 0 1\n", + "xerr = \"\"\"X_ERROR(0.1) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "\n", - "results = stim.run(xyz_circuit, shots=20, type=\"cpu\")\n", - "\n", - "for r in results: print(r)" + "Histogram([\"\".join(map(str, s)) for s in stim.run(xerr, shots=5000, type=\"clifford\")])" ] }, { @@ -370,55 +252,38 @@ "id": "8a236142", "metadata": {}, "source": [ - "### Qubit loss: `LOSS_ERROR`\n", + "### Loss\n", "\n", - "`LOSS_ERROR` independently loses each target qubit with the given probability.\n", - "A lost qubit is reported as `L` in the measurement results (rather than `0`/`1`)." + "Lost qubits show up as `L` (p = 0.15)." ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 17, "id": "a10524be", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "dc2641d6ae28493b81c2e2dffeca0e95", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# Independent loss on qubits 0 and 1, each with probability 0.15.\n", - "# Lost qubits show up as `L`; otherwise the qubit measures 0.\n", - "loss_circuit = \"\"\"LOSS_ERROR(0.15) 0 1\n", + "loss = \"\"\"LOSS_ERROR(0.15) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "\n", - "results = stim.run(loss_circuit, shots=20, type=\"cpu\")\n", - "\n", - "for r in results: print(r)" + "Histogram([\"\".join(map(str, s)) for s in stim.run(loss, shots=5000, type=\"clifford\")])" ] }, { @@ -426,57 +291,40 @@ "id": "0d45640f", "metadata": {}, "source": [ - "### Loss inside a correlated error\n", + "### Loss in a correlated error\n", "\n", - "A `CORRELATED_ERROR` chain can mix Pauli terms (`X`/`Y`/`Z`) and loss terms\n", - "(`L`) in the same fault. Here the mutually-exclusive branches lose qubit 0,\n", - "lose qubit 1, or lose qubit 0 while applying `Z` to qubit 1." + "Branches mix loss (`L`) and Pauli (`X`) terms." ] }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 21, "id": "5aadceb8", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Loss, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Loss, Zero]\n", - "[Zero, Loss]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Loss]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Loss, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e52a48b82ef849dd9c03a4b59f27459a", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# Mutually-exclusive correlated branches mixing loss and Pauli terms.\n", - "mixed_circuit = \"\"\"CORRELATED_ERROR(0.1) L0\n", + "mixed = \"\"\"CORRELATED_ERROR(0.1) L0\n", "ELSE_CORRELATED_ERROR(0.1) L1\n", - "ELSE_CORRELATED_ERROR(0.1) L0 Z1\n", + "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", "MR 0 1\n", "\"\"\"\n", - "\n", - "results = stim.run(mixed_circuit, shots=20, type=\"cpu\")\n", - "\n", - "for r in results: print(r)" + "Histogram([\"\".join(map(str, s)) for s in stim.run(mixed, shots=5000, type=\"clifford\")])" ] }, { @@ -484,86 +332,67 @@ "id": "1c40106e", "metadata": {}, "source": [ - "### Depolarizing noise: `DEPOLARIZE1`, `DEPOLARIZE2`\n", + "### Depolarizing\n", "\n", - "`DEPOLARIZE1(p)` applies one of the 3 single-qubit Paulis (each with\n", - "probability `p/3`) independently to each target. `DEPOLARIZE2(p)` applies one\n", - "of the 15 non-identity two-qubit Paulis (each with probability `p/15`) to each\n", - "qubit pair." + "`DEPOLARIZE1(p)`: one of 3 Paulis per qubit. `DEPOLARIZE2(p)`: one of 15 two-qubit Paulis." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "10a92131", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "DEPOLARIZE1(0.3):\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, One]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, Zero]\n", - "[Zero, One]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, One]\n", - "[Zero, One]\n", - "[One, One]\n", - "[Zero, Zero]\n", - "[Zero, One]\n", - "[Zero, Zero]\n", - "\n", - "DEPOLARIZE2(0.3):\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, One]\n", - "[Zero, Zero]\n", - "[Zero, One]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[Zero, Zero]\n", - "[One, One]\n", - "[One, One]\n", - "[One, Zero]\n", - "[One, Zero]\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c01d971b721c4e14bdac1440274e53e9", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# DEPOLARIZE1: single-qubit depolarizing on qubits 0 and 1.\n", - "# In the Z basis, X and Y components flip the measurement, so ~2/3 * p flip.\n", - "print(\"DEPOLARIZE1(0.3):\")\n", - "depolarize1_circuit = \"\"\"DEPOLARIZE1(0.3) 0 1\n", + "dep1 = \"\"\"DEPOLARIZE1(0.3) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "for r in stim.run(depolarize1_circuit, shots=20, type=\"cpu\"):\n", - " print(r)\n", - "\n", - "print(\"\\nDEPOLARIZE2(0.3):\")\n", - "depolarize2_circuit = \"\"\"DEPOLARIZE2(0.3) 0 1\n", + "Histogram([\"\".join(map(str, s)) for s in stim.run(dep1, shots=5000, type=\"clifford\")])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "c65f6416", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a2366083fb0341e4b191e5db0f401658", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dep2 = \"\"\"DEPOLARIZE2(0.3) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "\n", - "for r in stim.run(depolarize2_circuit, shots=20, type=\"cpu\"): print(r)" + "Histogram([\"\".join(map(str, s)) for s in stim.run(dep2, shots=5000, type=\"clifford\")])" ] } ], From 4a92c662bca33f2c9af6e00f325c0365f430909f Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 09:44:29 -0700 Subject: [PATCH 40/67] improve write_call api --- source/compiler/qsc_stim_parser/src/qir.rs | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index a38e2af228b..871a8b0ce6f 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -34,7 +34,17 @@ impl QirWriter { } } - // Writes: ` call void @__quantum__qis__{intrinsic}__body(ptr inttoptr (i64 N to ptr), ...)` + /// `__quantum__qis__{intrinsic}__body` + fn write_qis_call(&mut self, intrinsic: &str, operands: &[Operand]) { + self.write_raw_call(&format!("__quantum__qis__{intrinsic}__body"), operands); + } + + /// `__quantum__qis__{intrinsic}__adj` + fn write_qis_adj_call(&mut self, intrinsic: &str, operands: &[Operand]) { + self.write_raw_call(&format!("__quantum__qis__{intrinsic}__adj"), operands); + } + + // Writes: ` call void @{intrinsic}(ptr inttoptr (i64 N to ptr), ...)` // Resolves qubit indices via the qubit map and allocates result IDs internally. fn write_call(&mut self, intrinsic: &str, operands: &[Operand]) { write!( @@ -55,8 +65,8 @@ impl QirWriter { .collect::>() .join(", "); self.used_intrinsics - .entry(name.clone()) - .or_insert_with(|| format!("declare void @{name}({params})")); + .entry(intrinsic.to_string()) + .or_insert_with(|| format!("declare void @{intrinsic}({params})")); } fn write_noise_intrinsic(&mut self, name: &str, qubits: &[u32]) { @@ -388,7 +398,7 @@ impl<'noise> Compiler<'noise> { let TargetKind::Qubit { value, .. } = target.kind else { continue; }; - self.writer.write_call(&gate, &[Operand::Qubit(value)]); + self.writer.write_qis_call(&gate, &[Operand::Qubit(value)]); } } @@ -399,7 +409,7 @@ impl<'noise> Compiler<'noise> { let TargetKind::Qubit { value, .. } = target.kind else { continue; }; - self.writer.write_call(&gate, &[Operand::Qubit(value)]); + self.writer.write_qis_call(&gate, &[Operand::Qubit(value)]); } } else if gate == "sqrt_x" { // decomposed into H S H @@ -606,7 +616,8 @@ impl<'noise> Compiler<'noise> { let TargetKind::Qubit { value, .. } = target.kind else { continue; }; - self.writer.write_call("reset", &[Operand::Qubit(value)]); + self.writer + .write_qis_call("reset", &[Operand::Qubit(value)]); } } else if gate == "mr" { for target in &instruction.targets { @@ -614,7 +625,7 @@ impl<'noise> Compiler<'noise> { continue; }; self.writer - .write_call("mresetz", &[Operand::Qubit(value), Operand::Result]); + .write_qis_call("mresetz", &[Operand::Qubit(value), Operand::Result]); } } else if gate == "mrx" { // decomposed into H MRZ H @@ -623,9 +634,9 @@ impl<'noise> Compiler<'noise> { continue; }; let q = Operand::Qubit(value); - self.writer.write_call("h", &[q]); - self.writer.write_call("mresetz", &[q, Operand::Result]); - self.writer.write_call("h", &[q]); + self.writer.write_qis_call("h", &[q]); + self.writer.write_qis_call("mresetz", &[q, Operand::Result]); + self.writer.write_qis_call("h", &[q]); } } } From e492edb4959a8c28a5b88dc4819f17408e346442 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 09:45:01 -0700 Subject: [PATCH 41/67] support m, swap, s_dag qir generation --- source/compiler/qsc_stim_parser/src/qir.rs | 41 +++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 871a8b0ce6f..31bd01d2fc8 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -46,12 +46,8 @@ impl QirWriter { // Writes: ` call void @{intrinsic}(ptr inttoptr (i64 N to ptr), ...)` // Resolves qubit indices via the qubit map and allocates result IDs internally. - fn write_call(&mut self, intrinsic: &str, operands: &[Operand]) { - write!( - self.output, - " call void @__quantum__qis__{intrinsic}__body(" - ) - .unwrap(); + fn write_raw_call(&mut self, intrinsic: &str, operands: &[Operand]) { + write!(self.output, " call void @{intrinsic}(").unwrap(); for (i, &operand) in operands.iter().enumerate() { if i > 0 { write!(self.output, ", ").unwrap(); @@ -59,7 +55,6 @@ impl QirWriter { self.write_operand(operand); } writeln!(self.output, ")").unwrap(); - let name = format!("__quantum__qis__{intrinsic}__body"); let params = (0..operands.len()) .map(|_| "ptr") .collect::>() @@ -418,7 +413,15 @@ impl<'noise> Compiler<'noise> { continue; }; let q = Operand::Qubit(value); - self.writer.write_call("sx", &[q]); + self.writer.write_qis_call("sx", &[q]); + } + } else if gate == "s_dag" { + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer + .write_qis_adj_call("s", &[Operand::Qubit(value)]); } } } @@ -435,7 +438,19 @@ impl<'noise> Compiler<'noise> { continue; }; self.writer - .write_call(&gate, &[Operand::Qubit(v0), Operand::Qubit(v1)]); + .write_qis_call(&gate, &[Operand::Qubit(v0), Operand::Qubit(v1)]); + } + } else if gate == "swap" { + let targets = &instruction.targets; + for pair in targets.chunks(2) { + let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { + continue; + }; + let TargetKind::Qubit { value: v1, .. } = pair[1].kind else { + continue; + }; + self.writer + .write_qis_call("swap", &[Operand::Qubit(v0), Operand::Qubit(v1)]); } } } @@ -619,6 +634,14 @@ impl<'noise> Compiler<'noise> { self.writer .write_qis_call("reset", &[Operand::Qubit(value)]); } + } else if gate == "m" { + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer + .write_qis_call("m", &[Operand::Qubit(value), Operand::Result]); + } } else if gate == "mr" { for target in &instruction.targets { let TargetKind::Qubit { value, .. } = target.kind else { From 438f3c3f892c59822b8f812c4e2ad3af31f1ae27 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 09:58:16 -0700 Subject: [PATCH 42/67] minor fixes on stim_to_qir notebook --- samples/notebooks/stim_to_qir.ipynb | 224 ++++------------------------ 1 file changed, 27 insertions(+), 197 deletions(-) diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb index 3e36e8ea899..7ec4d48bfa5 100644 --- a/samples/notebooks/stim_to_qir.ipynb +++ b/samples/notebooks/stim_to_qir.ipynb @@ -15,7 +15,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "6c9df231", "metadata": {}, "outputs": [], @@ -36,52 +36,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a41fe64d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "define i64 @ENTRYPOINT__main() #0 {\n", - " call void @__quantum__rt__initialize(ptr null)\n", - " call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr))\n", - " call void @__quantum__qis__cx__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr))\n", - " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))\n", - " call void @__quantum__qis__mresetz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr))\n", - " call void @__quantum__rt__array_record_output(i64 2, ptr null)\n", - " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null)\n", - " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null)\n", - " ret i64 0\n", - "}\n", - "\n", - "declare void @__quantum__qis__cx__body(ptr, ptr)\n", - "declare void @__quantum__qis__mresetz__body(ptr, ptr)\n", - "declare void @__quantum__rt__result_record_output(ptr, ptr)\n", - "declare void @__quantum__rt__array_record_output(i64, ptr)\n", - "declare void @__quantum__rt__initialize(ptr)\n", - "declare void @__quantum__qis__h__body(ptr)\n", - "\n", - "attributes #0 = { \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"2\" \"required_num_results\"=\"2\" }\n", - "attributes #1 = { \"irreversible\" }\n", - "\n", - "; module flags\n", - "\n", - "!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}\n", - "\n", - "!0 = !{i32 1, !\"qir_major_version\", i32 2}\n", - "!1 = !{i32 7, !\"qir_minor_version\", i32 1}\n", - "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 false}\n", - "!3 = !{i32 1, !\"dynamic_result_management\", i1 false}\n", - "!4 = !{i32 5, !\"int_computations\", !{!\"i64\"}}\n", - "!5 = !{i32 5, !\"float_computations\", !{!\"double\"}}\n", - "!6 = !{i32 7, !\"backwards_branching\", i2 3}\n", - "!7 = !{i32 1, !\"arrays\", i1 true}\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "bell = \"\"\"H 0\n", "CX 0 1\n", @@ -94,29 +52,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "d859fe1a", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8aec9a404d224ea3b6e6fa15311cbccb", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Entangled: only 00 and 11 appear.\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(bell, shots=2000, type=\"clifford\")])" + "Histogram(stim.run(bell, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { @@ -131,26 +73,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "452e15ec", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "530e5e728ace4abbbf749aa38862749a", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Force the measurement to 1: every shot reports 1.\n", "preselect = \"\"\"#!preselect_begin\n", @@ -158,7 +84,7 @@ "MR 0\n", "#!preselect_expect 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(preselect, shots=2000, type=\"clifford\")])" + "Histogram(stim.run(preselect, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { @@ -181,31 +107,15 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "0c2b7a13", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "85dfea9eb96f45a3a661fd6ee1b9f768", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "correlated = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", "MR 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(correlated, shots=5000, type=\"clifford\")])" + "Histogram(stim.run(correlated, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { @@ -220,31 +130,15 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "57943533", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "73c0f33ec76c45a8868d90c62b14309f", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "xerr = \"\"\"X_ERROR(0.1) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(xerr, shots=5000, type=\"clifford\")])" + "Histogram(stim.run(xerr, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { @@ -259,31 +153,15 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "a10524be", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "dc2641d6ae28493b81c2e2dffeca0e95", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "loss = \"\"\"LOSS_ERROR(0.15) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(loss, shots=5000, type=\"clifford\")])" + "Histogram(stim.run(loss, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { @@ -298,33 +176,17 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "5aadceb8", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e52a48b82ef849dd9c03a4b59f27459a", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "mixed = \"\"\"CORRELATED_ERROR(0.1) L0\n", "ELSE_CORRELATED_ERROR(0.1) L1\n", "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", "MR 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(mixed, shots=5000, type=\"clifford\")])" + "Histogram(stim.run(mixed, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { @@ -339,60 +201,28 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "10a92131", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c01d971b721c4e14bdac1440274e53e9", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dep1 = \"\"\"DEPOLARIZE1(0.3) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(dep1, shots=5000, type=\"clifford\")])" + "Histogram(stim.run(dep1, shots=2000, type=\"clifford\"), labels=\"kets\")" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "c65f6416", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a2366083fb0341e4b191e5db0f401658", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dep2 = \"\"\"DEPOLARIZE2(0.3) 0 1\n", "MR 0 1\n", "\"\"\"\n", - "Histogram([\"\".join(map(str, s)) for s in stim.run(dep2, shots=5000, type=\"clifford\")])" + "Histogram(stim.run(dep2, shots=2000, type=\"clifford\"), labels=\"kets\")" ] } ], From d4df93237f775a5352176721c76b54420ce1c710 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 09:59:02 -0700 Subject: [PATCH 43/67] clear metadata from notebook --- samples/notebooks/stim_to_qir.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb index 7ec4d48bfa5..46ad80b410b 100644 --- a/samples/notebooks/stim_to_qir.ipynb +++ b/samples/notebooks/stim_to_qir.ipynb @@ -221,6 +221,7 @@ "source": [ "dep2 = \"\"\"DEPOLARIZE2(0.3) 0 1\n", "MR 0 1\n", + "\n", "\"\"\"\n", "Histogram(stim.run(dep2, shots=2000, type=\"clifford\"), labels=\"kets\")" ] From 44f52b7af789abd829fd6b5835f5958de1af35e3 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 10:00:37 -0700 Subject: [PATCH 44/67] cargo lock --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index d67ec0e1441..31a45c8a658 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2524,6 +2524,7 @@ name = "qsc_stim_parser" version = "0.0.0" dependencies = [ "enum-iterator", + "qdk_simulators", "qsc_data_structures", "rustc-hash", ] From 2f31d2e4fcb4ddb507794cebcbbd57bd8e7f4d19 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 10:39:27 -0700 Subject: [PATCH 45/67] fix widgets import --- samples/notebooks/stim_to_qir.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb index 46ad80b410b..954406f2767 100644 --- a/samples/notebooks/stim_to_qir.ipynb +++ b/samples/notebooks/stim_to_qir.ipynb @@ -21,7 +21,7 @@ "outputs": [], "source": [ "from qdk import stim\n", - "from qsharp_widgets import Histogram" + "from qdk.widgets import Histogram" ] }, { From 533017ae8b54bbc526b5d63991f5a056ba50ffd8 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 16 Jun 2026 10:44:11 -0700 Subject: [PATCH 46/67] give default value of None to NoiseConfig in compile stim --- source/qdk_package/qdk/stim/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/qdk_package/qdk/stim/__init__.py b/source/qdk_package/qdk/stim/__init__.py index a9d165e98b1..a2f205e57dc 100644 --- a/source/qdk_package/qdk/stim/__init__.py +++ b/source/qdk_package/qdk/stim/__init__.py @@ -3,7 +3,7 @@ from typing import List, Literal, Optional, Tuple -def compile(src: str, noise: Optional[NoiseConfig]) -> Tuple[str, NoiseConfig]: +def compile(src: str, noise: Optional[NoiseConfig] = None) -> Tuple[str, NoiseConfig]: return compile_stim_to_qir(src, noise) From 29eeb98b6591ea3481065958abde88d3f5139a00 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 17 Jun 2026 10:47:04 -0700 Subject: [PATCH 47/67] simplify instruction compilation --- source/compiler/qsc_stim_parser/src/qir.rs | 155 ++++++++------------- 1 file changed, 58 insertions(+), 97 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 31bd01d2fc8..f1c39623a8d 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -252,19 +252,6 @@ impl QirWriter { } } -enum InstructionKind { - PauliGate, - SingleQubitCliffordGate, - TwoQubitCliffordGate, - NoiseChannel, - CollapsingGate, - PairMeasurementGate, - GeneralizedPauliProductGate, - ControlFlow, - Annotations, - CustomInstruction, -} - /// A single fault term (`X`, `Y`, `Z`, or `L`) applied to a qubit. #[derive(Clone, Copy)] enum FaultChar { @@ -353,37 +340,64 @@ impl<'noise> Compiler<'noise> { self.finish_correlated_group(); } - match Self::instruction_kind(&instruction.name) { - InstructionKind::PauliGate => { - self.compile_pauli_gate(instruction); - } - InstructionKind::SingleQubitCliffordGate => { - self.compile_single_qubit_clifford_gate(instruction); - } - InstructionKind::TwoQubitCliffordGate => { - self.compile_two_qubit_clifford_gate(instruction); - } - InstructionKind::NoiseChannel => { - self.compile_noise_channel(instruction); - } - InstructionKind::CollapsingGate => { - self.compile_collapsing_gate(instruction); - } - InstructionKind::PairMeasurementGate => { - self.compile_pair_measurement_gate(instruction); - } - InstructionKind::GeneralizedPauliProductGate => { - self.compile_generalized_pauli_product_gate(instruction); - } - InstructionKind::ControlFlow => { - self.compile_control_flow(instruction); + match instruction.name.as_str() { + // Pauli Gates + "I" | "X" | "Y" | "Z" => self.compile_pauli_gate(instruction), + + // Single Qubit Clifford Gates + "C_NXYZ" | "C_NZYX" | "C_XNYZ" | "C_XYNZ" | "C_XYZ" | "C_ZNYX" | "C_ZYNX" | "C_ZYX" + | "H" | "H_NXY" | "H_NXZ" | "H_NYZ" | "H_XY" | "H_XZ" | "H_YZ" | "S" | "SQRT_X" + | "SQRT_X_DAG" | "SQRT_Y" | "SQRT_Y_DAG" | "SQRT_Z" | "SQRT_Z_DAG" | "S_DAG" => { + self.compile_single_qubit_clifford_gate(instruction) } - InstructionKind::Annotations => { - self.compile_annotations(instruction); + + // Two Qubit Clifford Gates + "CNOT" | "CX" | "CXSWAP" | "CY" | "CZ" | "CZSWAP" | "II" | "ISWAP" | "ISWAP_DAG" + | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" | "SQRT_ZZ" | "SQRT_ZZ_DAG" + | "SWAP" | "SWAPCX" | "SWAPCZ" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" + | "ZCX" | "ZCY" | "ZCZ" => self.compile_two_qubit_clifford_gate(instruction), + + // Noise Channels + "CORRELATED_ERROR" + | "DEPOLARIZE1" + | "DEPOLARIZE2" + | "E" + | "ELSE_CORRELATED_ERROR" + | "HERALDED_ERASE" + | "HERALDED_PAULI_CHANNEL_1" + | "II_ERROR" + | "I_ERROR" + | "PAULI_CHANNEL_1" + | "PAULI_CHANNEL_2" + | "X_ERROR" + | "Y_ERROR" + | "Z_ERROR" + | "LOSS_ERROR" => self.compile_noise_channel(instruction), + + // Collapsing Gates + "M" | "MR" | "MRX" | "MRY" | "MRZ" | "MX" | "MY" | "MZ" | "R" | "RX" | "RY" | "RZ" => { + self.compile_collapsing_gate(instruction) } - InstructionKind::CustomInstruction => { - self.compile_custom_instruction(instruction); + + // Pair Measurement Gates + "MXX" | "MYY" | "MZZ" => self.compile_pair_measurement_gate(instruction), + + // Generalized Pauli Product Gates + "MPP" | "SPP" | "SPP_DAG" => self.compile_generalized_pauli_product_gate(instruction), + + // Control Flow + "REPEAT" => self.compile_control_flow(instruction), + + // Annotations + "DETECTOR" | "MPAD" | "OBSERVABLE_INCLUDE" | "QUBIT_COORDS" | "SHIFT_COORDS" + | "TICK" => self.compile_annotations(instruction), + + // Custom Instructions + "#!preselect_begin" | "#!preselect_expect" | "#!rhai" => { + self.compile_custom_instruction(instruction) } + + _ => self.unsupported(instruction), } } @@ -725,62 +739,9 @@ impl<'noise> Compiler<'noise> { } } - fn instruction_kind(name: &str) -> InstructionKind { - match name { - // Pauli Gates - "I" | "X" | "Y" | "Z" => InstructionKind::PauliGate, - - // Single Qubit Clifford Gates - "C_NXYZ" | "C_NZYX" | "C_XNYZ" | "C_XYNZ" | "C_XYZ" | "C_ZNYX" | "C_ZYNX" | "C_ZYX" - | "H" | "H_NXY" | "H_NXZ" | "H_NYZ" | "H_XY" | "H_XZ" | "H_YZ" | "S" | "SQRT_X" - | "SQRT_X_DAG" | "SQRT_Y" | "SQRT_Y_DAG" | "SQRT_Z" | "SQRT_Z_DAG" | "S_DAG" => { - InstructionKind::SingleQubitCliffordGate - } - - // Two Qubit Clifford Gates - "CNOT" | "CX" | "CXSWAP" | "CY" | "CZ" | "CZSWAP" | "II" | "ISWAP" | "ISWAP_DAG" - | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" | "SQRT_ZZ" | "SQRT_ZZ_DAG" - | "SWAP" | "SWAPCX" | "SWAPCZ" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" - | "ZCX" | "ZCY" | "ZCZ" => InstructionKind::TwoQubitCliffordGate, - - // Noise Channels - "CORRELATED_ERROR" - | "DEPOLARIZE1" - | "DEPOLARIZE2" - | "E" - | "ELSE_CORRELATED_ERROR" - | "HERALDED_ERASE" - | "HERALDED_PAULI_CHANNEL_1" - | "II_ERROR" - | "I_ERROR" - | "PAULI_CHANNEL_1" - | "PAULI_CHANNEL_2" - | "X_ERROR" - | "Y_ERROR" - | "Z_ERROR" - | "LOSS_ERROR" => InstructionKind::NoiseChannel, - - // Collapsing Gates - "M" | "MR" | "MRX" | "MRY" | "MRZ" | "MX" | "MY" | "MZ" | "R" | "RX" | "RY" | "RZ" => { - InstructionKind::CollapsingGate - } - - // Pair Measurement Gates - "MXX" | "MYY" | "MZZ" => InstructionKind::PairMeasurementGate, - - // Generalized Pauli Product Gates - "MPP" | "SPP" | "SPP_DAG" => InstructionKind::GeneralizedPauliProductGate, - - // Control Flow - "REPEAT" => InstructionKind::ControlFlow, - - // Annotations - "DETECTOR" | "MPAD" | "OBSERVABLE_INCLUDE" | "QUBIT_COORDS" | "SHIFT_COORDS" - | "TICK" => InstructionKind::Annotations, - - "#!preselect_begin" | "#!preselect_expect" => InstructionKind::CustomInstruction, - _ => InstructionKind::CustomInstruction, - } + fn unsupported(&mut self, instruction: &Instruction) { + // TODO: IMPROVE ERROR HANDLING + panic!("Unsupported instruction: {}", instruction.name); } fn into_qir(mut self, circuit: &Circuit) -> String { From 96e48b2eb2b3eff131cc25703f515052999f2890 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 17 Jun 2026 11:16:38 -0700 Subject: [PATCH 48/67] simplify instruction compilation, support more gates --- source/compiler/qsc_stim_parser/src/qir.rs | 497 ++++++++++----------- 1 file changed, 229 insertions(+), 268 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index f1c39623a8d..fbb3e459da7 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -342,225 +342,287 @@ impl<'noise> Compiler<'noise> { match instruction.name.as_str() { // Pauli Gates - "I" | "X" | "Y" | "Z" => self.compile_pauli_gate(instruction), + "I" => (), + "X" | "Y" | "Z" => self.emit_single(instruction, &instruction.name.to_lowercase()), // Single Qubit Clifford Gates - "C_NXYZ" | "C_NZYX" | "C_XNYZ" | "C_XYNZ" | "C_XYZ" | "C_ZNYX" | "C_ZYNX" | "C_ZYX" - | "H" | "H_NXY" | "H_NXZ" | "H_NYZ" | "H_XY" | "H_XZ" | "H_YZ" | "S" | "SQRT_X" - | "SQRT_X_DAG" | "SQRT_Y" | "SQRT_Y_DAG" | "SQRT_Z" | "SQRT_Z_DAG" | "S_DAG" => { - self.compile_single_qubit_clifford_gate(instruction) + "H" => self.emit_single(instruction, &instruction.name.to_lowercase()), + "S" | "SQRT_Z" => self.emit_single(instruction, "s"), + "S_DAG" | "SQRT_Z_DAG" => self.emit_single_adj(instruction, "s"), + "SQRT_X" => self.emit_single(instruction, "sx"), + "SQRT_X_DAG" => { + self.emit_single(instruction, "s"); + self.emit_single(instruction, "h"); + self.emit_single(instruction, "s"); + } + "SQRT_Y" => { + self.emit_single(instruction, "s"); + self.emit_single(instruction, "s"); + self.emit_single(instruction, "h"); + } + "SQRT_Y_DAG" => { + self.emit_single(instruction, "h"); + self.emit_single(instruction, "s"); + self.emit_single(instruction, "s"); } // Two Qubit Clifford Gates - "CNOT" | "CX" | "CXSWAP" | "CY" | "CZ" | "CZSWAP" | "II" | "ISWAP" | "ISWAP_DAG" - | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" | "SQRT_ZZ" | "SQRT_ZZ_DAG" - | "SWAP" | "SWAPCX" | "SWAPCZ" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" - | "ZCX" | "ZCY" | "ZCZ" => self.compile_two_qubit_clifford_gate(instruction), + "II" => (), + "CX" | "CY" | "CZ" | "SWAP" => { + self.emit_pair(instruction, &instruction.name.to_lowercase()) + } // Noise Channels - "CORRELATED_ERROR" - | "DEPOLARIZE1" - | "DEPOLARIZE2" - | "E" - | "ELSE_CORRELATED_ERROR" - | "HERALDED_ERASE" - | "HERALDED_PAULI_CHANNEL_1" - | "II_ERROR" - | "I_ERROR" - | "PAULI_CHANNEL_1" - | "PAULI_CHANNEL_2" - | "X_ERROR" - | "Y_ERROR" - | "Z_ERROR" - | "LOSS_ERROR" => self.compile_noise_channel(instruction), + "CORRELATED_ERROR" | "ELSE_CORRELATED_ERROR" => { + self.accumulate_correlated_error(instruction) + } + + "X_ERROR" | "Y_ERROR" | "Z_ERROR" | "LOSS_ERROR" => { + self.compile_fault_error(instruction) + } + + "DEPOLARIZE1" => self.compile_depolarize_1(instruction), + "DEPOLARIZE2" => self.compile_depolarize_2(instruction), // Collapsing Gates - "M" | "MR" | "MRX" | "MRY" | "MRZ" | "MX" | "MY" | "MZ" | "R" | "RX" | "RY" | "RZ" => { - self.compile_collapsing_gate(instruction) + "R" | "RZ" => self.emit_single(instruction, "reset"), + "RX" => { + self.emit_single(instruction, "reset"); // RZ + self.emit_single(instruction, "h"); // Z -> X + } + "RY" => { + self.emit_single(instruction, "reset"); // RZ + self.emit_single(instruction, "h"); // Z -> X + self.emit_single(instruction, "s"); // X -> Y + } + "M" | "MZ" => self.emit_measure(instruction, "m"), + "MX" => { + self.emit_single(instruction, "h"); // X -> Z + self.emit_measure(instruction, "m"); // MZ + self.emit_single(instruction, "h"); // Z -> X + } + "MY" => { + self.emit_single_adj(instruction, "s"); // Y -> X + self.emit_single(instruction, "h"); // X -> Z + self.emit_measure(instruction, "m"); // MZ + self.emit_single(instruction, "h"); // Z -> X + self.emit_single(instruction, "s"); // X -> Y + } + "MR" | "MRZ" => self.emit_measure(instruction, "mresetz"), + "MRX" => { + self.emit_single(instruction, "h"); // X -> Z + self.emit_measure(instruction, "mresetz"); // MRZ + self.emit_single(instruction, "h"); // Z -> X + } + "MRY" => { + self.emit_single_adj(instruction, "s"); // Y -> X + self.emit_single(instruction, "h"); // X -> Z + self.emit_measure(instruction, "mresetz"); // MRZ + self.emit_single(instruction, "h"); // Z -> X + self.emit_single(instruction, "s"); // X -> Y } // Pair Measurement Gates - "MXX" | "MYY" | "MZZ" => self.compile_pair_measurement_gate(instruction), // Generalized Pauli Product Gates - "MPP" | "SPP" | "SPP_DAG" => self.compile_generalized_pauli_product_gate(instruction), // Control Flow - "REPEAT" => self.compile_control_flow(instruction), // Annotations - "DETECTOR" | "MPAD" | "OBSERVABLE_INCLUDE" | "QUBIT_COORDS" | "SHIFT_COORDS" - | "TICK" => self.compile_annotations(instruction), // Custom Instructions - "#!preselect_begin" | "#!preselect_expect" | "#!rhai" => { - self.compile_custom_instruction(instruction) - } + "!rhai" => (), + "#!preselect_begin" => self.compile_preselect_begin(), + "#!preselect_expect" => self.compile_preselect_expect(instruction), _ => self.unsupported(instruction), } } - fn compile_pauli_gate(&mut self, instruction: &Instruction) { - let gate = instruction.name.to_lowercase(); + fn emit_single(&mut self, instruction: &Instruction, intrinsic: &str) { for target in &instruction.targets { let TargetKind::Qubit { value, .. } = target.kind else { continue; }; - self.writer.write_qis_call(&gate, &[Operand::Qubit(value)]); + self.writer + .write_qis_call(intrinsic, &[Operand::Qubit(value)]); } } - fn compile_single_qubit_clifford_gate(&mut self, instruction: &Instruction) { - let gate = instruction.name.to_lowercase(); - if gate == "h" || gate == "s" { - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - self.writer.write_qis_call(&gate, &[Operand::Qubit(value)]); - } - } else if gate == "sqrt_x" { - // decomposed into H S H - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - let q = Operand::Qubit(value); - self.writer.write_qis_call("sx", &[q]); - } - } else if gate == "s_dag" { - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - self.writer - .write_qis_adj_call("s", &[Operand::Qubit(value)]); - } + fn emit_single_adj(&mut self, instruction: &Instruction, intrinsic: &str) { + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer + .write_qis_adj_call(intrinsic, &[Operand::Qubit(value)]); } } - fn compile_two_qubit_clifford_gate(&mut self, instruction: &Instruction) { - let gate = instruction.name.to_lowercase(); - if gate == "cz" || gate == "cx" || gate == "cy" { - let targets = &instruction.targets; - for pair in targets.chunks(2) { - let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { - continue; - }; - let TargetKind::Qubit { value: v1, .. } = pair[1].kind else { - continue; - }; - self.writer - .write_qis_call(&gate, &[Operand::Qubit(v0), Operand::Qubit(v1)]); - } - } else if gate == "swap" { - let targets = &instruction.targets; - for pair in targets.chunks(2) { - let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { - continue; - }; - let TargetKind::Qubit { value: v1, .. } = pair[1].kind else { - continue; - }; - self.writer - .write_qis_call("swap", &[Operand::Qubit(v0), Operand::Qubit(v1)]); - } + fn emit_pair(&mut self, instruction: &Instruction, intrinsic: &str) { + let targets = &instruction.targets; + for pair in targets.chunks(2) { + let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { + continue; + }; + let TargetKind::Qubit { value: v1, .. } = pair[1].kind else { + continue; + }; + self.writer + .write_qis_call(intrinsic, &[Operand::Qubit(v0), Operand::Qubit(v1)]); } } - fn compile_noise_channel(&mut self, instruction: &Instruction) { + fn compile_fault_error(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); - if gate == "correlated_error" || gate == "else_correlated_error" { - self.accumulate_correlated_error(instruction); - } else if gate == "x_error" - || gate == "y_error" - || gate == "z_error" - || gate == "loss_error" - { - let fault = match gate.as_str() { - "x_error" => FaultChar::X, - "y_error" => FaultChar::Y, - "z_error" => FaultChar::Z, - _ => FaultChar::Loss, + let fault = match gate.as_str() { + "x_error" => FaultChar::X, + "y_error" => FaultChar::Y, + "z_error" => FaultChar::Z, + _ => FaultChar::Loss, + }; + let probability = instruction.args[0]; + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.current_correlated_group + .get_or_insert_with(CorrelatedGroup::default) + .rows + .push(CorrelatedRow { + terms: vec![(value, fault)], + probability, + }); + self.finish_correlated_group(); // one independent table per qubit + } + } + + fn compile_depolarize_1(&mut self, instruction: &Instruction) { + let each = instruction.args[0] / 3.0; + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; }; - let probability = instruction.args[0]; - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - self.current_correlated_group - .get_or_insert_with(CorrelatedGroup::default) - .rows - .push(CorrelatedRow { + { + let group = self + .current_correlated_group + .get_or_insert_with(CorrelatedGroup::default); + for fault in [FaultChar::X, FaultChar::Y, FaultChar::Z] { + group.rows.push(CorrelatedRow { terms: vec![(value, fault)], - probability, + probability: each, }); - self.finish_correlated_group(); // one independent table per qubit + } } - } else if gate == "depolarize1" { - let each = instruction.args[0] / 3.0; - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - { - let group = self - .current_correlated_group - .get_or_insert_with(CorrelatedGroup::default); - for fault in [FaultChar::X, FaultChar::Y, FaultChar::Z] { + self.finish_correlated_group(); // one independent 1-qubit table per target + } + } + + fn compile_depolarize_2(&mut self, instruction: &Instruction) { + let each = instruction.args[0] / 15.0; + for pair in instruction.targets.chunks(2) { + let TargetKind::Qubit { value: q0, .. } = pair[0].kind else { + continue; + }; + let TargetKind::Qubit { value: q1, .. } = pair[1].kind else { + continue; + }; + { + let group = self + .current_correlated_group + .get_or_insert_with(CorrelatedGroup::default); + // All 16 (p0, p1) combos except (I, I); None means identity on that qubit. + let options = [ + None, + Some(FaultChar::X), + Some(FaultChar::Y), + Some(FaultChar::Z), + ]; + for p0 in options { + for p1 in options { + if p0.is_none() && p1.is_none() { + continue; // skip identity + } + let mut terms = Vec::new(); + if let Some(f) = p0 { + terms.push((q0, f)); + } + if let Some(f) = p1 { + terms.push((q1, f)); + } group.rows.push(CorrelatedRow { - terms: vec![(value, fault)], + terms, probability: each, }); } } - self.finish_correlated_group(); // one independent 1-qubit table per target - } - } else if gate == "depolarize2" { - let each = instruction.args[0] / 15.0; - for pair in instruction.targets.chunks(2) { - let TargetKind::Qubit { value: q0, .. } = pair[0].kind else { - continue; - }; - let TargetKind::Qubit { value: q1, .. } = pair[1].kind else { - continue; - }; - { - let group = self - .current_correlated_group - .get_or_insert_with(CorrelatedGroup::default); - // All 16 (p0, p1) combos except (I, I); None means identity on that qubit. - let options = [ - None, - Some(FaultChar::X), - Some(FaultChar::Y), - Some(FaultChar::Z), - ]; - for p0 in options { - for p1 in options { - if p0.is_none() && p1.is_none() { - continue; // skip identity - } - let mut terms = Vec::new(); - if let Some(f) = p0 { - terms.push((q0, f)); - } - if let Some(f) = p1 { - terms.push((q1, f)); - } - group.rows.push(CorrelatedRow { - terms, - probability: each, - }); - } - } - } - self.finish_correlated_group(); // one independent 2-qubit table per pair } + self.finish_correlated_group(); // one independent 2-qubit table per pair + } + } + + fn emit_measure(&mut self, instruction: &Instruction, intrinsic: &str) { + for target in &instruction.targets { + let TargetKind::Qubit { value, .. } = target.kind else { + continue; + }; + self.writer + .write_qis_call(intrinsic, &[Operand::Qubit(value), Operand::Result]); } } + fn compile_preselect_begin(&mut self) { + self.last_preselect_begin = match self.last_preselect_begin { + None => Some(0), + Some(n) => Some(n + 1), + }; + let id = self.last_preselect_begin.unwrap(); + let label = format!("preselect_begin_{id}"); + self.writer.write_jump(&label); // terminate the previous block + self.writer.write_label(&label); // start the new block + } + + fn compile_preselect_expect(&mut self, instruction: &Instruction) { + let id = self.last_preselect_begin.unwrap(); + let reg = format!("preselect_r{}", self.num_preselect_expects); + self.num_preselect_expects += 1; + + // First target: which result to read + let TargetKind::Qubit { + value: result_id, .. + } = instruction.targets[0].kind + else { + return; + }; + // Second target: expected value (0 or 1) + let TargetKind::Qubit { + value: expected, .. + } = instruction.targets[1].kind + else { + return; + }; + + // Read the result into %reg + self.writer + .write_read_result(®, Operand::Qubit(result_id)); + + let begin_label = format!("preselect_begin_{id}"); + let continue_label = format!("preselect_continue_{id}"); + + // Branch: if result matches expected → continue, else → retry + if expected == 0 { + // expected 0: if read is true (1) → mismatch → retry + self.writer + .write_branch(®, &begin_label, &continue_label); + } else { + // expected 1: if read is true (1) → match → continue + self.writer + .write_branch(®, &continue_label, &begin_label); + } + + self.writer.write_label(&continue_label); + } + fn accumulate_correlated_error(&mut self, instruction: &Instruction) { let probability = instruction.args[0]; let mut terms = Vec::new(); @@ -638,107 +700,6 @@ impl<'noise> Compiler<'noise> { self.writer.write_noise_intrinsic(&name, &columns); } - fn compile_collapsing_gate(&mut self, instruction: &Instruction) { - let gate = instruction.name.to_lowercase(); - if gate == "r" { - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - self.writer - .write_qis_call("reset", &[Operand::Qubit(value)]); - } - } else if gate == "m" { - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - self.writer - .write_qis_call("m", &[Operand::Qubit(value), Operand::Result]); - } - } else if gate == "mr" { - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - self.writer - .write_qis_call("mresetz", &[Operand::Qubit(value), Operand::Result]); - } - } else if gate == "mrx" { - // decomposed into H MRZ H - for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { - continue; - }; - let q = Operand::Qubit(value); - self.writer.write_qis_call("h", &[q]); - self.writer.write_qis_call("mresetz", &[q, Operand::Result]); - self.writer.write_qis_call("h", &[q]); - } - } - } - - fn compile_pair_measurement_gate(&mut self, _instruction: &Instruction) {} - - fn compile_generalized_pauli_product_gate(&mut self, _instruction: &Instruction) {} - - fn compile_control_flow(&mut self, _instruction: &Instruction) {} - - fn compile_annotations(&mut self, _instruction: &Instruction) {} - - fn compile_custom_instruction(&mut self, instruction: &Instruction) { - let instruction_name = instruction.name.to_lowercase(); - if instruction_name == "#!preselect_begin" { - self.last_preselect_begin = match self.last_preselect_begin { - None => Some(0), - Some(n) => Some(n + 1), - }; - let id = self.last_preselect_begin.unwrap(); - let label = format!("preselect_begin_{id}"); - self.writer.write_jump(&label); // terminate the previous block - self.writer.write_label(&label); // start the new block - } else if instruction_name == "#!preselect_expect" { - let id = self.last_preselect_begin.unwrap(); - let reg = format!("preselect_r{}", self.num_preselect_expects); - self.num_preselect_expects += 1; - - // First target: which result to read - let TargetKind::Qubit { - value: result_id, .. - } = instruction.targets[0].kind - else { - return; - }; - // Second target: expected value (0 or 1) - let TargetKind::Qubit { - value: expected, .. - } = instruction.targets[1].kind - else { - return; - }; - - // Read the result into %reg - self.writer - .write_read_result(®, Operand::Qubit(result_id)); - - let begin_label = format!("preselect_begin_{id}"); - let continue_label = format!("preselect_continue_{id}"); - - // Branch: if result matches expected → continue, else → retry - if expected == 0 { - // expected 0: if read is true (1) → mismatch → retry - self.writer - .write_branch(®, &begin_label, &continue_label); - } else { - // expected 1: if read is true (1) → match → continue - self.writer - .write_branch(®, &continue_label, &begin_label); - } - - self.writer.write_label(&continue_label); - } - } - fn unsupported(&mut self, instruction: &Instruction) { // TODO: IMPROVE ERROR HANDLING panic!("Unsupported instruction: {}", instruction.name); From 99e71da578bcb322dc3c3548fb09f0f5136b9b01 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 18 Jun 2026 17:24:36 -0700 Subject: [PATCH 49/67] add basic error handling for qir generation --- Cargo.lock | 2 + source/compiler/qsc_stim_parser/Cargo.toml | 2 + source/compiler/qsc_stim_parser/src/lib.rs | 2 +- source/compiler/qsc_stim_parser/src/qir.rs | 198 +++++++++++++++++---- source/qdk_package/qdk/_native.pyi | 8 + source/qdk_package/qdk/stim/__init__.py | 2 +- source/qdk_package/src/interop.rs | 12 +- source/qdk_package/src/interpreter.rs | 11 +- 8 files changed, 200 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abd93783b18..bebc1130386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2542,9 +2542,11 @@ name = "qsc_stim_parser" version = "0.0.0" dependencies = [ "enum-iterator", + "miette", "qdk_simulators", "qsc_data_structures", "rustc-hash", + "thiserror", ] [[package]] diff --git a/source/compiler/qsc_stim_parser/Cargo.toml b/source/compiler/qsc_stim_parser/Cargo.toml index c71c2151720..4999323d99e 100644 --- a/source/compiler/qsc_stim_parser/Cargo.toml +++ b/source/compiler/qsc_stim_parser/Cargo.toml @@ -8,5 +8,7 @@ enum-iterator.workspace = true qdk_simulators = { path = "../../simulators" } qsc_data_structures = { path = "../qsc_data_structures" } rustc-hash = { workspace = true } +miette = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] diff --git a/source/compiler/qsc_stim_parser/src/lib.rs b/source/compiler/qsc_stim_parser/src/lib.rs index 97d7b555b52..79c52ae29df 100644 --- a/source/compiler/qsc_stim_parser/src/lib.rs +++ b/source/compiler/qsc_stim_parser/src/lib.rs @@ -7,7 +7,7 @@ pub mod lex; pub mod parser; pub mod qir; -pub fn compile(src: &str, noise: &mut NoiseConfig) -> String { +pub fn compile(src: &str, noise: &mut NoiseConfig) -> Result> { let circuit = parser::parse(src); qir::compile_to_qir(&circuit, noise) } diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index fbb3e459da7..4ca5f5546c7 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -4,8 +4,11 @@ use qdk_simulators::noise_config::{NoiseConfig, NoiseTable, encode_pauli}; use crate::parser::*; +use miette::Diagnostic; +use qsc_data_structures::span::Span; use rustc_hash::FxHashMap; use std::fmt::Write; +use thiserror::Error; #[derive(Clone, Copy)] enum Operand { @@ -283,6 +286,24 @@ struct CorrelatedGroup { rows: Vec, } +#[derive(Clone, Debug, Error, Diagnostic)] +pub enum Error { + #[error("unsupported instruction: {name}")] + #[diagnostic(code("Stim.UnsupportedInstruction"))] + UnsupportedInstruction { + name: String, + #[label] + span: Span, + }, + #[error("unknown instruction: {name}")] + #[diagnostic(code("Stim.UnknownInstruction"))] + UnknownInstruction { + name: String, + #[label] + span: Span, + }, +} + struct Compiler<'noise> { writer: QirWriter, last_preselect_begin: Option, @@ -290,6 +311,7 @@ struct Compiler<'noise> { noise: &'noise mut NoiseConfig, current_correlated_group: Option, num_noise_intrinsics: u32, + errors: Vec, } impl<'noise> Compiler<'noise> { @@ -301,6 +323,7 @@ impl<'noise> Compiler<'noise> { noise, current_correlated_group: None, num_noise_intrinsics: 0, + errors: Vec::new(), } } @@ -346,96 +369,191 @@ impl<'noise> Compiler<'noise> { "X" | "Y" | "Z" => self.emit_single(instruction, &instruction.name.to_lowercase()), // Single Qubit Clifford Gates - "H" => self.emit_single(instruction, &instruction.name.to_lowercase()), + "C_NXYZ" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0; S 0; S 0 + self.emit_single_adj(instruction, "s"); + self.emit_single(instruction, "h"); + self.emit_single(instruction, "z"); + } + "C_NZYX" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0; S 0; S 0 + self.emit_single(instruction, "z"); + self.emit_single(instruction, "h"); + self.emit_single_adj(instruction, "s"); + } + "C_XNYZ" => { + // Stim decomposition (into H, S, CX, M, R): S 0; H 0 + self.emit_single(instruction, "s"); + self.emit_single(instruction, "h"); + } + "C_XYNZ" => { + // Stim decomposition (into H, S, CX, M, R): S 0; H 0; S 0; S 0 + self.emit_single(instruction, "s"); + self.emit_single(instruction, "h"); + self.emit_single(instruction, "z"); + } + "C_XYZ" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0 + self.emit_single_adj(instruction, "s"); + self.emit_single(instruction, "h"); + } + "C_ZNYX" => { + // Stim decomposition (into H, S, CX, M, R): H 0; S 0; S 0; S 0 + self.emit_single(instruction, "h"); + self.emit_single_adj(instruction, "s"); + } + "C_ZYNX" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0 + self.emit_single(instruction, "z"); + self.emit_single(instruction, "h"); + self.emit_single(instruction, "s"); + } + "C_ZYX" => { + // Stim decomposition (into H, S, CX, M, R): H 0; S 0 + self.emit_single(instruction, "h"); + self.emit_single(instruction, "s"); + } + "H" | "H_XZ" => self.emit_single(instruction, "h"), + "H_NXY" => { + // Stim decomposition (into H, S, CX, M, R): S 0; H 0; S 0; S 0; H 0 + self.emit_single(instruction, "s"); + self.emit_single(instruction, "x"); + } + "H_NXZ" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0; S 0 + self.emit_single(instruction, "z"); + self.emit_single(instruction, "h"); + self.emit_single(instruction, "z"); + } + "H_NYZ" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0; H 0 + self.emit_single(instruction, "z"); + self.emit_single(instruction, "sx"); + } + "H_XY" => { + // Stim decomposition (into H, S, CX, M, R): H 0; S 0; S 0; H 0; S 0 + self.emit_single(instruction, "x"); + self.emit_single(instruction, "s"); + } + "H_YZ" => { + // Stim decomposition (into H, S, CX, M, R): H 0; S 0; H 0; S 0; S 0 + self.emit_single(instruction, "sx"); + self.emit_single(instruction, "z"); + } "S" | "SQRT_Z" => self.emit_single(instruction, "s"), - "S_DAG" | "SQRT_Z_DAG" => self.emit_single_adj(instruction, "s"), "SQRT_X" => self.emit_single(instruction, "sx"), "SQRT_X_DAG" => { + // Stim decomposition (into H, S, CX, M, R): S 0; H 0; S 0 self.emit_single(instruction, "s"); self.emit_single(instruction, "h"); self.emit_single(instruction, "s"); } "SQRT_Y" => { - self.emit_single(instruction, "s"); - self.emit_single(instruction, "s"); + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0 + self.emit_single(instruction, "z"); self.emit_single(instruction, "h"); } "SQRT_Y_DAG" => { + // Stim decomposition (into H, S, CX, M, R): H 0; S 0; S 0 self.emit_single(instruction, "h"); - self.emit_single(instruction, "s"); - self.emit_single(instruction, "s"); + self.emit_single(instruction, "z"); } + "S_DAG" | "SQRT_Z_DAG" => self.emit_single_adj(instruction, "s"), // Two Qubit Clifford Gates + "CX" | "CNOT" | "ZCX" => self.emit_pair(instruction, "cx"), + "CXSWAP" => self.unsupported(instruction), + "CY" | "ZCY" => self.emit_pair(instruction, "cy"), + "CZ" | "ZCZ" => self.emit_pair(instruction, "cz"), + "CZSWAP" | "SWAPCZ" => self.unsupported(instruction), "II" => (), - "CX" | "CY" | "CZ" | "SWAP" => { - self.emit_pair(instruction, &instruction.name.to_lowercase()) + "ISWAP" | "ISWAP_DAG" | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" + | "SQRT_ZZ" | "SQRT_ZZ_DAG" => self.unsupported(instruction), + "SWAP" => self.emit_pair(instruction, &instruction.name.to_lowercase()), + "SWAPCX" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" => { + self.unsupported(instruction) } // Noise Channels "CORRELATED_ERROR" | "ELSE_CORRELATED_ERROR" => { self.accumulate_correlated_error(instruction) } - + "DEPOLARIZE1" => self.compile_depolarize_1(instruction), + "DEPOLARIZE2" => self.compile_depolarize_2(instruction), + "E" + | "HERALDED_ERASE" + | "HERALDED_PAULI_CHANNEL_1" + | "II_ERROR" + | "I_ERROR" + | "PAULI_CHANNEL_1" + | "PAULI_CHANNEL_2" => self.unsupported(instruction), "X_ERROR" | "Y_ERROR" | "Z_ERROR" | "LOSS_ERROR" => { self.compile_fault_error(instruction) } - "DEPOLARIZE1" => self.compile_depolarize_1(instruction), - "DEPOLARIZE2" => self.compile_depolarize_2(instruction), - // Collapsing Gates - "R" | "RZ" => self.emit_single(instruction, "reset"), - "RX" => { - self.emit_single(instruction, "reset"); // RZ + "M" | "MZ" => self.emit_measure(instruction, "m"), + "MR" | "MRZ" => self.emit_measure(instruction, "mresetz"), + "MRX" => { + // Stim decomposition (into H, S, CX, M, R): H 0; M 0; R 0; H 0 + self.emit_single(instruction, "h"); // X -> Z + self.emit_measure(instruction, "mresetz"); // MRZ self.emit_single(instruction, "h"); // Z -> X } - "RY" => { - self.emit_single(instruction, "reset"); // RZ + "MRY" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0; M 0; R 0; H 0; S 0 + self.emit_single_adj(instruction, "s"); // Y -> X + self.emit_single(instruction, "h"); // X -> Z + self.emit_measure(instruction, "mresetz"); // MRZ self.emit_single(instruction, "h"); // Z -> X self.emit_single(instruction, "s"); // X -> Y } - "M" | "MZ" => self.emit_measure(instruction, "m"), "MX" => { + // Stim decomposition (into H, S, CX, M, R): H 0; M 0; H 0 self.emit_single(instruction, "h"); // X -> Z self.emit_measure(instruction, "m"); // MZ self.emit_single(instruction, "h"); // Z -> X } "MY" => { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0; M 0; H 0; S 0 self.emit_single_adj(instruction, "s"); // Y -> X self.emit_single(instruction, "h"); // X -> Z self.emit_measure(instruction, "m"); // MZ self.emit_single(instruction, "h"); // Z -> X self.emit_single(instruction, "s"); // X -> Y } - "MR" | "MRZ" => self.emit_measure(instruction, "mresetz"), - "MRX" => { - self.emit_single(instruction, "h"); // X -> Z - self.emit_measure(instruction, "mresetz"); // MRZ + "R" | "RZ" => self.emit_single(instruction, "reset"), + "RX" => { + // Stim decomposition (into H, S, CX, M, R): R 0; H 0 + self.emit_single(instruction, "reset"); // RZ self.emit_single(instruction, "h"); // Z -> X } - "MRY" => { - self.emit_single_adj(instruction, "s"); // Y -> X - self.emit_single(instruction, "h"); // X -> Z - self.emit_measure(instruction, "mresetz"); // MRZ + "RY" => { + // Stim decomposition (into H, S, CX, M, R): R 0; H 0; S 0 + self.emit_single(instruction, "reset"); // RZ self.emit_single(instruction, "h"); // Z -> X self.emit_single(instruction, "s"); // X -> Y } // Pair Measurement Gates + "MXX" | "MYY" | "MZZ" => self.unsupported(instruction), // Generalized Pauli Product Gates + "MPP" | "SPP" | "SPP_DAG" => self.unsupported(instruction), // Control Flow + "REPEAT" => self.unsupported(instruction), // Annotations + "DETECTOR" | "MPAD" | "OBSERVABLE_INCLUDE" | "QUBIT_COORDS" | "SHIFT_COORDS" + | "TICK" => (), // Custom Instructions "!rhai" => (), "#!preselect_begin" => self.compile_preselect_begin(), "#!preselect_expect" => self.compile_preselect_expect(instruction), - _ => self.unsupported(instruction), + _ => self.unknown(instruction), } } @@ -701,19 +819,35 @@ impl<'noise> Compiler<'noise> { } fn unsupported(&mut self, instruction: &Instruction) { - // TODO: IMPROVE ERROR HANDLING - panic!("Unsupported instruction: {}", instruction.name); + self.errors.push(Error::UnsupportedInstruction { + name: instruction.name.clone(), + span: instruction.span, + }); } - fn into_qir(mut self, circuit: &Circuit) -> String { + fn unknown(&mut self, instruction: &Instruction) { + self.errors.push(Error::UnknownInstruction { + name: instruction.name.clone(), + span: instruction.span, + }); + } + + fn into_qir(mut self, circuit: &Circuit) -> Result> { self.writer.write_header(); self.compile_circuit(circuit); self.finish_correlated_group(); self.writer.write_footer(); - self.writer.output + if self.errors.is_empty() { + Ok(self.writer.output) + } else { + Err(self.errors) + } } } -pub fn compile_to_qir(circuit: &Circuit, noise: &mut NoiseConfig) -> String { +pub fn compile_to_qir( + circuit: &Circuit, + noise: &mut NoiseConfig, +) -> Result> { Compiler::new(noise).into_qir(circuit) } diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index a68aa90fc55..0126ba8e1ca 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -521,6 +521,13 @@ class QasmError(BaseException): ... +class StimError(BaseException): + """ + An error returned from the Stim compiler. + """ + + ... + def physical_estimates(logical_resources: str, params: str) -> str: """ Estimates physical resources from pre-calculated logical resources. @@ -663,6 +670,7 @@ def compile_stim_to_qir( :param noise: The noise configuration to use. :return: The converted QIR code as a string and the noise configuration. :rtype: Tuple[str, NoiseConfig] + :raises StimError: If there is an error compiling the Stim program. """ ... diff --git a/source/qdk_package/qdk/stim/__init__.py b/source/qdk_package/qdk/stim/__init__.py index a2f205e57dc..3707edbc115 100644 --- a/source/qdk_package/qdk/stim/__init__.py +++ b/source/qdk_package/qdk/stim/__init__.py @@ -1,5 +1,5 @@ from ..simulation import run_qir -from .._native import NoiseConfig, compile_stim_to_qir +from .._native import NoiseConfig, StimError, compile_stim_to_qir from typing import List, Literal, Optional, Tuple diff --git a/source/qdk_package/src/interop.rs b/source/qdk_package/src/interop.rs index 758b692dd64..f429cde8109 100644 --- a/source/qdk_package/src/interop.rs +++ b/source/qdk_package/src/interop.rs @@ -15,7 +15,7 @@ use crate::fs::file_system; use crate::interpreter::data_interop::value_to_pyobj; use crate::interpreter::{ CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError, - TargetProfile, format_error, format_errors, + StimError, TargetProfile, format_error, format_errors, }; use crate::qir_simulation::{NoiseConfig, bind_noise_config, unbind_noise_config}; use pyo3::IntoPyObjectExt; @@ -476,7 +476,15 @@ pub(crate) fn compile_stim_to_qir( |noise_config| unbind_noise_config(py, noise_config), ); - let qir = qsc_stim_parser::compile(source, &mut noise_config); + let qir = qsc_stim_parser::compile(source, &mut noise_config).map_err(|errors| { + StimError::new_err( + errors + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"), + ) + })?; Ok((qir, bind_noise_config(py, &noise_config)?)) } diff --git a/source/qdk_package/src/interpreter.rs b/source/qdk_package/src/interpreter.rs index 28b289f33ad..6c1dfea1fcd 100644 --- a/source/qdk_package/src/interpreter.rs +++ b/source/qdk_package/src/interpreter.rs @@ -15,7 +15,8 @@ use crate::{ interop::{ circuit_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp, compile_stim_to_qir, create_filesystem_from_py, get_operation_name, get_output_semantics, - get_program_type, get_search_path, resource_estimate_qasm_program, run_qasm_program, sanitize_name, + get_program_type, get_search_path, resource_estimate_qasm_program, run_qasm_program, + sanitize_name, }, interpreter::data_interop::{ PrimitiveKind, TypeIR, TypeKind, UdtFields, UdtIR, UdtValue, collect_udt_fields, @@ -147,6 +148,7 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { register_qre_submodule(m)?; // QASM interop m.add("QasmError", py.get_type::())?; + m.add("StimError", py.get_type::())?; m.add_function(wrap_pyfunction!(resource_estimate_qasm_program, m)?)?; m.add_function(wrap_pyfunction!(run_qasm_program, m)?)?; m.add_function(wrap_pyfunction!(circuit_qasm_program, m)?)?; @@ -1174,6 +1176,13 @@ create_exception!( "An error returned from the OpenQASM parser." ); +create_exception!( + module, + StimError, + pyo3::exceptions::PyException, + "An error returned from the Stim compiler." +); + pub(crate) fn format_errors(errors: Vec) -> String { errors .into_iter() From 1da048c45f0d45e8268d9da945037a487bcedd09 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 18 Jun 2026 18:06:44 -0700 Subject: [PATCH 50/67] mark all stim apis as experimental --- source/qdk_package/qdk/_native.pyi | 4 ++++ source/qdk_package/qdk/stim/__init__.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index 0126ba8e1ca..41e4be1bb3f 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -523,6 +523,8 @@ class QasmError(BaseException): class StimError(BaseException): """ + EXPERIMENTAL: + An error returned from the Stim compiler. """ @@ -664,6 +666,8 @@ def compile_stim_to_qir( source: str, noise: Optional[NoiseConfig] ) -> Tuple[str, NoiseConfig]: """ + EXPERIMENTAL: + Converts a Stim program to QIR. :param source: The Stim source code to convert. diff --git a/source/qdk_package/qdk/stim/__init__.py b/source/qdk_package/qdk/stim/__init__.py index 3707edbc115..fed97522ddd 100644 --- a/source/qdk_package/qdk/stim/__init__.py +++ b/source/qdk_package/qdk/stim/__init__.py @@ -1,9 +1,19 @@ +"""EXPERIMENTAL: Stim-to-QIR compilation and simulation. + +This module is experimental and its API may change in a future release. +""" + from ..simulation import run_qir from .._native import NoiseConfig, StimError, compile_stim_to_qir from typing import List, Literal, Optional, Tuple def compile(src: str, noise: Optional[NoiseConfig] = None) -> Tuple[str, NoiseConfig]: + """ + EXPERIMENTAL: + + Compile a Stim program to QIR. + """ return compile_stim_to_qir(src, noise) @@ -14,5 +24,10 @@ def run( seed: Optional[int] = None, type: Optional[Literal["clifford", "cpu", "gpu"]] = None, ) -> List: + """ + EXPERIMENTAL: + + Compile and simulate a Stim program. + """ qir, noise = compile(src, noise) return run_qir(qir, shots, noise, seed, type) From b75721ce73adb4924604dab59842cb88076e170d Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 18 Jun 2026 19:09:33 -0700 Subject: [PATCH 51/67] add unsupported targets and temporary unsupported arguments error --- source/compiler/qsc_stim_parser/src/qir.rs | 77 +++++++++++++++++----- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 4ca5f5546c7..d278d7bdf90 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -302,6 +302,20 @@ pub enum Error { #[label] span: Span, }, + #[error("unsupported argument in instruction: {instruction}")] + #[diagnostic(code("Stim.UnsupportedArgument"))] + UnsupportedArgument { + instruction: String, + #[label] + span: Span, + }, + #[error("unsupported target in instruction: {instruction}")] + #[diagnostic(code("Stim.UnsupportedTarget"))] + UnsupportedTarget { + instruction: String, + #[label] + span: Span, + }, } struct Compiler<'noise> { @@ -558,18 +572,22 @@ impl<'noise> Compiler<'noise> { } fn emit_single(&mut self, instruction: &Instruction, intrinsic: &str) { + self.unsupported_args(instruction); // Temporary error + for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { + let Some(value) = self.expect_qubit(instruction, target) else { continue; }; + self.writer .write_qis_call(intrinsic, &[Operand::Qubit(value)]); } } fn emit_single_adj(&mut self, instruction: &Instruction, intrinsic: &str) { + self.unsupported_args(instruction); // Temporary error for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { + let Some(value) = self.expect_qubit(instruction, target) else { continue; }; self.writer @@ -578,12 +596,13 @@ impl<'noise> Compiler<'noise> { } fn emit_pair(&mut self, instruction: &Instruction, intrinsic: &str) { + self.unsupported_args(instruction); // Temporary error let targets = &instruction.targets; for pair in targets.chunks(2) { - let TargetKind::Qubit { value: v0, .. } = pair[0].kind else { + let Some(v0) = self.expect_qubit(instruction, &pair[0]) else { continue; }; - let TargetKind::Qubit { value: v1, .. } = pair[1].kind else { + let Some(v1) = self.expect_qubit(instruction, &pair[1]) else { continue; }; self.writer @@ -601,7 +620,7 @@ impl<'noise> Compiler<'noise> { }; let probability = instruction.args[0]; for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { + let Some(value) = self.expect_qubit(instruction, target) else { continue; }; self.current_correlated_group @@ -618,7 +637,7 @@ impl<'noise> Compiler<'noise> { fn compile_depolarize_1(&mut self, instruction: &Instruction) { let each = instruction.args[0] / 3.0; for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { + let Some(value) = self.expect_qubit(instruction, target) else { continue; }; { @@ -639,10 +658,10 @@ impl<'noise> Compiler<'noise> { fn compile_depolarize_2(&mut self, instruction: &Instruction) { let each = instruction.args[0] / 15.0; for pair in instruction.targets.chunks(2) { - let TargetKind::Qubit { value: q0, .. } = pair[0].kind else { + let Some(q0) = self.expect_qubit(instruction, &pair[0]) else { continue; }; - let TargetKind::Qubit { value: q1, .. } = pair[1].kind else { + let Some(q1) = self.expect_qubit(instruction, &pair[1]) else { continue; }; { @@ -680,8 +699,9 @@ impl<'noise> Compiler<'noise> { } fn emit_measure(&mut self, instruction: &Instruction, intrinsic: &str) { + self.unsupported_args(instruction); // Temporary error for target in &instruction.targets { - let TargetKind::Qubit { value, .. } = target.kind else { + let Some(value) = self.expect_qubit(instruction, target) else { continue; }; self.writer @@ -701,22 +721,17 @@ impl<'noise> Compiler<'noise> { } fn compile_preselect_expect(&mut self, instruction: &Instruction) { + self.unsupported_args(instruction); // Temporary error let id = self.last_preselect_begin.unwrap(); let reg = format!("preselect_r{}", self.num_preselect_expects); self.num_preselect_expects += 1; // First target: which result to read - let TargetKind::Qubit { - value: result_id, .. - } = instruction.targets[0].kind - else { + let Some(result_id) = self.expect_qubit(instruction, &instruction.targets[0]) else { return; }; // Second target: expected value (0 or 1) - let TargetKind::Qubit { - value: expected, .. - } = instruction.targets[1].kind - else { + let Some(expected) = self.expect_qubit(instruction, &instruction.targets[1]) else { return; }; @@ -818,6 +833,21 @@ impl<'noise> Compiler<'noise> { self.writer.write_noise_intrinsic(&name, &columns); } + fn expect_qubit(&mut self, instruction: &Instruction, target: &Target) -> Option { + let TargetKind::Qubit { + value, + negated: false, + } = target.kind + else { + self.emit_error(Error::UnsupportedTarget { + instruction: instruction.name.clone(), + span: target.span, + }); + return None; + }; + Some(value) + } + fn unsupported(&mut self, instruction: &Instruction) { self.errors.push(Error::UnsupportedInstruction { name: instruction.name.clone(), @@ -825,6 +855,15 @@ impl<'noise> Compiler<'noise> { }); } + fn unsupported_args(&mut self, instruction: &Instruction) { + if !instruction.args.is_empty() { + self.errors.push(Error::UnsupportedArgument { + instruction: instruction.name.clone(), + span: instruction.span, + }); + } + } + fn unknown(&mut self, instruction: &Instruction) { self.errors.push(Error::UnknownInstruction { name: instruction.name.clone(), @@ -832,6 +871,10 @@ impl<'noise> Compiler<'noise> { }); } + fn emit_error(&mut self, error: Error) { + self.errors.push(error); + } + fn into_qir(mut self, circuit: &Circuit) -> Result> { self.writer.write_header(); self.compile_circuit(circuit); From fafa3e74c45e9bacc9227a1c38724669e8724668 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 18 Jun 2026 19:46:47 -0700 Subject: [PATCH 52/67] fix incorrect ordering when broadcasting instructions --- source/compiler/qsc_stim_parser/src/qir.rs | 298 +++++++++++---------- 1 file changed, 150 insertions(+), 148 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index d278d7bdf90..6f8be6cf301 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -380,110 +380,114 @@ impl<'noise> Compiler<'noise> { match instruction.name.as_str() { // Pauli Gates "I" => (), - "X" | "Y" | "Z" => self.emit_single(instruction, &instruction.name.to_lowercase()), + "X" | "Y" | "Z" => self.broadcast(instruction, |s, q| { + s.op(&instruction.name.to_lowercase(), q); + }), // Single Qubit Clifford Gates - "C_NXYZ" => { + "C_NXYZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0; S 0; S 0 - self.emit_single_adj(instruction, "s"); - self.emit_single(instruction, "h"); - self.emit_single(instruction, "z"); - } - "C_NZYX" => { + s.op_adj("s", q); + s.op("h", q); + s.op("z", q); + }), + "C_NZYX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0; S 0; S 0 - self.emit_single(instruction, "z"); - self.emit_single(instruction, "h"); - self.emit_single_adj(instruction, "s"); - } - "C_XNYZ" => { + s.op("z", q); + s.op("h", q); + s.op_adj("s", q); + }), + "C_XNYZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; H 0 - self.emit_single(instruction, "s"); - self.emit_single(instruction, "h"); - } - "C_XYNZ" => { + s.op("s", q); + s.op("h", q); + }), + "C_XYNZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; H 0; S 0; S 0 - self.emit_single(instruction, "s"); - self.emit_single(instruction, "h"); - self.emit_single(instruction, "z"); - } - "C_XYZ" => { + s.op("s", q); + s.op("h", q); + s.op("z", q); + }), + "C_XYZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0 - self.emit_single_adj(instruction, "s"); - self.emit_single(instruction, "h"); - } - "C_ZNYX" => { + s.op_adj("s", q); + s.op("h", q); + }), + "C_ZNYX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; S 0; S 0; S 0 - self.emit_single(instruction, "h"); - self.emit_single_adj(instruction, "s"); - } - "C_ZYNX" => { + s.op("h", q); + s.op_adj("s", q); + }), + "C_ZYNX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0 - self.emit_single(instruction, "z"); - self.emit_single(instruction, "h"); - self.emit_single(instruction, "s"); - } - "C_ZYX" => { + s.op("z", q); + s.op("h", q); + s.op("s", q); + }), + "C_ZYX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; S 0 - self.emit_single(instruction, "h"); - self.emit_single(instruction, "s"); - } - "H" | "H_XZ" => self.emit_single(instruction, "h"), - "H_NXY" => { + s.op("h", q); + s.op("s", q); + }), + "H" | "H_XZ" => self.broadcast(instruction, |s, q| s.op("h", q)), + "H_NXY" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; H 0; S 0; S 0; H 0 - self.emit_single(instruction, "s"); - self.emit_single(instruction, "x"); - } - "H_NXZ" => { + s.op("s", q); + s.op("x", q); + }), + "H_NXZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0; S 0 - self.emit_single(instruction, "z"); - self.emit_single(instruction, "h"); - self.emit_single(instruction, "z"); - } - "H_NYZ" => { + s.op("z", q); + s.op("h", q); + s.op("z", q); + }), + "H_NYZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0; S 0; H 0 - self.emit_single(instruction, "z"); - self.emit_single(instruction, "sx"); - } - "H_XY" => { + s.op("z", q); + s.op("sx", q); + }), + "H_XY" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; S 0; S 0; H 0; S 0 - self.emit_single(instruction, "x"); - self.emit_single(instruction, "s"); - } - "H_YZ" => { + s.op("x", q); + s.op("s", q); + }), + "H_YZ" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; S 0; H 0; S 0; S 0 - self.emit_single(instruction, "sx"); - self.emit_single(instruction, "z"); - } - "S" | "SQRT_Z" => self.emit_single(instruction, "s"), - "SQRT_X" => self.emit_single(instruction, "sx"), - "SQRT_X_DAG" => { + s.op("sx", q); + s.op("z", q); + }), + "S" | "SQRT_Z" => self.broadcast(instruction, |s, q| s.op("s", q)), + "SQRT_X" => self.broadcast(instruction, |s, q| s.op("sx", q)), + "SQRT_X_DAG" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; H 0; S 0 - self.emit_single(instruction, "s"); - self.emit_single(instruction, "h"); - self.emit_single(instruction, "s"); - } - "SQRT_Y" => { + s.op("s", q); + s.op("h", q); + s.op("s", q); + }), + "SQRT_Y" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; H 0 - self.emit_single(instruction, "z"); - self.emit_single(instruction, "h"); - } - "SQRT_Y_DAG" => { + s.op("z", q); + s.op("h", q); + }), + "SQRT_Y_DAG" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; S 0; S 0 - self.emit_single(instruction, "h"); - self.emit_single(instruction, "z"); - } - "S_DAG" | "SQRT_Z_DAG" => self.emit_single_adj(instruction, "s"), + s.op("h", q); + s.op("z", q); + }), + "S_DAG" | "SQRT_Z_DAG" => self.broadcast(instruction, |s, q| s.op_adj("s", q)), // Two Qubit Clifford Gates - "CX" | "CNOT" | "ZCX" => self.emit_pair(instruction, "cx"), + "CX" | "CNOT" | "ZCX" => self.broadcast_pair(instruction, |s, q0, q1| { + s.op_2("cx", q0, q1); + }), "CXSWAP" => self.unsupported(instruction), - "CY" | "ZCY" => self.emit_pair(instruction, "cy"), - "CZ" | "ZCZ" => self.emit_pair(instruction, "cz"), + "CY" | "ZCY" => self.broadcast_pair(instruction, |s, q0, q1| s.op_2("cy", q0, q1)), + "CZ" | "ZCZ" => self.broadcast_pair(instruction, |s, q0, q1| s.op_2("cz", q0, q1)), "CZSWAP" | "SWAPCZ" => self.unsupported(instruction), "II" => (), "ISWAP" | "ISWAP_DAG" | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" | "SQRT_ZZ" | "SQRT_ZZ_DAG" => self.unsupported(instruction), - "SWAP" => self.emit_pair(instruction, &instruction.name.to_lowercase()), + "SWAP" => self.broadcast_pair(instruction, |s, q0, q1| s.op_2("swap", q0, q1)), "SWAPCX" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" => { self.unsupported(instruction) } @@ -506,48 +510,48 @@ impl<'noise> Compiler<'noise> { } // Collapsing Gates - "M" | "MZ" => self.emit_measure(instruction, "m"), - "MR" | "MRZ" => self.emit_measure(instruction, "mresetz"), - "MRX" => { + "M" | "MZ" => self.broadcast(instruction, |s, q| s.op_measure("m", q)), + "MR" | "MRZ" => self.broadcast(instruction, |s, q| s.op_measure("mresetz", q)), + "MRX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; M 0; R 0; H 0 - self.emit_single(instruction, "h"); // X -> Z - self.emit_measure(instruction, "mresetz"); // MRZ - self.emit_single(instruction, "h"); // Z -> X - } - "MRY" => { + s.op("h", q); // X -> Z + s.op_measure("mresetz", q); // MRZ + s.op("h", q); // Z -> X + }), + "MRY" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0; M 0; R 0; H 0; S 0 - self.emit_single_adj(instruction, "s"); // Y -> X - self.emit_single(instruction, "h"); // X -> Z - self.emit_measure(instruction, "mresetz"); // MRZ - self.emit_single(instruction, "h"); // Z -> X - self.emit_single(instruction, "s"); // X -> Y - } - "MX" => { + s.op_adj("s", q); // Y -> X + s.op("h", q); // X -> Z + s.op_measure("mresetz", q); // MRZ + s.op("h", q); // Z -> X + s.op("s", q); // X -> Y + }), + "MX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): H 0; M 0; H 0 - self.emit_single(instruction, "h"); // X -> Z - self.emit_measure(instruction, "m"); // MZ - self.emit_single(instruction, "h"); // Z -> X - } - "MY" => { + s.op("h", q); // X -> Z + s.op_measure("m", q); // MZ + s.op("h", q); // Z -> X + }), + "MY" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 0; M 0; H 0; S 0 - self.emit_single_adj(instruction, "s"); // Y -> X - self.emit_single(instruction, "h"); // X -> Z - self.emit_measure(instruction, "m"); // MZ - self.emit_single(instruction, "h"); // Z -> X - self.emit_single(instruction, "s"); // X -> Y - } - "R" | "RZ" => self.emit_single(instruction, "reset"), - "RX" => { + s.op_adj("s", q); // Y -> X + s.op("h", q); // X -> Z + s.op_measure("m", q); // MZ + s.op("h", q); // Z -> X + s.op("s", q); // X -> Y + }), + "R" | "RZ" => self.broadcast(instruction, |s, q| s.op("reset", q)), + "RX" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): R 0; H 0 - self.emit_single(instruction, "reset"); // RZ - self.emit_single(instruction, "h"); // Z -> X - } - "RY" => { + s.op("reset", q); // RZ + s.op("h", q); // Z -> X + }), + "RY" => self.broadcast(instruction, |s, q| { // Stim decomposition (into H, S, CX, M, R): R 0; H 0; S 0 - self.emit_single(instruction, "reset"); // RZ - self.emit_single(instruction, "h"); // Z -> X - self.emit_single(instruction, "s"); // X -> Y - } + s.op("reset", q); // RZ + s.op("h", q); // Z -> X + s.op("s", q); // X -> Y + }), // Pair Measurement Gates "MXX" | "MYY" | "MZZ" => self.unsupported(instruction), @@ -571,45 +575,54 @@ impl<'noise> Compiler<'noise> { } } - fn emit_single(&mut self, instruction: &Instruction, intrinsic: &str) { + fn broadcast(&mut self, instruction: &Instruction, mut f: impl FnMut(&mut Self, u32)) { self.unsupported_args(instruction); // Temporary error - for target in &instruction.targets { - let Some(value) = self.expect_qubit(instruction, target) else { + let Some(q) = self.expect_qubit(instruction, target) else { continue; }; - - self.writer - .write_qis_call(intrinsic, &[Operand::Qubit(value)]); + f(self, q); } } - fn emit_single_adj(&mut self, instruction: &Instruction, intrinsic: &str) { - self.unsupported_args(instruction); // Temporary error - for target in &instruction.targets { - let Some(value) = self.expect_qubit(instruction, target) else { - continue; - }; - self.writer - .write_qis_adj_call(intrinsic, &[Operand::Qubit(value)]); - } - } - - fn emit_pair(&mut self, instruction: &Instruction, intrinsic: &str) { + fn broadcast_pair( + &mut self, + instruction: &Instruction, + mut f: impl FnMut(&mut Self, u32, u32), + ) { self.unsupported_args(instruction); // Temporary error let targets = &instruction.targets; for pair in targets.chunks(2) { - let Some(v0) = self.expect_qubit(instruction, &pair[0]) else { + let Some(q0) = self.expect_qubit(instruction, &pair[0]) else { continue; }; - let Some(v1) = self.expect_qubit(instruction, &pair[1]) else { + let Some(q1) = self.expect_qubit(instruction, &pair[1]) else { continue; }; - self.writer - .write_qis_call(intrinsic, &[Operand::Qubit(v0), Operand::Qubit(v1)]); + f(self, q0, q1); } } + fn op(&mut self, intrinsic: &str, qubit: u32) { + self.writer + .write_qis_call(intrinsic, &[Operand::Qubit(qubit)]); + } + + fn op_adj(&mut self, intrinsic: &str, qubit: u32) { + self.writer + .write_qis_adj_call(intrinsic, &[Operand::Qubit(qubit)]); + } + + fn op_measure(&mut self, intrinsic: &str, qubit: u32) { + self.writer + .write_qis_call(intrinsic, &[Operand::Qubit(qubit), Operand::Result]); + } + + fn op_2(&mut self, intrinsic: &str, q0: u32, q1: u32) { + self.writer + .write_qis_call(intrinsic, &[Operand::Qubit(q0), Operand::Qubit(q1)]); + } + fn compile_fault_error(&mut self, instruction: &Instruction) { let gate = instruction.name.to_lowercase(); let fault = match gate.as_str() { @@ -698,17 +711,6 @@ impl<'noise> Compiler<'noise> { } } - fn emit_measure(&mut self, instruction: &Instruction, intrinsic: &str) { - self.unsupported_args(instruction); // Temporary error - for target in &instruction.targets { - let Some(value) = self.expect_qubit(instruction, target) else { - continue; - }; - self.writer - .write_qis_call(intrinsic, &[Operand::Qubit(value), Operand::Result]); - } - } - fn compile_preselect_begin(&mut self) { self.last_preselect_begin = match self.last_preselect_begin { None => Some(0), From 53382dbd3dbb6aa357d27282954bd72cca4c276d Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 19 Jun 2026 11:24:32 -0700 Subject: [PATCH 53/67] fix semantics for correlated error --- source/compiler/qsc_stim_parser/src/qir.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 6f8be6cf301..0af0f639d53 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -813,6 +813,8 @@ impl<'noise> Compiler<'noise> { let mut pauli_strings = Vec::with_capacity(group.rows.len()); let mut probabilities = Vec::with_capacity(group.rows.len()); + // Stim's correlated error chain is sequential: a row only fires if no earlier row did + let mut remaining_probability = 1.0; for row in &group.rows { // Build the fault string over the group columns; untouched qubits are `I`. let mut chars = vec!['I'; columns.len()]; @@ -822,7 +824,9 @@ impl<'noise> Compiler<'noise> { } let pauli: String = chars.into_iter().collect(); pauli_strings.push(encode_pauli(&pauli)); - probabilities.push(row.probability); + let output_probability = remaining_probability * row.probability; + probabilities.push(output_probability); + remaining_probability *= 1.0 - row.probability; } let table = NoiseTable { From 0b08f3855d340ec60c5f8b7829fe786b03434c7c Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 19 Jun 2026 13:02:10 -0700 Subject: [PATCH 54/67] support more gates --- source/compiler/qsc_stim_parser/src/qir.rs | 181 +++++++++++++++++++-- 1 file changed, 170 insertions(+), 11 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 0af0f639d53..589c53d1f7d 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -480,26 +480,159 @@ impl<'noise> Compiler<'noise> { "CX" | "CNOT" | "ZCX" => self.broadcast_pair(instruction, |s, q0, q1| { s.op_2("cx", q0, q1); }), - "CXSWAP" => self.unsupported(instruction), + "CXSWAP" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): CX 1 0; CX 0 1 + s.op_2("cx", q1, q0); + s.op_2("cx", q0, q1); + }), "CY" | "ZCY" => self.broadcast_pair(instruction, |s, q0, q1| s.op_2("cy", q0, q1)), "CZ" | "ZCZ" => self.broadcast_pair(instruction, |s, q0, q1| s.op_2("cz", q0, q1)), - "CZSWAP" | "SWAPCZ" => self.unsupported(instruction), + "CZSWAP" | "SWAPCZ" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 0; CX 0 1; CX 1 0; H 1 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op_2("cx", q1, q0); + s.op("h", q1); + }), "II" => (), - "ISWAP" | "ISWAP_DAG" | "SQRT_XX" | "SQRT_XX_DAG" | "SQRT_YY" | "SQRT_YY_DAG" - | "SQRT_ZZ" | "SQRT_ZZ_DAG" => self.unsupported(instruction), + "ISWAP" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 0; CX 0 1; CX 1 0; H 1; S 1; S 0 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op_2("cx", q1, q0); + s.op("h", q1); + s.op("s", q1); + s.op("s", q0); + }), + "ISWAP_DAG" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; S 1; S 1; S 1; H 1; CX 1 0; CX 0 1; H 0 + s.op_adj("s", q0); + s.op_adj("s", q1); + s.op("h", q1); + s.op_2("cx", q1, q0); + s.op_2("cx", q0, q1); + s.op("h", q0); + }), + "SQRT_XX" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 0; CX 0 1; H 1; S 0; S 1; H 0; H 1 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("h", q1); + s.op("s", q0); + s.op("s", q1); + s.op("h", q0); + s.op("h", q1); + }), + "SQRT_XX_DAG" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 0; CX 0 1; H 1; S 0; S 0; S 0; S 1; S 1; S 1; H 0; H 1 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("h", q1); + s.op_adj("s", q0); + s.op_adj("s", q1); + s.op("h", q0); + s.op("h", q1); + }), + "SQRT_YY" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; S 1; S 1; S 1; H 0; CX 0 1; H 1; S 0; S 1; H 0; H 1; S 0; S 1 + s.op_adj("s", q0); // S 0; S 0; S 0 + s.op_adj("s", q1); // S 1; S 1; S 1 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("h", q1); + s.op("s", q0); + s.op("s", q1); + s.op("h", q0); + s.op("h", q1); + s.op("s", q0); + s.op("s", q1); + }), + "SQRT_YY_DAG" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; S 1; H 0; CX 0 1; H 1; S 0; S 1; H 0; H 1; S 0; S 1; S 1; S 1 + s.op_adj("s", q0); // S 0; S 0; S 0 + s.op("s", q1); // S 1 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("h", q1); + s.op("s", q0); + s.op("s", q1); + s.op("h", q0); + s.op("h", q1); + s.op("s", q0); + s.op_adj("s", q1); // S 1; S 1; S 1 + }), + "SQRT_ZZ" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 1; CX 0 1; H 1; S 0; S 1 + s.op("h", q1); + s.op_2("cx", q0, q1); + s.op("h", q1); + s.op("s", q0); + s.op("s", q1); + }), + "SQRT_ZZ_DAG" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 1; CX 0 1; H 1; S 0; S 0; S 0; S 1; S 1; S 1 + s.op("h", q1); + s.op_2("cx", q0, q1); + s.op("h", q1); + s.op_adj("s", q0); + s.op_adj("s", q1); + }), "SWAP" => self.broadcast_pair(instruction, |s, q0, q1| s.op_2("swap", q0, q1)), - "SWAPCX" | "XCX" | "XCY" | "XCZ" | "YCX" | "YCY" | "YCZ" => { - self.unsupported(instruction) - } + "SWAPCX" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): CX 0 1; CX 1 0 + s.op_2("cx", q0, q1); + s.op_2("cx", q1, q0); + }), + "XCX" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 0; CX 0 1; H 0 + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("h", q0); + }), + "XCY" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): H 0; S 1; S 1; S 1; CX 0 1; H 0; S 1 + s.op("h", q0); + s.op_adj("s", q1); + s.op_2("cx", q0, q1); + s.op("h", q0); + s.op("s", q1); + }), + "XCZ" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): CX 1 0 + s.op_2("cx", q1, q0); + }), + "YCX" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; H 1; CX 1 0; S 0; H 1 + s.op_adj("s", q0); + s.op("h", q1); + s.op_2("cx", q1, q0); + s.op("s", q0); + s.op("h", q1); + }), + "YCY" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; S 1; S 1; S 1; H 0; CX 0 1; H 0; S 0; S 1 + s.op_adj("s", q0); + s.op_adj("s", q1); + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("h", q0); + s.op("s", q0); + s.op("s", q1); + }), + "YCZ" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 0; S 0; CX 1 0; S 0 + s.op_adj("s", q0); + s.op_2("cx", q1, q0); + s.op("s", q0); + }), // Noise Channels - "CORRELATED_ERROR" | "ELSE_CORRELATED_ERROR" => { + "E" | "CORRELATED_ERROR" | "ELSE_CORRELATED_ERROR" => { self.accumulate_correlated_error(instruction) } "DEPOLARIZE1" => self.compile_depolarize_1(instruction), "DEPOLARIZE2" => self.compile_depolarize_2(instruction), - "E" - | "HERALDED_ERASE" + "HERALDED_ERASE" | "HERALDED_PAULI_CHANNEL_1" | "II_ERROR" | "I_ERROR" @@ -554,7 +687,33 @@ impl<'noise> Compiler<'noise> { }), // Pair Measurement Gates - "MXX" | "MYY" | "MZZ" => self.unsupported(instruction), + "MXX" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): CX 0 1; H 0; M 0; H 0; CX 0 1 + s.op_2("cx", q0, q1); + s.op("h", q0); + s.op_measure("m", q0); + s.op("h", q0); + s.op_2("cx", q0, q1); + }), + "MYY" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): S 0; S 1; CX 0 1; H 0; M 0; S 1; S 1; H 0; CX 0 1; S 0; S 1 + s.op("s", q0); + s.op("s", q1); + s.op_2("cx", q0, q1); + s.op("h", q0); + s.op_measure("m", q0); + s.op("z", q1); + s.op("h", q0); + s.op_2("cx", q0, q1); + s.op("s", q0); + s.op("s", q1); + }), + "MZZ" => self.broadcast_pair(instruction, |s, q0, q1| { + // Stim decomposition (into H, S, CX, M, R): CX 0 1; M 1; CX 0 1 + s.op_2("cx", q0, q1); + s.op_measure("m", q1); + s.op_2("cx", q0, q1); + }), // Generalized Pauli Product Gates "MPP" | "SPP" | "SPP_DAG" => self.unsupported(instruction), From f72be0d390d7358988467be0e2b7e4425b87ed8d Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 19 Jun 2026 14:27:43 -0700 Subject: [PATCH 55/67] remove unwrap from QirWriter --- source/compiler/qsc_stim_parser/src/qir.rs | 137 ++++++++------------- 1 file changed, 52 insertions(+), 85 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index 589c53d1f7d..ff32f4576f6 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -37,6 +37,12 @@ impl QirWriter { } } + fn write_fmt(&mut self, args: std::fmt::Arguments) { + self.output + .write_fmt(args) + .expect("writing to a String should be infallible"); + } + /// `__quantum__qis__{intrinsic}__body` fn write_qis_call(&mut self, intrinsic: &str, operands: &[Operand]) { self.write_raw_call(&format!("__quantum__qis__{intrinsic}__body"), operands); @@ -50,14 +56,14 @@ impl QirWriter { // Writes: ` call void @{intrinsic}(ptr inttoptr (i64 N to ptr), ...)` // Resolves qubit indices via the qubit map and allocates result IDs internally. fn write_raw_call(&mut self, intrinsic: &str, operands: &[Operand]) { - write!(self.output, " call void @{intrinsic}(").unwrap(); + write!(self, " call void @{intrinsic}("); for (i, &operand) in operands.iter().enumerate() { if i > 0 { - write!(self.output, ", ").unwrap(); + write!(self, ", "); } self.write_operand(operand); } - writeln!(self.output, ")").unwrap(); + writeln!(self, ")"); let params = (0..operands.len()) .map(|_| "ptr") .collect::>() @@ -68,17 +74,17 @@ impl QirWriter { } fn write_noise_intrinsic(&mut self, name: &str, qubits: &[u32]) { - write!(self.output, " call void @{name}(").unwrap(); + write!(self, " call void @{name}("); for (i, &qubit) in qubits.iter().enumerate() { if i > 0 { - write!(self.output, ", ").unwrap(); + write!(self, ", "); } // Register the qubit so it is reflected in `required_num_qubits`, but emit // the raw Stim index as the operand. let id = self.map_qubit(qubit); - write!(self.output, "ptr inttoptr (i64 {id} to ptr)").unwrap(); + write!(self, "ptr inttoptr (i64 {id} to ptr)"); } - writeln!(self.output, ")").unwrap(); + writeln!(self, ")"); let params = (0..qubits.len()) .map(|_| "ptr") .collect::>() @@ -95,49 +101,40 @@ impl QirWriter { Operand::Qubit(stim_index) => self.map_qubit(stim_index), Operand::Result => self.next_result(), }; - write!(self.output, "ptr inttoptr (i64 {id} to ptr)").unwrap(); + write!(self, "ptr inttoptr (i64 {id} to ptr)"); } // Writes a label: `{name}:` fn write_label(&mut self, name: &str) { - writeln!(self.output, "{name}:").unwrap(); + writeln!(self, "{name}:"); } // Writes: ` br i1 %{cond}, label %{true_label}, label %{false_label}` fn write_branch(&mut self, cond: &str, true_label: &str, false_label: &str) { writeln!( - self.output, + self, " br i1 %{cond}, label %{true_label}, label %{false_label}" - ) - .unwrap(); + ); } // Writes: ` br label %{label}` fn write_jump(&mut self, label: &str) { - writeln!(self.output, " br label %{label}").unwrap(); + writeln!(self, " br label %{label}"); } // Writes: ` %{dest} = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 N to ptr))` fn write_read_result(&mut self, dest: &str, operand: Operand) { - write!( - self.output, - " %{dest} = call i1 @__quantum__rt__read_result(" - ) - .unwrap(); + write!(self, " %{dest} = call i1 @__quantum__rt__read_result("); self.write_operand(operand); - writeln!(self.output, ")").unwrap(); + writeln!(self, ")"); self.used_intrinsics .entry("__quantum__rt__read_result".to_string()) .or_insert_with(|| "declare i1 @__quantum__rt__read_result(ptr)".to_string()); } fn write_header(&mut self) { - writeln!(self.output, "define i64 @ENTRYPOINT__main() #0 {{").unwrap(); - writeln!( - self.output, - " call void @__quantum__rt__initialize(ptr null)" - ) - .unwrap(); + writeln!(self, "define i64 @ENTRYPOINT__main() #0 {{"); + writeln!(self, " call void @__quantum__rt__initialize(ptr null)"); self.used_intrinsics .entry("__quantum__rt__initialize".to_string()) .or_insert_with(|| "declare void @__quantum__rt__initialize(ptr)".to_string()); @@ -146,10 +143,9 @@ impl QirWriter { fn write_record_output(&mut self) { let num_results = self.num_results; writeln!( - self.output, + self, " call void @__quantum__rt__array_record_output(i64 {num_results}, ptr null)" - ) - .unwrap(); + ); self.used_intrinsics .entry("__quantum__rt__array_record_output".to_string()) .or_insert_with(|| { @@ -157,10 +153,9 @@ impl QirWriter { }); for i in 0..num_results { writeln!( - self.output, + self, " call void @__quantum__rt__result_record_output(ptr inttoptr (i64 {i} to ptr), ptr null)" - ) - .unwrap(); + ); } self.used_intrinsics .entry("__quantum__rt__result_record_output".to_string()) @@ -170,75 +165,47 @@ impl QirWriter { } fn write_declarations(&mut self) { - writeln!(self.output).unwrap(); - for decl in self.used_intrinsics.values() { - writeln!(self.output, "{decl}").unwrap(); + writeln!(self); + let decls: Vec = self.used_intrinsics.values().cloned().collect(); + for decl in decls { + writeln!(self, "{decl}"); } } fn write_footer(&mut self) { self.write_record_output(); - writeln!(self.output, " ret i64 0").unwrap(); - writeln!(self.output, "}}").unwrap(); + writeln!(self, " ret i64 0"); + writeln!(self, "}}"); self.write_declarations(); let num_qubits = self.qubit_map.len(); let num_results = self.num_results; - writeln!(self.output).unwrap(); + writeln!(self); writeln!( - self.output, + self, "attributes #0 = {{ \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"{num_qubits}\" \"required_num_results\"=\"{num_results}\" }}" - ).unwrap(); - writeln!(self.output, "attributes #1 = {{ \"irreversible\" }}").unwrap(); - writeln!(self.output).unwrap(); - writeln!(self.output, "; module flags").unwrap(); - writeln!(self.output).unwrap(); + ); + writeln!(self, "attributes #1 = {{ \"irreversible\" }}"); + writeln!(self); + writeln!(self, "; module flags"); + writeln!(self); if self.has_noise_intrinsic { - writeln!(self.output, "attributes #2 = {{ \"qdk_noise\" }}").unwrap(); - writeln!(self.output).unwrap(); + writeln!(self, "attributes #2 = {{ \"qdk_noise\" }}"); + writeln!(self); } + writeln!(self, "!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}"); + writeln!(self); + writeln!(self, "!0 = !{{i32 1, !\"qir_major_version\", i32 2}}"); + writeln!(self, "!1 = !{{i32 7, !\"qir_minor_version\", i32 1}}"); + writeln!(self, "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}"); + writeln!(self, "!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}"); + writeln!(self, "!4 = !{{i32 5, !\"int_computations\", !{{!\"i64\"}}}}"); writeln!( - self.output, - "!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}" - ) - .unwrap(); - writeln!(self.output).unwrap(); - writeln!( - self.output, - "!0 = !{{i32 1, !\"qir_major_version\", i32 2}}" - ) - .unwrap(); - writeln!( - self.output, - "!1 = !{{i32 7, !\"qir_minor_version\", i32 1}}" - ) - .unwrap(); - writeln!( - self.output, - "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}" - ) - .unwrap(); - writeln!( - self.output, - "!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}" - ) - .unwrap(); - writeln!( - self.output, - "!4 = !{{i32 5, !\"int_computations\", !{{!\"i64\"}}}}" - ) - .unwrap(); - writeln!( - self.output, + self, "!5 = !{{i32 5, !\"float_computations\", !{{!\"double\"}}}}" - ) - .unwrap(); - writeln!( - self.output, - "!6 = !{{i32 7, !\"backwards_branching\", i2 3}}" - ) - .unwrap(); - writeln!(self.output, "!7 = !{{i32 1, !\"arrays\", i1 true}}").unwrap(); + ); + writeln!(self, "!6 = !{{i32 7, !\"backwards_branching\", i2 3}}"); + writeln!(self, "!7 = !{{i32 1, !\"arrays\", i1 true}}"); } // Maps a Stim qubit index to a 0-based QIR qubit ID. From e0210b080d6e27ccc2d906b081e1d3cb080f9618 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 19 Jun 2026 14:36:46 -0700 Subject: [PATCH 56/67] remove remaining unwraps --- source/compiler/qsc_stim_parser/src/parser.rs | 12 ++++++-- source/compiler/qsc_stim_parser/src/qir.rs | 28 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index d191ba7ec81..724f6e7af15 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -337,7 +337,9 @@ impl<'a> Parser<'a> { span, kind: TargetKind::Pauli { negated, - pauli: head.parse::().unwrap(), + pauli: head + .parse::() + .expect("lexer guarantees X/Y/Z prefix for Pauli targets"), value, }, } @@ -376,11 +378,15 @@ impl<'a> Parser<'a> { } fn extract_uint(&mut self, token: Token, span: Option) -> u32 { - self.extract_string(token, span).parse().unwrap() + self.extract_string(token, span) + .parse() + .expect("lexer guarantees valid uint") } fn extract_double(&mut self, token: Token, span: Option) -> f64 { - self.extract_string(token, span).parse().unwrap() + self.extract_string(token, span) + .parse() + .expect("lexer guarantees valid double") } fn extract_string(&self, token: Token, span: Option) -> String { diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/qsc_stim_parser/src/qir.rs index ff32f4576f6..382a8f27251 100644 --- a/source/compiler/qsc_stim_parser/src/qir.rs +++ b/source/compiler/qsc_stim_parser/src/qir.rs @@ -193,13 +193,25 @@ impl QirWriter { writeln!(self, "attributes #2 = {{ \"qdk_noise\" }}"); writeln!(self); } - writeln!(self, "!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}"); + writeln!( + self, + "!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}" + ); writeln!(self); writeln!(self, "!0 = !{{i32 1, !\"qir_major_version\", i32 2}}"); writeln!(self, "!1 = !{{i32 7, !\"qir_minor_version\", i32 1}}"); - writeln!(self, "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}"); - writeln!(self, "!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}"); - writeln!(self, "!4 = !{{i32 5, !\"int_computations\", !{{!\"i64\"}}}}"); + writeln!( + self, + "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}" + ); + writeln!( + self, + "!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}" + ); + writeln!( + self, + "!4 = !{{i32 5, !\"int_computations\", !{{!\"i64\"}}}}" + ); writeln!( self, "!5 = !{{i32 5, !\"float_computations\", !{{!\"double\"}}}}" @@ -842,7 +854,9 @@ impl<'noise> Compiler<'noise> { None => Some(0), Some(n) => Some(n + 1), }; - let id = self.last_preselect_begin.unwrap(); + let id = self + .last_preselect_begin + .expect("last_preselect_begin was just set to Some above"); let label = format!("preselect_begin_{id}"); self.writer.write_jump(&label); // terminate the previous block self.writer.write_label(&label); // start the new block @@ -850,7 +864,9 @@ impl<'noise> Compiler<'noise> { fn compile_preselect_expect(&mut self, instruction: &Instruction) { self.unsupported_args(instruction); // Temporary error - let id = self.last_preselect_begin.unwrap(); + let id = self + .last_preselect_begin + .expect("PRESELECT_EXPECT must be preceded by a PRESELECT_BEGIN"); let reg = format!("preselect_r{}", self.num_preselect_expects); self.num_preselect_expects += 1; From d51d0bde8fc7c76552ddd5569c743d9f32430e2c Mon Sep 17 00:00:00 2001 From: Bill Ticehurst Date: Fri, 19 Jun 2026 16:54:38 -0700 Subject: [PATCH 57/67] Fix markdown cell --- samples/notebooks/stim_to_qir.ipynb | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb index 954406f2767..0952a148fe4 100644 --- a/samples/notebooks/stim_to_qir.ipynb +++ b/samples/notebooks/stim_to_qir.ipynb @@ -88,21 +88,16 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "ca62aa9f", - "metadata": { - "vscode": { - "languageId": "markdown" - } - }, - "outputs": [], + "metadata": {}, "source": [ "## Noise channels\n", + "The following sections detail specifying noise\n", "\n", "### Correlated error\n", "\n", - "`X0 X1` fire together → only `00` and `11`." + "The `X0 X1` fire together → only `00` and `11`." ] }, { @@ -229,7 +224,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.12)", + "display_name": ".venv (3.14.3)", "language": "python", "name": "python3" }, @@ -243,7 +238,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.14.3" } }, "nbformat": 4, From 1cd012d993a5e08176e8f2e10953664c20f5783e Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 16:23:29 -0700 Subject: [PATCH 58/67] minor fixes for lexer and parser --- source/compiler/qsc_stim_parser/src/lex.rs | 2 +- source/compiler/qsc_stim_parser/src/parser.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index b98458176e2..d42a30472bc 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -41,7 +41,7 @@ impl Display for TokenKind { TokenKind::InstructionName => f.write_str("instruction_name"), TokenKind::Rec => f.write_str("rec"), TokenKind::Sweep => f.write_str("sweep"), - TokenKind::Tag => write!(f, "tag"), + TokenKind::Tag => f.write_str("tag"), TokenKind::Open(delim) => write!(f, "open({})", delim), TokenKind::Close(delim) => write!(f, "close({})", delim), TokenKind::Star => write!(f, "star"), diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 724f6e7af15..15b98aef293 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -1,11 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::lex::Delim::Brace; -use crate::lex::Delim::Paren; -use crate::lex::Lexer; -use crate::lex::Token; -use crate::lex::TokenKind; +use crate::lex::{Delim::Brace, Delim::Paren, Lexer, Token, TokenKind}; use qsc_data_structures::span::Span; use std::{iter::Peekable, str::FromStr}; From 41e441c74b3877d1e39d6099f067b6d287945db4 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 16:41:41 -0700 Subject: [PATCH 59/67] minor fixes for lexer and parser --- source/compiler/qsc_stim_parser/src/lex.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index d42a30472bc..2b197a93e84 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -44,10 +44,10 @@ impl Display for TokenKind { TokenKind::Tag => f.write_str("tag"), TokenKind::Open(delim) => write!(f, "open({})", delim), TokenKind::Close(delim) => write!(f, "close({})", delim), - TokenKind::Star => write!(f, "star"), - TokenKind::Bang => write!(f, "bang"), - TokenKind::Comma => write!(f, "comma"), - TokenKind::Unknown => write!(f, "unknown"), + TokenKind::Star => f.write_str("star"), + TokenKind::Bang => f.write_str("bang"), + TokenKind::Comma => f.write_str("comma"), + TokenKind::Unknown => f.write_str("unknown"), } } } From c4731f3c3775fb01e8d11f9960cdc014de4b76f9 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 16:48:36 -0700 Subject: [PATCH 60/67] fix whitespace/newline behavior --- source/compiler/qsc_stim_parser/src/lex.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 2b197a93e84..2a081128bfe 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -89,13 +89,16 @@ impl<'a> Lexer<'a> { while self.chars.next_if(|i| f(i.1)).is_some() {} } - fn whitespace(&mut self) { + fn eat_horizontal_whitespace(&mut self) { + self.eat_while(|c| c == ' ' || c == '\t' || c == '\r'); + } + + fn eat_whitespace(&mut self) { self.eat_while(char::is_whitespace); } fn comment(&mut self) { self.eat_while(|c| c != '\n'); - self.whitespace(); } fn scan_number(&mut self) -> TokenKind { @@ -154,11 +157,11 @@ impl Iterator for Lexer<'_> { let lo: u32 = offset.try_into().expect("offset should fit into u32"); let token_kind = match c { '\n' => { - self.whitespace(); + self.eat_whitespace(); TokenKind::Newline } - ' ' | '\t' => { - self.whitespace(); + ' ' | '\t' | '\r' => { + self.eat_horizontal_whitespace(); return self.next(); } '#' => { From fe700f4a87859ec53b6d7aec0d504011dee59332 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 16:49:34 -0700 Subject: [PATCH 61/67] line / block can end with a EOF instead of necessarily with a newline --- source/compiler/qsc_stim_parser/src/parser.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 15b98aef293..7fdb7d30895 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -121,6 +121,15 @@ impl<'a> Parser<'a> { token } + fn expect_line_end(&mut self) { + if let Some(token) = self.tokens.peek().copied() { + if token.kind != TokenKind::Newline { + panic!("expected newline or end of input, got {:?}", token.kind); + } + self.tokens.next(); + } + } + pub fn parse(&mut self) -> Circuit { let input_len = self .input @@ -187,7 +196,7 @@ impl<'a> Parser<'a> { } } let closing_brace = self.expect(TokenKind::Close(Brace)); - self.expect(TokenKind::Newline); + self.expect_line_end(); let hi = closing_brace.span.hi; Block { span: Span { lo, hi }, @@ -197,7 +206,7 @@ impl<'a> Parser<'a> { } fn parse_line(&mut self, instruction: Instruction) -> Line { - self.expect(TokenKind::Newline); + self.expect_line_end(); Line { span: instruction.span, instruction, From e8024fe699dcb3160d845ac3ef77d1d2ea24ec1c Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 17:05:59 -0700 Subject: [PATCH 62/67] remove unneeded line span --- source/compiler/qsc_stim_parser/src/parser.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/qsc_stim_parser/src/parser.rs index 7fdb7d30895..66b4f2bff2d 100644 --- a/source/compiler/qsc_stim_parser/src/parser.rs +++ b/source/compiler/qsc_stim_parser/src/parser.rs @@ -19,7 +19,6 @@ pub enum Item { #[derive(Debug)] pub struct Line { - pub span: Span, pub instruction: Instruction, } @@ -207,10 +206,7 @@ impl<'a> Parser<'a> { fn parse_line(&mut self, instruction: Instruction) -> Line { self.expect_line_end(); - Line { - span: instruction.span, - instruction, - } + Line { instruction } } fn parse_instruction(&mut self) -> Instruction { From 7b5d32798c49b844ce9e9e4e86f8a47755d5315c Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 17:28:28 -0700 Subject: [PATCH 63/67] ensure there are at least one digit after '.' and in scientific notation exponents --- source/compiler/qsc_stim_parser/src/lex.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 2a081128bfe..1de4f1666df 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -105,6 +105,10 @@ impl<'a> Lexer<'a> { self.eat_while(|c| c.is_ascii_digit()); let mut is_double = false; if self.chars.next_if(|(_, c)| *c == '.').is_some() { + // A decimal point must be followed by at least one digit. + if self.chars.next_if(|(_, c)| c.is_ascii_digit()).is_none() { + return TokenKind::Unknown; + } self.eat_while(|c| c.is_ascii_digit()); is_double = true; } @@ -115,6 +119,10 @@ impl<'a> Lexer<'a> { { // scientific notation self.chars.next_if(|(_, c)| *c == '+' || *c == '-'); + // An exponent must contain at least one digit. + if self.chars.next_if(|(_, c)| c.is_ascii_digit()).is_none() { + return TokenKind::Unknown; + } self.eat_while(|c| c.is_ascii_digit()); is_double = true; } From 60635c232debebfdb3da7eacd572302e25eaef57 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 18:07:09 -0700 Subject: [PATCH 64/67] improving lexing of numbers --- source/compiler/qsc_stim_parser/src/lex.rs | 48 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/qsc_stim_parser/src/lex.rs index 1de4f1666df..5045778178f 100644 --- a/source/compiler/qsc_stim_parser/src/lex.rs +++ b/source/compiler/qsc_stim_parser/src/lex.rs @@ -101,15 +101,41 @@ impl<'a> Lexer<'a> { self.eat_while(|c| c != '\n'); } - fn scan_number(&mut self) -> TokenKind { + fn eat_one_or_more_digits(&mut self) -> bool { + if self.chars.next_if(|(_, c)| c.is_ascii_digit()).is_none() { + return false; + } self.eat_while(|c| c.is_ascii_digit()); + true + } + + fn scan_number(&mut self, signed: bool) -> TokenKind { + // Lexes a number: an optional sign, an integer part, an optional + // fractional part, and an optional exponent. + let mut is_double = false; - if self.chars.next_if(|(_, c)| *c == '.').is_some() { - // A decimal point must be followed by at least one digit. - if self.chars.next_if(|(_, c)| c.is_ascii_digit()).is_none() { + if signed { + // The leading sign was already consumed by the caller: + // "<+>1", "<->42", "<+>3.5e-2" + // This block consumes the integer digits: "+<1>", "-<42>" + if !self.eat_one_or_more_digits() { return TokenKind::Unknown; } + is_double = true; // A signed number is always a double. + } else { + // The first digit was already consumed by the caller: + // "<4>2", "<3>.14" + // This block consumes the remaining integer digits: "4<2>" self.eat_while(|c| c.is_ascii_digit()); + } + + if self.chars.next_if(|(_, c)| *c == '.').is_some() { + // Optional fractional part: a '.' followed by one or more digits. + // "3<.14>", "0<.5>" + // A '.' with no digits after it ("3.") => Unknown. + if !self.eat_one_or_more_digits() { + return TokenKind::Unknown; + } is_double = true; } if self @@ -117,15 +143,18 @@ impl<'a> Lexer<'a> { .next_if(|(_, c)| *c == 'e' || *c == 'E') .is_some() { - // scientific notation + // Optional exponent: 'e'/'E', an optional sign, then one or more digits. + // "1", "2.5", "6" + // A bare "1e" or "1e-" (no exponent digits) => Unknown. self.chars.next_if(|(_, c)| *c == '+' || *c == '-'); - // An exponent must contain at least one digit. - if self.chars.next_if(|(_, c)| c.is_ascii_digit()).is_none() { + if !self.eat_one_or_more_digits() { return TokenKind::Unknown; } - self.eat_while(|c| c.is_ascii_digit()); is_double = true; } + + // No '.' and no exponent => an unsigned integer ("42" => Uint); + // a sign, '.', or exponent makes it a Double ("-42", "3.14", "1e9"). if is_double { TokenKind::Double } else { @@ -188,7 +217,8 @@ impl Iterator for Lexer<'_> { '*' => TokenKind::Star, '!' => TokenKind::Bang, ',' => TokenKind::Comma, - '0'..='9' => self.scan_number(), + '+' | '-' => self.scan_number(true), + '0'..='9' => self.scan_number(false), 'A'..='Z' | 'a'..='z' => self.scan_identifier(lo as usize), '[' => { self.eat_while(|c| c != ']'); From c8217cdcd425e10e95b6269a070024c850361577 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 18:39:42 -0700 Subject: [PATCH 65/67] revert changes to bind_noise_config and create a separate bind_stim_noise_config function --- source/qdk_package/src/interop.rs | 4 ++-- source/qdk_package/src/qir_simulation.rs | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/source/qdk_package/src/interop.rs b/source/qdk_package/src/interop.rs index f429cde8109..5dd928977ba 100644 --- a/source/qdk_package/src/interop.rs +++ b/source/qdk_package/src/interop.rs @@ -17,7 +17,7 @@ use crate::interpreter::{ CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError, StimError, TargetProfile, format_error, format_errors, }; -use crate::qir_simulation::{NoiseConfig, bind_noise_config, unbind_noise_config}; +use crate::qir_simulation::{NoiseConfig, bind_stim_noise_config, unbind_noise_config}; use pyo3::IntoPyObjectExt; use pyo3::exceptions::PyException; use pyo3::prelude::*; @@ -485,7 +485,7 @@ pub(crate) fn compile_stim_to_qir( .join("\n"), ) })?; - Ok((qir, bind_noise_config(py, &noise_config)?)) + Ok((qir, bind_stim_noise_config(py, &noise_config)?)) } /// Enriches the compilation errors to provide more helpful messages diff --git a/source/qdk_package/src/qir_simulation.rs b/source/qdk_package/src/qir_simulation.rs index 8d83984d91c..064f203558e 100644 --- a/source/qdk_package/src/qir_simulation.rs +++ b/source/qdk_package/src/qir_simulation.rs @@ -240,7 +240,7 @@ fn generic_float_cast(value: T) -> Q { num_traits::NumCast::from(value).expect("casting f64 to f32 should succeed") } -pub(crate) fn bind_noise_config( +fn bind_noise_config( py: Python, value: &qdk_simulators::noise_config::NoiseConfig, ) -> PyResult { @@ -271,14 +271,27 @@ pub(crate) fn bind_noise_config( mz: Py::new(py, NoiseTable::from(value.mz.clone()))?, mresetz: Py::new(py, NoiseTable::from(value.mresetz.clone()))?, // idle: Py::new(py, IdleNoiseParams::from(value.idle))?, - intrinsics: Py::new(py, to_intrinsics_table(py, &value.intrinsics)?)?, + intrinsics: Py::new(py, NoiseIntrinsicsTable::default())?, }) } +/// Builds a Python [`NoiseConfig`] for the Stim-to-QIR path, populating the +/// intrinsics table from the Rust intrinsics map. The intrinsics handling is +/// specific to Stim, so it lives here rather than in the general +/// [`bind_noise_config`]. +pub(crate) fn bind_stim_noise_config( + py: Python, + value: &qdk_simulators::noise_config::NoiseConfig, +) -> PyResult { + let mut config = bind_noise_config(py, value)?; + config.intrinsics = Py::new(py, to_stim_intrinsics_table(py, &value.intrinsics)?)?; + Ok(config) +} + /// Builds a Python [`NoiseIntrinsicsTable`] from the Rust intrinsics map, /// preserving each intrinsic's id and naming it `noise_intrinsic_{id}` so the /// name matches the call emitted by the Stim-to-QIR compiler. -fn to_intrinsics_table( +fn to_stim_intrinsics_table( py: Python, intrinsics: &FxHashMap>, ) -> PyResult { From 2ebe859190dfde66b2ce3daf1e593426bf7244e1 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 19:02:02 -0700 Subject: [PATCH 66/67] revert changes to common.wsgl --- .../src/gpu_full_state_simulator/common.wgsl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/simulators/src/gpu_full_state_simulator/common.wgsl b/source/simulators/src/gpu_full_state_simulator/common.wgsl index 8cea491fa1b..fb3eccacbf5 100644 --- a/source/simulators/src/gpu_full_state_simulator/common.wgsl +++ b/source/simulators/src/gpu_full_state_simulator/common.wgsl @@ -356,18 +356,18 @@ fn apply_1q_pauli_noise(shot_idx: u32, op_idx: u32, noise_idx: u32) { shot.unitary[1] = op.unitary[5]; shot.unitary[4] = op.unitary[0]; shot.unitary[5] = op.unitary[1]; - } else if (rand < (p_x + p_z)) { - // Apply Z error (negate |1> state) - shot.unitary[0] = op.unitary[0]; - shot.unitary[1] = op.unitary[1]; - shot.unitary[4] = cplxNeg(op.unitary[4]); - shot.unitary[5] = cplxNeg(op.unitary[5]); - } else if (rand < (p_x + p_z + p_y)) { + } else if (rand < (p_x + p_y)) { // Apply the Y permutation (swap rows with negated |0> state) shot.unitary[0] = cplxNeg(op.unitary[4]); shot.unitary[1] = cplxNeg(op.unitary[5]); shot.unitary[4] = op.unitary[0]; shot.unitary[5] = op.unitary[1]; + } else if (rand < (p_x + p_y + p_z)) { + // Apply Z error (negate |1> state) + shot.unitary[0] = op.unitary[0]; + shot.unitary[1] = op.unitary[1]; + shot.unitary[4] = cplxNeg(op.unitary[4]); + shot.unitary[5] = cplxNeg(op.unitary[5]); } else { // Either loss or no noise: the gate executes unmodified. If loss was // sampled, schedule a loss commit for this qubit; a following From c961bdd1f84b1d6db524e0239300e569ffda1dc0 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 22 Jun 2026 19:21:23 -0700 Subject: [PATCH 67/67] change crate name to stim_compiler --- Cargo.lock | 26 +++++++++---------- Cargo.toml | 2 +- .../Cargo.toml | 2 +- .../src/lex.rs | 0 .../src/lib.rs | 0 .../src/parser.rs | 0 .../src/qir.rs | 0 source/qdk_package/Cargo.toml | 2 +- source/qdk_package/src/interop.rs | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) rename source/compiler/{qsc_stim_parser => stim_compiler}/Cargo.toml (92%) rename source/compiler/{qsc_stim_parser => stim_compiler}/src/lex.rs (100%) rename source/compiler/{qsc_stim_parser => stim_compiler}/src/lib.rs (100%) rename source/compiler/{qsc_stim_parser => stim_compiler}/src/parser.rs (100%) rename source/compiler/{qsc_stim_parser => stim_compiler}/src/qir.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index bebc1130386..2bd3edee7f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,13 +2095,13 @@ dependencies = [ "qdk_simulators", "qre", "qsc", - "qsc_stim_parser", "rand 0.10.1", "rayon", "resource_estimator", "rustc-hash", "serde", "serde_json", + "stim_compiler", ] [[package]] @@ -2537,18 +2537,6 @@ dependencies = [ "rustc-hash", ] -[[package]] -name = "qsc_stim_parser" -version = "0.0.0" -dependencies = [ - "enum-iterator", - "miette", - "qdk_simulators", - "qsc_data_structures", - "rustc-hash", - "thiserror", -] - [[package]] name = "qsc_wasm" version = "0.0.0" @@ -3062,6 +3050,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stim_compiler" +version = "0.0.0" +dependencies = [ + "enum-iterator", + "miette", + "qdk_simulators", + "qsc_data_structures", + "rustc-hash", + "thiserror", +] + [[package]] name = "str_stack" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e4714176c40..7101e39bb47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ members = [ "source/compiler/qsc_hir", "source/compiler/qsc_openqasm_compiler", "source/compiler/qsc_openqasm_parser", - "source/compiler/qsc_stim_parser", + "source/compiler/stim_compiler", "source/compiler/qsc_linter", "source/compiler/qsc_lowerer", "source/compiler/qsc_parse", diff --git a/source/compiler/qsc_stim_parser/Cargo.toml b/source/compiler/stim_compiler/Cargo.toml similarity index 92% rename from source/compiler/qsc_stim_parser/Cargo.toml rename to source/compiler/stim_compiler/Cargo.toml index 4999323d99e..dc9827e0a0c 100644 --- a/source/compiler/qsc_stim_parser/Cargo.toml +++ b/source/compiler/stim_compiler/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "qsc_stim_parser" +name = "stim_compiler" edition.workspace = true version.workspace = true diff --git a/source/compiler/qsc_stim_parser/src/lex.rs b/source/compiler/stim_compiler/src/lex.rs similarity index 100% rename from source/compiler/qsc_stim_parser/src/lex.rs rename to source/compiler/stim_compiler/src/lex.rs diff --git a/source/compiler/qsc_stim_parser/src/lib.rs b/source/compiler/stim_compiler/src/lib.rs similarity index 100% rename from source/compiler/qsc_stim_parser/src/lib.rs rename to source/compiler/stim_compiler/src/lib.rs diff --git a/source/compiler/qsc_stim_parser/src/parser.rs b/source/compiler/stim_compiler/src/parser.rs similarity index 100% rename from source/compiler/qsc_stim_parser/src/parser.rs rename to source/compiler/stim_compiler/src/parser.rs diff --git a/source/compiler/qsc_stim_parser/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs similarity index 100% rename from source/compiler/qsc_stim_parser/src/qir.rs rename to source/compiler/stim_compiler/src/qir.rs diff --git a/source/qdk_package/Cargo.toml b/source/qdk_package/Cargo.toml index 97b0658b3e1..6b15b4f8d12 100644 --- a/source/qdk_package/Cargo.toml +++ b/source/qdk_package/Cargo.toml @@ -16,7 +16,7 @@ num-bigint = { workspace = true } num-complex = { workspace = true } num-traits = { workspace = true } qsc = { path = "../compiler/qsc" } -qsc_stim_parser = { path = "../compiler/qsc_stim_parser" } +stim_compiler = { path = "../compiler/stim_compiler" } qdk_simulators = { path = "../simulators" } resource_estimator = { path = "../resource_estimator" } qre = { path = "../qre" } diff --git a/source/qdk_package/src/interop.rs b/source/qdk_package/src/interop.rs index 5dd928977ba..d9c59ab5c2e 100644 --- a/source/qdk_package/src/interop.rs +++ b/source/qdk_package/src/interop.rs @@ -476,7 +476,7 @@ pub(crate) fn compile_stim_to_qir( |noise_config| unbind_noise_config(py, noise_config), ); - let qir = qsc_stim_parser::compile(source, &mut noise_config).map_err(|errors| { + let qir = stim_compiler::compile(source, &mut noise_config).map_err(|errors| { StimError::new_err( errors .iter()