Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/rust-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,28 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y llvm clang
# Add LLVM 14 repository
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo add-apt-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-14 main"
sudo apt-get update
# Install LLVM 14 specifically
sudo apt-get install -y llvm-14 clang-14
# Create symlinks for llc and clang
sudo update-alternatives --install /usr/bin/llc llc /usr/bin/llc-14 100
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-14 100
# Verify installation
which llc
llc --version

- name: Install LLVM Tools (macOS)
if: matrix.os == 'macos-latest'
run: |
brew install llvm
echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH
brew install llvm@14
echo "$(brew --prefix llvm@14)/bin" >> $GITHUB_PATH
# Make sure it's available in the current step too
export PATH="$(brew --prefix llvm)/bin:$PATH"
export PATH="$(brew --prefix llvm@14)/bin:$PATH"
which llc
llc --version

- name: Install LLVM Tools (Windows)
if: matrix.os == 'windows-latest'
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
tmp/
**/.*/settings.local.json

# Ignore helper text in root
*.txt

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ calls to Wasm VMs, conditional branching, and more.
- Fast Simulation: Leverages a fast stabilizer simulation algorithm.
- Multi-language extensions: Core functionalities implemented via Rust for performance and safety. Additional add-ons
and extension support in C/C++ via Cython.
- QIR Support: Execute Quantum Intermediate Representation programs (requires LLVM version 14 with the 'llc' tool).

## Getting Started

Expand Down Expand Up @@ -97,6 +98,17 @@ To use PECOS in your Rust project, add the following to your `Cargo.toml`:
pecos = "0.x.x" # Replace with the latest version
```

#### Optional Dependencies

- **LLVM version 14**: Required for QIR (Quantum Intermediate Representation) support
- Linux: `sudo apt install llvm-14`
- macOS: `brew install llvm@14`
- Windows: Download LLVM 14.x installer from [LLVM releases](https://releases.llvm.org/download.html#14.0.0)

**Note**: Only LLVM version 14.x is compatible. LLVM 15 or later versions will not work with PECOS's QIR implementation.

If LLVM 14 is not installed, PECOS will still function normally but QIR-related features will be disabled.

## Development Setup

If you are interested in editing or developing the code in this project, see this
Expand Down
206 changes: 192 additions & 14 deletions crates/pecos-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,40 @@ struct CompileArgs {
program: String,
}

#[derive(Args)]
/// Type of quantum noise model to use for simulation
#[derive(PartialEq, Eq, Clone, Debug, Default)]
enum NoiseModelType {
/// Simple depolarizing noise model with uniform error probabilities
///
/// This model applies the same error probability to all operations
#[default]
Depolarizing,
/// General noise model with configurable error probabilities
///
/// This model allows setting different error probabilities for:
/// - state preparation
/// - measurement of |0⟩ state
/// - measurement of |1⟩ state
/// - single-qubit gates
/// - two-qubit gates
General,
}

impl std::str::FromStr for NoiseModelType {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"depolarizing" | "dep" => Ok(NoiseModelType::Depolarizing),
"general" | "gen" => Ok(NoiseModelType::General),
_ => Err(format!(
"Unknown noise model type: {s}. Valid options are 'depolarizing' (dep) or 'general' (gen)"
)),
}
}
}

#[derive(Args, Debug)]
struct RunArgs {
/// Path to the quantum program (LLVM IR or JSON)
program: String,
Expand All @@ -42,34 +75,150 @@ struct RunArgs {
#[arg(short, long, default_value_t = 1)]
workers: usize,

/// Depolarizing noise probability (between 0 and 1)
/// Type of noise model to use (depolarizing or general)
#[arg(long = "model", value_parser, default_value = "depolarizing")]
noise_model: NoiseModelType,

/// Noise probability (between 0 and 1)
/// For depolarizing model: uniform error probability
/// For general model: comma-separated probabilities in order:
/// `prep,meas_0,meas_1,single_qubit,two_qubit`
/// Example: --noise 0.01,0.02,0.02,0.05,0.1
#[arg(short = 'p', long = "noise", value_parser = parse_noise_probability)]
noise_probability: Option<f64>,
noise_probability: Option<String>,

/// Seed for random number generation (for reproducible results)
#[arg(short = 'd', long)]
seed: Option<u64>,
}

fn parse_noise_probability(arg: &str) -> Result<f64, String> {
let prob: f64 = arg
.parse()
.map_err(|_| "Must be a valid floating point number")?;
if !(0.0..=1.0).contains(&prob) {
return Err("Noise probability must be between 0 and 1".into());
/// Parse noise probability specification from command line argument
///
/// For a depolarizing model, a single probability is expected: "0.01"
/// For a general model, five probabilities are expected: "0.01,0.02,0.02,0.05,0.1"
/// representing [prep, `meas_0`, `meas_1`, `single_qubit`, `two_qubit`]
fn parse_noise_probability(arg: &str) -> Result<String, String> {
// Split string into values (either a single value or comma-separated list)
let values: Vec<&str> = if arg.contains(',') {
arg.split(',').collect()
} else {
vec![arg]
};

// Check number of values
if values.len() != 1 && values.len() != 5 {
return Err(format!(
"Expected 1 or 5 probabilities, got {}",
values.len()
));
}

// Validate each probability value
for s in &values {
// Parse and validate numeric value
let prob = s
.trim()
.parse::<f64>()
.map_err(|_| format!("Invalid value '{s}': not a valid number"))?;

// Check value range
if !(0.0..=1.0).contains(&prob) {
return Err(format!("Probability {prob} must be between 0 and 1"));
}
}

Ok(arg.to_string())
}

/// Extract probability values from noise specification string
///
/// Handles both single value and comma-separated formats, with safe defaults
fn parse_noise_values(noise_str_opt: Option<&String>) -> Vec<f64> {
// Default to 0.0 if no string provided
let Some(noise_str) = noise_str_opt else {
return vec![0.0];
};

// Parse either comma-separated or single value
if noise_str.contains(',') {
noise_str
.split(',')
.map(|s| s.trim().parse::<f64>().unwrap_or(0.0))
.collect()
} else {
vec![noise_str.parse::<f64>().unwrap_or(0.0)]
}
Ok(prob)
}

/// Parse a single probability value for depolarizing noise model
///
/// Takes the first probability value if multiple are provided
fn parse_depolarizing_noise_probability(noise_str_opt: Option<&String>) -> f64 {
parse_noise_values(noise_str_opt)[0] // Always has at least one value
}

/// Parse five probability values for general noise model
///
/// Returns a tuple of five probabilities: (prep, `meas_0`, `meas_1`, `single_qubit`, `two_qubit`)
/// If a single value is provided, it's used for all five parameters
fn parse_general_noise_probabilities(noise_str_opt: Option<&String>) -> (f64, f64, f64, f64, f64) {
let probs = parse_noise_values(noise_str_opt);

if probs.len() == 5 {
(probs[0], probs[1], probs[2], probs[3], probs[4])
} else {
// Use the first value for all parameters
let p = probs[0];
(p, p, p, p, p)
}
}

/// Run a quantum program with the specified arguments
///
/// This function sets up the appropriate engines and noise models based on
/// the command line arguments, then runs the specified program and outputs
/// the results.
fn run_program(args: &RunArgs) -> Result<(), Box<dyn Error>> {
let program_path = get_program_path(&args.program)?;
let prob = args.noise_probability.unwrap_or(0.0);

let classical_engine = setup_engine(&program_path, Some(args.shots.div_ceil(args.workers)))?;

let results = MonteCarloEngine::run_with_classical_engine(
// Create the appropriate noise model based on user selection
let noise_model: Box<dyn NoiseModel> = match args.noise_model {
NoiseModelType::Depolarizing => {
// Create a depolarizing noise model with single probability
let prob = parse_depolarizing_noise_probability(args.noise_probability.as_ref());
let mut model = DepolarizingNoiseModel::new_uniform(prob);

// Set seed if provided
if let Some(s) = args.seed {
let noise_seed = derive_seed(s, "noise_model");
model.set_seed(noise_seed)?;
}

Box::new(model)
}
NoiseModelType::General => {
// Create a general noise model with five probabilities
let (prep, meas_0, meas_1, single_qubit, two_qubit) =
parse_general_noise_probabilities(args.noise_probability.as_ref());
let mut model = GeneralNoiseModel::new(prep, meas_0, meas_1, single_qubit, two_qubit);

// Set seed if provided
if let Some(s) = args.seed {
let noise_seed = derive_seed(s, "noise_model");
model.reset_with_seed(noise_seed).map_err(|e| {
Box::<dyn Error>::from(format!("Failed to set noise model seed: {e}"))
})?;
}

Box::new(model)
}
};

// Use the generic approach with the selected noise model
let results = MonteCarloEngine::run_with_noise_model(
classical_engine,
prob,
noise_model,
args.shots,
args.workers,
args.seed,
Expand Down Expand Up @@ -128,6 +277,7 @@ mod tests {
assert_eq!(args.seed, Some(42));
assert_eq!(args.shots, 100);
assert_eq!(args.workers, 2);
assert_eq!(args.noise_model, NoiseModelType::Depolarizing); // Default
}
Commands::Compile(_) => panic!("Expected Run command"),
}
Expand All @@ -142,6 +292,34 @@ mod tests {
assert_eq!(args.seed, None);
assert_eq!(args.shots, 100);
assert_eq!(args.workers, 2);
assert_eq!(args.noise_model, NoiseModelType::Depolarizing); // Default
}
Commands::Compile(_) => panic!("Expected Run command"),
}
}

#[test]
fn verify_cli_general_noise_model() {
let cmd = Cli::parse_from([
"pecos",
"run",
"program.json",
"--model",
"general",
"-p",
"0.01,0.02,0.03,0.04,0.05",
"-d",
"42",
]);

match cmd.command {
Commands::Run(args) => {
assert_eq!(args.seed, Some(42));
assert_eq!(args.noise_model, NoiseModelType::General);
assert_eq!(
args.noise_probability,
Some("0.01,0.02,0.03,0.04,0.05".to_string())
);
}
Commands::Compile(_) => panic!("Expected Run command"),
}
Expand Down
24 changes: 21 additions & 3 deletions crates/pecos-engines/QIR_RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@

The QIR (Quantum Intermediate Representation) compiler in PECOS uses a Rust runtime library to implement quantum operations. This library is automatically built by the `build.rs` script in the `pecos-engines` crate.

## Requirements

To use QIR functionality, you need:

- **LLVM version 14 specifically**:
- On Linux: Install using your package manager (e.g., `sudo apt install llvm-14`)
- On macOS: Install using Homebrew (`brew install llvm@14`)
- On Windows: Download and install LLVM 14.x from the [LLVM website](https://releases.llvm.org/download.html#14.0.0)

- **Required tools**:
- Linux/macOS: The `llc` compiler tool must be in your PATH
- Windows: The `clang` compiler must be in your PATH

**Note**: PECOS requires LLVM version 14.x specifically, not newer versions. LLVM 15 or later versions are not compatible with PECOS's QIR implementation.

If LLVM 14 is not installed or the required tools aren't found, QIR functionality will be disabled but the rest of PECOS will continue to work normally.

## How It Works

The `build.rs` script:

1. Runs automatically when building the `pecos-engines` crate
2. Checks if the QIR runtime library needs to be rebuilt
3. Builds the library only if necessary (if source files have changed)
4. Places the built library in both `target/debug` and `target/release` directories
2. Checks for LLVM 14+ dependencies
3. Checks if the QIR runtime library needs to be rebuilt
4. Builds the library only if necessary (if source files have changed)
5. Places the built library in both `target/debug` and `target/release` directories

When the QIR compiler runs, it looks for the pre-built library in these locations. If the library is not found, the compiler will attempt to build it by running `cargo build -p pecos-engines` before raising an error.

Expand Down
Loading