From 319504339ae222c733332a1e77b101987e851b6f Mon Sep 17 00:00:00 2001 From: Mathias Soeken Date: Wed, 9 Oct 2024 09:55:45 -0700 Subject: [PATCH] Error budget pruning strategy in resource estimator core (#1951) Resource estimation takes as input an error budget. This is a triplet of budget contribution to logical errors, rotation synthesis errors, and magic state distillation errors. The user can either specify each of these three values separately or provide a total value that is uniformly distributed among the three parts. So far, RE does not modify these provided values. This PR implements a pruning strategy for the error budget that will decrease the budget for logical errors and rotation synthesis errors (without affecting the estimation), while increasing the budget for magic state distillation errors in order to reduce the resources required for factories. For logical errors, we can reduce the budget by finding the largest value that still leads to the same code parameter (e.g., code distance). Similarly, for synthesis errors, we can reduce the budget by finding the largest value that still leads to the same number of magic states. The latter is layout dependent, and therefore a new function is added to the `Overhead` trait. The function has an empty default implementation, and therefore does not break existing layout implementations. The strategy needs to be explicitly set for the resource estimation and only works for a resource estimation without any other constraints (e.g., maximum number of qubits or duration), and does not work for frontier based estimation. There is no job parameter yet to enable the behavior. --- .../examples/basic_logical_counts.rs | 6 +- resource_estimator/src/estimates.rs | 2 +- .../src/estimates/error_budget.rs | 21 ++- resource_estimator/src/estimates/layout.rs | 7 +- .../src/estimates/physical_estimation.rs | 120 +++++++++----- .../physical_estimation/estimate_frontier.rs | 38 +++-- .../estimate_without_restrictions.rs | 86 ++++++++-- .../estimates/physical_estimation/result.rs | 7 +- resource_estimator/src/system.rs | 7 +- .../src/system/data/logical_counts.rs | 18 ++- resource_estimator/src/system/tests.rs | 152 ++++++++++-------- 11 files changed, 308 insertions(+), 156 deletions(-) diff --git a/resource_estimator/examples/basic_logical_counts.rs b/resource_estimator/examples/basic_logical_counts.rs index 1e1ed2227c..6190d6fee2 100644 --- a/resource_estimator/examples/basic_logical_counts.rs +++ b/resource_estimator/examples/basic_logical_counts.rs @@ -52,11 +52,13 @@ fn main() { // After we have set up all required inputs for the resource estimation // task, we can set up an estimation instance. - let estimation = PhysicalResourceEstimation::new(code, qubit, builder, logical_counts, budget); + let estimation = PhysicalResourceEstimation::new(code, qubit, builder, logical_counts); // In this example, we perform a standard estimation without any further // constraints. - let result = estimation.estimate().expect("estimation does not fail"); + let result = estimation + .estimate(&budget) + .expect("estimation does not fail"); // There is a lot of data contained in the resource estimation result // object, but in this sample we are only printing the total number of diff --git a/resource_estimator/src/estimates.rs b/resource_estimator/src/estimates.rs index 9d39df009b..b8c5d6f76a 100644 --- a/resource_estimator/src/estimates.rs +++ b/resource_estimator/src/estimates.rs @@ -4,7 +4,7 @@ mod error; pub use error::Error; mod error_budget; -pub use error_budget::ErrorBudget; +pub use error_budget::{ErrorBudget, ErrorBudgetStrategy}; mod error_correction; pub use error_correction::{ CodeWithThresholdAndDistance, CodeWithThresholdAndDistanceEvaluator, ErrorCorrection, diff --git a/resource_estimator/src/estimates/error_budget.rs b/resource_estimator/src/estimates/error_budget.rs index 22256e431d..1d2e993ff8 100644 --- a/resource_estimator/src/estimates/error_budget.rs +++ b/resource_estimator/src/estimates/error_budget.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct ErrorBudget { /// Probability of at least one logical error logical: f64, @@ -36,15 +36,34 @@ impl ErrorBudget { self.logical } + pub fn set_logical(&mut self, logical: f64) { + self.logical = logical; + } + /// Get the error budget's tstates. #[must_use] pub fn magic_states(&self) -> f64 { self.magic_states } + pub fn set_magic_states(&mut self, magic_states: f64) { + self.magic_states = magic_states; + } + /// Get the error budget's rotations. #[must_use] pub fn rotations(&self) -> f64 { self.rotations } + + pub fn set_rotations(&mut self, rotations: f64) { + self.rotations = rotations; + } +} + +#[derive(Default, Copy, Clone)] +pub enum ErrorBudgetStrategy { + #[default] + Static, + PruneLogicalAndRotations, } diff --git a/resource_estimator/src/estimates/layout.rs b/resource_estimator/src/estimates/layout.rs index 46af664d56..3f8f2a6280 100644 --- a/resource_estimator/src/estimates/layout.rs +++ b/resource_estimator/src/estimates/layout.rs @@ -3,7 +3,7 @@ use serde::Serialize; -use super::ErrorBudget; +use super::{ErrorBudget, ErrorBudgetStrategy}; /// Trait to model post-layout logical overhead pub trait Overhead { @@ -23,6 +23,11 @@ pub trait Overhead { /// The index is used to indicate the type of magic states and must be /// supported by available factory builders in the physical estimation. fn num_magic_states(&self, budget: &ErrorBudget, index: usize) -> u64; + + /// When implemented, prunes the error budget with respect to the provided + /// strategy + #[allow(unused_variables)] + fn prune_error_budget(&self, budget: &mut ErrorBudget, strategy: ErrorBudgetStrategy) {} } /// This is the realized logical overhead after applying an error budget. This diff --git a/resource_estimator/src/estimates/physical_estimation.rs b/resource_estimator/src/estimates/physical_estimation.rs index 0b70fe7dcd..232c7c5dca 100644 --- a/resource_estimator/src/estimates/physical_estimation.rs +++ b/resource_estimator/src/estimates/physical_estimation.rs @@ -5,7 +5,10 @@ mod estimate_frontier; mod estimate_without_restrictions; mod result; -use super::{Error, ErrorBudget, ErrorCorrection, Factory, FactoryBuilder, LogicalPatch, Overhead}; +use super::{ + Error, ErrorBudget, ErrorBudgetStrategy, ErrorCorrection, Factory, FactoryBuilder, + LogicalPatch, Overhead, +}; use std::{borrow::Cow, rc::Rc}; use estimate_frontier::EstimateFrontier; @@ -18,12 +21,12 @@ pub struct PhysicalResourceEstimation { qubit: Rc, factory_builder: Builder, layout_overhead: Rc, - error_budget: ErrorBudget, // optional constraint parameters logical_depth_factor: Option, max_factories: Option, max_duration: Option, max_physical_qubits: Option, + error_budget_strategy: ErrorBudgetStrategy, } impl< @@ -37,18 +40,17 @@ impl< qubit: Rc, factory_builder: Builder, layout_overhead: Rc, - error_budget: ErrorBudget, ) -> Self { Self { ftp, qubit, factory_builder, layout_overhead, - error_budget, logical_depth_factor: None, max_factories: None, max_duration: None, max_physical_qubits: None, + error_budget_strategy: ErrorBudgetStrategy::default(), } } @@ -60,10 +62,6 @@ impl< &self.layout_overhead } - pub fn error_budget(&self) -> &ErrorBudget { - &self.error_budget - } - pub fn set_logical_depth_factor(&mut self, logical_depth_factor: f64) { self.logical_depth_factor = Some(logical_depth_factor); } @@ -77,6 +75,14 @@ impl< self.max_physical_qubits = Some(max_physical_qubits); } + pub fn error_budget_strategy(&self) -> ErrorBudgetStrategy { + self.error_budget_strategy + } + + pub fn set_error_budget_strategy(&mut self, error_budget_strategy: ErrorBudgetStrategy) { + self.error_budget_strategy = error_budget_strategy; + } + pub fn factory_builder(&self) -> &Builder { &self.factory_builder } @@ -85,42 +91,52 @@ impl< &mut self.factory_builder } - pub fn estimate(&self) -> Result, Error> { + pub fn estimate( + &self, + error_budget: &ErrorBudget, + ) -> Result, Error> { match (self.max_duration, self.max_physical_qubits) { - (None, None) => self.estimate_without_restrictions(), + (None, None) => self.estimate_without_restrictions(error_budget), (None, Some(max_physical_qubits)) => { - self.estimate_with_max_num_qubits(max_physical_qubits) + self.estimate_with_max_num_qubits(error_budget, max_physical_qubits) + } + (Some(max_duration), None) => { + self.estimate_with_max_duration(error_budget, max_duration) } - (Some(max_duration), None) => self.estimate_with_max_duration(max_duration), _ => Err(Error::BothDurationAndPhysicalQubitsProvided), } } pub fn build_frontier( &self, + error_budget: &ErrorBudget, ) -> Result>, Error> { - EstimateFrontier::new(self)?.estimate() + EstimateFrontier::new(self, error_budget)?.estimate() } pub fn estimate_without_restrictions( &self, + error_budget: &ErrorBudget, ) -> Result, Error> { - EstimateWithoutRestrictions::new(self).estimate() + EstimateWithoutRestrictions::new(self).estimate(error_budget) } fn compute_initial_optimization_values( &self, + error_budget: &ErrorBudget, ) -> Result, Error> { - let num_cycles_required_by_layout_overhead = self.compute_num_cycles()?; + let num_cycles_required_by_layout_overhead = self.compute_num_cycles(error_budget)?; // The required magic state error rate is computed by dividing the total // error budget for magic states by the number of magic states required // for the algorithm. - let required_logical_magic_state_error_rate = self.error_budget.magic_states() - / (self.layout_overhead.num_magic_states(&self.error_budget, 0) as f64); + let required_logical_magic_state_error_rate = error_budget.magic_states() + / (self.layout_overhead.num_magic_states(error_budget, 0) as f64); - let required_logical_error_rate = - self.required_logical_error_rate(num_cycles_required_by_layout_overhead); + let required_logical_error_rate = self.required_logical_error_rate( + error_budget.logical(), + num_cycles_required_by_layout_overhead, + ); let min_code_parameter = self.compute_code_parameter(required_logical_error_rate)?; @@ -135,6 +151,7 @@ impl< #[allow(clippy::too_many_lines)] pub fn estimate_with_max_duration( &self, + error_budget: &ErrorBudget, max_duration_in_nanoseconds: u64, ) -> Result, Error> { if self.factory_builder.num_magic_state_types() != 1 { @@ -146,9 +163,9 @@ impl< num_cycles_required_by_layout_overhead, required_logical_error_rate, required_logical_magic_state_error_rate, - } = self.compute_initial_optimization_values()?; + } = self.compute_initial_optimization_values(error_budget)?; - let num_magic_states = self.layout_overhead.num_magic_states(&self.error_budget, 0); + let num_magic_states = self.layout_overhead.num_magic_states(error_budget, 0); if num_magic_states == 0 { let logical_patch = LogicalPatch::new(&self.ftp, min_code_parameter, self.qubit.clone())?; @@ -159,6 +176,7 @@ impl< return Ok(PhysicalResourceEstimationResult::without_factories( self, logical_patch, + error_budget, num_cycles_required_by_layout_overhead, required_logical_error_rate, )); @@ -191,7 +209,7 @@ impl< } let max_num_cycles_allowed_by_error_rate = - self.logical_cycles_for_code_parameter(&code_parameter)?; + self.logical_cycles_for_code_parameter(error_budget.logical(), &code_parameter)?; if max_num_cycles_allowed_by_error_rate < num_cycles_required_by_layout_overhead { continue; @@ -230,8 +248,13 @@ impl< &logical_patch, max_num_cycles_allowed, ) { - let num_factories = - self.num_factories(&logical_patch, 0, &factory, max_num_cycles_allowed); + let num_factories = self.num_factories( + &logical_patch, + 0, + &factory, + error_budget, + max_num_cycles_allowed, + ); let num_cycles_required_for_magic_states = self .compute_num_cycles_required_for_magic_states( @@ -239,6 +262,7 @@ impl< num_factories, &factory, &logical_patch, + error_budget, ); // This num_cycles could be larger than num_cycles_required_by_layout_overhead @@ -256,6 +280,7 @@ impl< let result = PhysicalResourceEstimationResult::new( self, LogicalPatch::new(&self.ftp, code_parameter.clone(), self.qubit.clone())?, + error_budget, num_cycles, vec![Some(FactoryPart::new( factory.into_owned(), @@ -281,6 +306,7 @@ impl< #[allow(clippy::too_many_lines)] pub fn estimate_with_max_num_qubits( &self, + error_budget: &ErrorBudget, max_num_qubits: u64, ) -> Result, Error> { if self.factory_builder.num_magic_state_types() != 1 { @@ -292,9 +318,9 @@ impl< num_cycles_required_by_layout_overhead, required_logical_error_rate, required_logical_magic_state_error_rate, - } = self.compute_initial_optimization_values()?; + } = self.compute_initial_optimization_values(error_budget)?; - let num_magic_states = self.layout_overhead.num_magic_states(&self.error_budget, 0); + let num_magic_states = self.layout_overhead.num_magic_states(error_budget, 0); if num_magic_states == 0 { let logical_patch = LogicalPatch::new(&self.ftp, min_code_parameter, self.qubit.clone())?; @@ -302,6 +328,7 @@ impl< return Ok(PhysicalResourceEstimationResult::without_factories( self, logical_patch, + error_budget, num_cycles_required_by_layout_overhead, required_logical_error_rate, )); @@ -335,7 +362,7 @@ impl< max_num_qubits - physical_qubits_for_algorithm; let max_num_cycles_allowed_by_error_rate = - self.logical_cycles_for_code_parameter(&code_parameter)?; + self.logical_cycles_for_code_parameter(error_budget.logical(), &code_parameter)?; if max_num_cycles_allowed_by_error_rate < num_cycles_required_by_layout_overhead { continue; @@ -384,6 +411,7 @@ impl< num_factories, &factory, &logical_patch, + error_budget, ); let num_cycles = num_cycles_required_for_magic_states @@ -402,6 +430,7 @@ impl< let result = PhysicalResourceEstimationResult::new( self, logical_patch, + error_budget, num_cycles, vec![Some(FactoryPart::new( factory.into_owned(), @@ -433,12 +462,13 @@ impl< num_factories: u64, factory: &Builder::Factory, logical_patch: &LogicalPatch, + error_budget: &ErrorBudget, ) -> u64 { let magic_states_per_run = num_factories * factory.num_output_states(); let required_runs = self .layout_overhead - .num_magic_states(&self.error_budget, magic_state_index) + .num_magic_states(error_budget, magic_state_index) .div_ceil(magic_states_per_run); let required_duration = required_runs * factory.duration(); @@ -497,16 +527,18 @@ impl< num_logical_patches * patch.physical_qubits() } + fn volume(&self, num_cycles: u64) -> u64 { + self.layout_overhead.logical_qubits() * num_cycles + } + /// Computes required logical error rate for a logical operation one one /// qubit /// /// The logical volume is the number of logical qubits times the number of /// cycles. We obtain the required logical error rate by dividing the error /// budget for logical operations by the volume. - fn required_logical_error_rate(&self, num_cycles: u64) -> f64 { - let volume = self.layout_overhead.logical_qubits() * num_cycles; - - self.error_budget.logical() / volume as f64 + fn required_logical_error_rate(&self, logical_error_budget: f64, num_cycles: u64) -> f64 { + logical_error_budget / self.volume(num_cycles) as f64 } /// Computes the code parameter for the required logical error rate @@ -519,6 +551,7 @@ impl< /// Computes the number of possible cycles given a chosen code parameter fn logical_cycles_for_code_parameter( &self, + logical_error_budget: f64, code_parameter: &E::Parameter, ) -> Result { // Compute the achievable error rate for the code parameter @@ -527,15 +560,16 @@ impl< .logical_error_rate(&self.qubit, code_parameter) .map_err(Error::LogicalErrorRateComputationFailed)?; - Ok((self.error_budget.logical() - / (self.layout_overhead.logical_qubits() as f64 * error_rate)) - .floor() as u64) + Ok( + (logical_error_budget / (self.layout_overhead.logical_qubits() as f64 * error_rate)) + .floor() as u64, + ) } // Possibly adjusts number of cycles C from initial starting point C_min - fn compute_num_cycles(&self) -> Result { + fn compute_num_cycles(&self, error_budget: &ErrorBudget) -> Result { // Start loop with C = C_min - let mut num_cycles = self.layout_overhead.logical_depth(&self.error_budget); + let mut num_cycles = self.layout_overhead.logical_depth(error_budget); // Perform logical depth scaling if given by constraint if let Some(logical_depth_scaling) = self.logical_depth_factor { @@ -545,11 +579,8 @@ impl< // We cannot perform resource estimation when there are neither magic states nor cycles if num_cycles == 0 - && (0..self.factory_builder.num_magic_state_types()).all(|index| { - self.layout_overhead - .num_magic_states(&self.error_budget, index) - == 0 - }) + && (0..self.factory_builder.num_magic_state_types()) + .all(|index| self.layout_overhead.num_magic_states(error_budget, index) == 0) { return Err(Error::AlgorithmHasNoResources); } @@ -565,6 +596,7 @@ impl< logical_patch: &LogicalPatch, magic_state_index: usize, factory: &Builder::Factory, + error_budget: &ErrorBudget, num_cycles: u64, ) -> u64 { // first, try with the exact calculation; if that does not work, use @@ -574,12 +606,12 @@ impl< let num_states_per_run = (total_duration / factory.duration()) * factory.num_output_states(); self.layout_overhead - .num_magic_states(&self.error_budget, magic_state_index) + .num_magic_states(error_budget, magic_state_index) .div_ceil(num_states_per_run) } else { let magic_states_per_cycles = self.layout_overhead - .num_magic_states(&self.error_budget, magic_state_index) as f64 + .num_magic_states(error_budget, magic_state_index) as f64 / (factory.num_output_states() * num_cycles) as f64; let factory_duration_fraction = diff --git a/resource_estimator/src/estimates/physical_estimation/estimate_frontier.rs b/resource_estimator/src/estimates/physical_estimation/estimate_frontier.rs index dc1047ddda..15f1582713 100644 --- a/resource_estimator/src/estimates/physical_estimation/estimate_frontier.rs +++ b/resource_estimator/src/estimates/physical_estimation/estimate_frontier.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ops::Deref}; use crate::estimates::{ optimization::{Point2D, Population}, - Error, ErrorCorrection, Factory, FactoryBuilder, LogicalPatch, Overhead, + Error, ErrorBudget, ErrorCorrection, Factory, FactoryBuilder, LogicalPatch, Overhead, }; use super::{ @@ -12,6 +12,7 @@ use super::{ pub struct EstimateFrontier<'a, E: ErrorCorrection, B: FactoryBuilder, L> { estimator: &'a PhysicalResourceEstimation, + error_budget: ErrorBudget, min_cycles: u64, required_logical_error_rate: f64, required_logical_magic_state_error_rate: f64, @@ -25,26 +26,27 @@ impl< L: Overhead, > EstimateFrontier<'a, E, B, L> { - pub fn new(estimator: &'a PhysicalResourceEstimation) -> Result { + pub fn new( + estimator: &'a PhysicalResourceEstimation, + error_budget: &ErrorBudget, + ) -> Result { if estimator.factory_builder.num_magic_state_types() == 1 { - let min_cycles = estimator.compute_num_cycles()?; + let min_cycles = estimator.compute_num_cycles(error_budget)?; - let required_logical_error_rate = estimator.required_logical_error_rate(min_cycles); + let required_logical_error_rate = + estimator.required_logical_error_rate(error_budget.logical(), min_cycles); // The required magic state error rate is computed by dividing the total // error budget for magic states by the number of magic states required // for the algorithm. - let required_logical_magic_state_error_rate = estimator.error_budget.magic_states() - / estimator - .layout_overhead - .num_magic_states(&estimator.error_budget, 0) as f64; + let required_logical_magic_state_error_rate = error_budget.magic_states() + / estimator.layout_overhead.num_magic_states(error_budget, 0) as f64; - let num_magic_states = estimator - .layout_overhead - .num_magic_states(&estimator.error_budget, 0); + let num_magic_states = estimator.layout_overhead.num_magic_states(error_budget, 0); Ok(Self { estimator, + error_budget: error_budget.clone(), min_cycles, required_logical_error_rate, required_logical_magic_state_error_rate, @@ -65,6 +67,7 @@ impl< return Ok(vec![PhysicalResourceEstimationResult::without_factories( self, logical_patch, + &self.error_budget, self.min_cycles, self.required_logical_error_rate, )]); @@ -139,7 +142,7 @@ impl< LogicalPatch::new(&self.ftp, code_parameter.clone(), self.qubit.clone())?; let max_num_cycles_allowed_by_error_rate = - self.logical_cycles_for_code_parameter(code_parameter)?; + self.logical_cycles_for_code_parameter(self.error_budget.logical(), code_parameter)?; if max_num_cycles_allowed_by_error_rate < self.min_cycles { return Ok(()); @@ -157,8 +160,13 @@ impl< // Here we compute the number of factories required limited by the // maximum number of cycles allowed by the duration constraint (and // the error rate). - let min_num_factories = - self.num_factories(&logical_patch, 0, &factory, max_num_cycles_allowed); + let min_num_factories = self.num_factories( + &logical_patch, + 0, + &factory, + &self.error_budget, + max_num_cycles_allowed, + ); for num_factories in min_num_factories.. { let num_cycles_required_for_magic_states = self @@ -167,6 +175,7 @@ impl< num_factories, factory.as_ref(), &logical_patch, + &self.error_budget, ); // This num_cycles could be larger than min_cycles but must @@ -185,6 +194,7 @@ impl< let result = PhysicalResourceEstimationResult::new( self, LogicalPatch::new(&self.ftp, code_parameter.clone(), self.qubit.clone())?, + &self.error_budget, num_cycles, vec![Some(factory_part)], self.required_logical_error_rate, diff --git a/resource_estimator/src/estimates/physical_estimation/estimate_without_restrictions.rs b/resource_estimator/src/estimates/physical_estimation/estimate_without_restrictions.rs index 9d6a874ea3..b9534293c0 100644 --- a/resource_estimator/src/estimates/physical_estimation/estimate_without_restrictions.rs +++ b/resource_estimator/src/estimates/physical_estimation/estimate_without_restrictions.rs @@ -1,6 +1,9 @@ use std::{borrow::Cow, ops::Deref}; -use crate::estimates::{Error, ErrorCorrection, Factory, FactoryBuilder, LogicalPatch, Overhead}; +use crate::estimates::{ + Error, ErrorBudget, ErrorBudgetStrategy, ErrorCorrection, Factory, FactoryBuilder, + LogicalPatch, Overhead, +}; use super::{ FactoryForCycles, FactoryPart, PhysicalResourceEstimation, PhysicalResourceEstimationResult, @@ -21,14 +24,48 @@ impl< Self { estimator } } - pub fn estimate(&self) -> Result, Error> { - let mut num_cycles = self.compute_num_cycles()?; + pub fn estimate( + &self, + error_budget: &ErrorBudget, + ) -> Result, Error> { + let mut num_cycles = self.compute_num_cycles(error_budget)?; + + // NOTE: for now we reset the error_budget_strategy if also + // max_factories is set, because it may lead to an inconsistent + // configuration. + let adjusted_strategy = if self.max_factories.is_some() { + ErrorBudgetStrategy::Static + } else { + self.error_budget_strategy + }; loop { - let required_logical_error_rate = self.required_logical_error_rate(num_cycles); + let mut error_budget = error_budget.clone(); + + self.layout_overhead() + .prune_error_budget(&mut error_budget, self.error_budget_strategy()); + + let required_logical_error_rate = + self.required_logical_error_rate(error_budget.logical(), num_cycles); let code_parameter = self.compute_code_parameter(required_logical_error_rate)?; - let max_allowed_num_cycles_for_code_parameter = - self.logical_cycles_for_code_parameter(&code_parameter)?; + + let max_allowed_num_cycles_for_code_parameter = match adjusted_strategy { + ErrorBudgetStrategy::Static => { + self.logical_cycles_for_code_parameter(error_budget.logical(), &code_parameter)? + } + ErrorBudgetStrategy::PruneLogicalAndRotations => { + let new_logical = self + .ftp + .logical_error_rate(&self.qubit, &code_parameter) + .map_err(Error::LogicalErrorRateComputationFailed)? + * (self.volume(num_cycles) as f64); + let diff = error_budget.logical() - new_logical; + error_budget.set_logical(new_logical); + let new_magic_states = error_budget.magic_states() + diff; + error_budget.set_magic_states(new_magic_states); + num_cycles + } + }; let logical_patch = LogicalPatch::new(&self.ftp, code_parameter.clone(), self.qubit.clone())?; @@ -40,6 +77,7 @@ impl< &logical_patch, num_cycles, max_allowed_num_cycles_for_code_parameter, + &error_budget, index, )? { FactoryPartsResult::NoMagicStates => { @@ -62,6 +100,7 @@ impl< return Ok(PhysicalResourceEstimationResult::new( self, logical_patch, + &error_budget, num_cycles, factory_parts, required_logical_error_rate, @@ -77,17 +116,16 @@ impl< logical_patch: &LogicalPatch, min_cycles: u64, max_cycles: u64, + error_budget: &ErrorBudget, index: usize, ) -> Result, Error> { - let num_magic_states = self - .layout_overhead - .num_magic_states(&self.error_budget, index); + let num_magic_states = self.layout_overhead.num_magic_states(error_budget, index); if num_magic_states == 0 { return Ok(FactoryPartsResult::NoMagicStates); } - let required_logical_magic_state_error_rate = (self.error_budget.magic_states() + let required_logical_magic_state_error_rate = (error_budget.magic_states() / self.factory_builder.num_magic_state_types() as f64) / (num_magic_states as f64); @@ -111,10 +149,21 @@ impl< if let Some(FactoryForCycles { factory, num_cycles: num_cycles_required, - }) = self.find_factory(index, &factories, logical_patch, min_cycles, max_cycles) - { - let num_factories = - self.num_factories(logical_patch, index, &factory, num_cycles_required); + }) = self.find_factory( + index, + &factories, + logical_patch, + error_budget, + min_cycles, + max_cycles, + ) { + let num_factories = self.num_factories( + logical_patch, + index, + &factory, + error_budget, + num_cycles_required, + ); Ok(FactoryPartsResult::Success { factory_part: FactoryPart::new( factory.into_owned(), @@ -134,6 +183,7 @@ impl< magic_state_index: usize, factories: &[Cow<'b, B::Factory>], logical_patch: &LogicalPatch, + error_budget: &ErrorBudget, min_cycles: u64, max_cycles: u64, ) -> Option> { @@ -147,6 +197,7 @@ impl< && self.is_max_factories_constraint_satisfied( logical_patch, factory, + error_budget, min_cycles, ) }) @@ -161,6 +212,7 @@ impl< magic_state_index, factories, logical_patch, + error_budget, max_cycles, ) { return Some(factory); @@ -174,6 +226,7 @@ impl< magic_state_index: usize, factories: &[Cow<'b, B::Factory>], logical_patch: &LogicalPatch, + error_budget: &ErrorBudget, max_cycles: u64, ) -> Option> { self.max_factories.map_or_else( @@ -196,7 +249,7 @@ impl< let magic_states_per_run = max_factories * factory.num_output_states(); let required_runs = self .layout_overhead - .num_magic_states(&self.error_budget, magic_state_index) + .num_magic_states(error_budget, magic_state_index) .div_ceil(magic_states_per_run); let required_duration = required_runs * factory.duration(); let num = required_duration.div_ceil(logical_patch.logical_cycle_time()); @@ -215,10 +268,11 @@ impl< &self, logical_patch: &LogicalPatch, factory: &B::Factory, + error_budget: &ErrorBudget, num_cycles: u64, ) -> bool { self.max_factories.map_or(true, |max_factories| { - max_factories >= self.num_factories(logical_patch, 0, factory, num_cycles) + max_factories >= self.num_factories(logical_patch, 0, factory, error_budget, num_cycles) }) } } diff --git a/resource_estimator/src/estimates/physical_estimation/result.rs b/resource_estimator/src/estimates/physical_estimation/result.rs index 08691b8ce1..cf4937182c 100644 --- a/resource_estimator/src/estimates/physical_estimation/result.rs +++ b/resource_estimator/src/estimates/physical_estimation/result.rs @@ -37,6 +37,7 @@ impl, F: Factory, logical_patch: LogicalPatch, + error_budget: &ErrorBudget, num_cycles: u64, factory_parts: Vec>>, required_logical_error_rate: f64, @@ -71,10 +72,10 @@ impl, F: Factory, F: Factory, logical_patch: LogicalPatch, + error_budget: &ErrorBudget, num_cycles: u64, required_logical_patch_error_rate: f64, ) -> Self { Self::new( estimation, logical_patch, + error_budget, num_cycles, std::iter::repeat(()) .map(|()| None) diff --git a/resource_estimator/src/system.rs b/resource_estimator/src/system.rs index dda190d7e4..ea49779b85 100644 --- a/resource_estimator/src/system.rs +++ b/resource_estimator/src/system.rs @@ -98,7 +98,6 @@ fn estimate_single { - let estimation_result = estimation.estimate().map_err(std::convert::Into::into); + let estimation_result = estimation + .estimate(&partitioning) + .map_err(std::convert::Into::into); estimation_result .map(|result| data::Success::new(job_params, logical_resources, result)) } diff --git a/resource_estimator/src/system/data/logical_counts.rs b/resource_estimator/src/system/data/logical_counts.rs index a4dfde3df9..f61dc19ba9 100644 --- a/resource_estimator/src/system/data/logical_counts.rs +++ b/resource_estimator/src/system/data/logical_counts.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::{ - estimates::{ErrorBudget, Overhead}, + estimates::{ErrorBudget, ErrorBudgetStrategy, Overhead}, system::constants::{ NUM_MEASUREMENTS_PER_R, NUM_MEASUREMENTS_PER_TOF, NUM_TS_PER_ROTATION_A_COEFFICIENT, NUM_TS_PER_ROTATION_B_COEFFICIENT, @@ -77,6 +77,22 @@ impl Overhead for LogicalResourceCounts { .unwrap_or_default() * self.rotation_count } + + fn prune_error_budget(&self, budget: &mut ErrorBudget, strategy: ErrorBudgetStrategy) { + if matches![strategy, ErrorBudgetStrategy::PruneLogicalAndRotations] { + if let Some(num_ts_per_rotation) = self.num_ts_per_rotation(budget.rotations()) { + let new_rotations_budget = (self.rotation_count as f64) + / 2.0_f64.powf( + ((num_ts_per_rotation as f64) - NUM_TS_PER_ROTATION_B_COEFFICIENT) + / NUM_TS_PER_ROTATION_A_COEFFICIENT, + ); + + let diff = budget.rotations() - new_rotations_budget; + budget.set_rotations(new_rotations_budget); + budget.set_magic_states(budget.magic_states() + diff); + } + } + } } impl PartitioningOverhead for LogicalResourceCounts { diff --git a/resource_estimator/src/system/tests.rs b/resource_estimator/src/system/tests.rs index 3cdb50657f..842b00c2af 100644 --- a/resource_estimator/src/system/tests.rs +++ b/resource_estimator/src/system/tests.rs @@ -5,8 +5,8 @@ use serde_json::{json, Map, Value}; use crate::{ estimates::{ - ErrorBudget, ErrorCorrection, Factory, FactoryBuilder, FactoryPart, Overhead, - PhysicalResourceEstimation, PhysicalResourceEstimationResult, + ErrorBudget, ErrorBudgetStrategy, ErrorCorrection, Factory, FactoryBuilder, FactoryPart, + Overhead, PhysicalResourceEstimation, PhysicalResourceEstimationResult, }, system::modeling::{floquet_code, surface_code_gate_based}, }; @@ -148,10 +148,9 @@ pub fn test_no_tstates() { qubit, create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); - assert!(estimation.estimate().is_err()); + assert!(estimation.estimate(&partitioning).is_err()); } #[test] @@ -167,10 +166,9 @@ pub fn single_tstate() -> Result<()> { qubit, create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); - estimation.estimate()?; + estimation.estimate(&partitioning)?; Ok(()) } @@ -191,10 +189,9 @@ pub fn perfect_tstate() -> Result<()> { qubit, create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); - estimation.estimate()?; + estimation.estimate(&partitioning)?; Ok(()) } @@ -237,10 +234,9 @@ pub fn test_hubbard_e2e() -> Result<()> { qubit.clone(), create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); - let result = estimation.estimate()?; + let result = estimation.estimate(&partitioning)?; let part = get_factory(&result); let logical_qubit = result.logical_patch(); @@ -334,10 +330,9 @@ pub fn test_hubbard_e2e_measurement_based() -> Result<()> { qubit.clone(), create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); - let result = estimation.estimate()?; + let result = estimation.estimate(&partitioning)?; let part = get_factory(&result); let logical_qubit = result.logical_patch(); @@ -428,14 +423,15 @@ pub fn test_hubbard_e2e_increasing_max_duration() -> Result<()> { qubit, create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); let max_duration_in_nanoseconds1: u64 = 50_000_000_u64; let max_duration_in_nanoseconds2: u64 = 500_000_000_u64; - let result1 = estimation.estimate_with_max_duration(max_duration_in_nanoseconds1)?; - let result2 = estimation.estimate_with_max_duration(max_duration_in_nanoseconds2)?; + let result1 = + estimation.estimate_with_max_duration(&partitioning, max_duration_in_nanoseconds1)?; + let result2 = + estimation.estimate_with_max_duration(&partitioning, max_duration_in_nanoseconds2)?; assert!(result1.runtime() <= max_duration_in_nanoseconds1); assert!(result2.runtime() <= max_duration_in_nanoseconds2); @@ -456,14 +452,13 @@ pub fn test_hubbard_e2e_increasing_max_num_qubits() -> Result<()> { qubit, create_factory_builder(), Rc::new(layout_overhead), - partitioning, ); let max_num_qubits1: u64 = 11000; let max_num_qubits2: u64 = 20000; - let result1 = estimation.estimate_with_max_num_qubits(max_num_qubits1)?; - let result2 = estimation.estimate_with_max_num_qubits(max_num_qubits2)?; + let result1 = estimation.estimate_with_max_num_qubits(&partitioning, max_num_qubits1)?; + let result2 = estimation.estimate_with_max_num_qubits(&partitioning, max_num_qubits2)?; assert!(result1.physical_qubits() <= max_num_qubits1); assert!(result2.physical_qubits() <= max_num_qubits2); @@ -474,8 +469,10 @@ pub fn test_hubbard_e2e_increasing_max_num_qubits() -> Result<()> { Ok(()) } -fn prepare_chemistry_estimation_with_expected_majorana( -) -> PhysicalResourceEstimation { +fn prepare_chemistry_estimation_with_expected_majorana() -> ( + PhysicalResourceEstimation, + ErrorBudget, +) { let ftp = floquet_code(); let qubit = Rc::new(PhysicalQubit::qubit_maj_ns_e6()); @@ -494,11 +491,8 @@ fn prepare_chemistry_estimation_with_expected_majorana( let partitioning = ErrorBudgetSpecification::Total(0.01) .partitioning(&counts) .expect("partitioning should succeed"); - PhysicalResourceEstimation::new( - ftp, - qubit, - create_factory_builder(), - Rc::new(counts), + ( + PhysicalResourceEstimation::new(ftp, qubit, create_factory_builder(), Rc::new(counts)), partitioning, ) } @@ -507,9 +501,9 @@ fn prepare_chemistry_estimation_with_expected_majorana( pub fn test_chemistry_small_max_duration() { let max_duration_in_nanoseconds: u64 = 1_000_000_000_u64; - let estimation = prepare_chemistry_estimation_with_expected_majorana(); + let (estimation, budget) = prepare_chemistry_estimation_with_expected_majorana(); - let result = estimation.estimate_with_max_duration(max_duration_in_nanoseconds); + let result = estimation.estimate_with_max_duration(&budget, max_duration_in_nanoseconds); match result { Err(crate::estimates::Error::MaxDurationTooSmall) => {} @@ -520,9 +514,9 @@ pub fn test_chemistry_small_max_duration() { #[test] pub fn test_chemistry_small_max_num_qubits() { let max_num_qubits: u64 = 10_000; - let estimation = prepare_chemistry_estimation_with_expected_majorana(); + let (estimation, budget) = prepare_chemistry_estimation_with_expected_majorana(); - let result = estimation.estimate_with_max_num_qubits(max_num_qubits); + let result = estimation.estimate_with_max_num_qubits(&budget, max_num_qubits); match result { Err(crate::estimates::Error::MaxPhysicalQubitsTooSmall) => {} @@ -534,9 +528,9 @@ pub fn test_chemistry_small_max_num_qubits() { pub fn test_chemistry_based_max_duration() -> Result<()> { let max_duration_in_nanoseconds: u64 = 365 * 24 * 3600 * 1_000_000_000_u64; - let estimation = prepare_chemistry_estimation_with_expected_majorana(); + let (estimation, budget) = prepare_chemistry_estimation_with_expected_majorana(); - let result = estimation.estimate_with_max_duration(max_duration_in_nanoseconds)?; + let result = estimation.estimate_with_max_duration(&budget, max_duration_in_nanoseconds)?; let part = get_factory(&result); let logical_qubit = result.logical_patch(); @@ -591,9 +585,9 @@ pub fn test_chemistry_based_max_duration() -> Result<()> { pub fn test_chemistry_based_max_num_qubits() -> Result<()> { let max_num_qubits: u64 = 4_923_120; - let estimation = prepare_chemistry_estimation_with_expected_majorana(); + let (estimation, budget) = prepare_chemistry_estimation_with_expected_majorana(); - let result = estimation.estimate_with_max_num_qubits(max_num_qubits)?; + let result = estimation.estimate_with_max_num_qubits(&budget, max_num_qubits)?; let part = get_factory(&result); let logical_qubit = result.logical_patch(); @@ -643,8 +637,10 @@ pub fn test_chemistry_based_max_num_qubits() -> Result<()> { Ok(()) } -fn prepare_factorization_estimation_with_optimistic_majorana( -) -> PhysicalResourceEstimation { +fn prepare_factorization_estimation_with_optimistic_majorana() -> ( + PhysicalResourceEstimation, + ErrorBudget, +) { let ftp = floquet_code(); let qubit = Rc::new(PhysicalQubit::qubit_maj_ns_e6()); @@ -663,26 +659,23 @@ fn prepare_factorization_estimation_with_optimistic_majorana( let partitioning = ErrorBudgetSpecification::Total(1e-3) .partitioning(&counts) .expect("partitioning should succeed"); - PhysicalResourceEstimation::new( - ftp, - qubit, - create_factory_builder(), - Rc::new(counts), + ( + PhysicalResourceEstimation::new(ftp, qubit, create_factory_builder(), Rc::new(counts)), partitioning, ) } #[test] pub fn test_factorization_2048_max_duration_matches_regular_estimate() -> Result<()> { - let estimation = prepare_factorization_estimation_with_optimistic_majorana(); + let (estimation, budget) = prepare_factorization_estimation_with_optimistic_majorana(); - let result_no_max_duration = estimation.estimate_without_restrictions()?; + let result_no_max_duration = estimation.estimate_without_restrictions(&budget)?; let part_no_max_duration = get_factory(&result_no_max_duration); let logical_qubit_no_max_duration = result_no_max_duration.logical_patch(); let max_duration_in_nanoseconds: u64 = result_no_max_duration.runtime(); - let result = estimation.estimate_with_max_duration(max_duration_in_nanoseconds)?; + let result = estimation.estimate_with_max_duration(&budget, max_duration_in_nanoseconds)?; let part = get_factory(&result); let logical_qubit = result.logical_patch(); @@ -726,15 +719,15 @@ pub fn test_factorization_2048_max_duration_matches_regular_estimate() -> Result #[test] pub fn test_factorization_2048_max_num_qubits_matches_regular_estimate() -> Result<()> { - let estimation = prepare_factorization_estimation_with_optimistic_majorana(); + let (estimation, budget) = prepare_factorization_estimation_with_optimistic_majorana(); - let result_no_max_num_qubits = estimation.estimate_without_restrictions()?; + let result_no_max_num_qubits = estimation.estimate_without_restrictions(&budget)?; let part_no_max_num_qubits = get_factory(&result_no_max_num_qubits); let logical_qubit_no_max_num_qubits = result_no_max_num_qubits.logical_patch(); let max_num_qubits = result_no_max_num_qubits.physical_qubits(); - let result = estimation.estimate_with_max_num_qubits(max_num_qubits)?; + let result = estimation.estimate_with_max_num_qubits(&budget, max_num_qubits)?; let part = get_factory(&result); let logical_qubit = result.logical_patch(); @@ -776,8 +769,10 @@ pub fn test_factorization_2048_max_num_qubits_matches_regular_estimate() -> Resu Ok(()) } -fn prepare_ising20x20_estimation_with_pessimistic_gate_based( -) -> PhysicalResourceEstimation { +fn prepare_ising20x20_estimation_with_pessimistic_gate_based() -> ( + PhysicalResourceEstimation, + ErrorBudget, +) { let ftp = surface_code_gate_based(); let qubit = Rc::new(PhysicalQubit::qubit_gate_us_e3()); @@ -796,11 +791,8 @@ fn prepare_ising20x20_estimation_with_pessimistic_gate_based( let partitioning = ErrorBudgetSpecification::Total(1e-3) .partitioning(&counts) .expect("cannot setup error budget partitioning"); - PhysicalResourceEstimation::new( - ftp, - qubit, - create_factory_builder(), - Rc::new(counts), + ( + PhysicalResourceEstimation::new(ftp, qubit, create_factory_builder(), Rc::new(counts)), partitioning, ) } @@ -808,10 +800,10 @@ fn prepare_ising20x20_estimation_with_pessimistic_gate_based( #[test] fn test_chemistry_based_max_factories() { for max_factories in 1..=14 { - let mut estimation = prepare_chemistry_estimation_with_expected_majorana(); + let (mut estimation, budget) = prepare_chemistry_estimation_with_expected_majorana(); estimation.set_max_factories(max_factories); - let result = estimation.estimate().expect("failed to estimate"); + let result = estimation.estimate(&budget).expect("failed to estimate"); let actual_factories = result.factory_parts()[0] .as_ref() .expect("has factories") @@ -824,11 +816,31 @@ fn test_chemistry_based_max_factories() { } } +#[test] +fn test_budget_pruning() { + let (mut estimation, budget) = prepare_ising20x20_estimation_with_pessimistic_gate_based(); + estimation.set_error_budget_strategy(ErrorBudgetStrategy::Static); + let result1 = estimation.estimate(&budget).expect("failed to estimate"); + + estimation.set_error_budget_strategy(ErrorBudgetStrategy::PruneLogicalAndRotations); + let result2 = estimation.estimate(&budget).expect("failed to estimate"); + + assert_eq!(result1.physical_qubits(), 2_938_540); + assert_eq!(result1.runtime(), 1_734_282_000_000); + + assert_eq!(result2.physical_qubits(), 2_154_380); + assert_eq!(result2.runtime(), 1_734_282_000_000); + + assert!(result1.error_budget().logical() >= result2.error_budget().logical()); + assert!(result1.error_budget().rotations() >= result2.error_budget().rotations()); + assert!(result1.error_budget().magic_states() <= result2.error_budget().magic_states()); +} + #[test] fn build_frontier_test() { - let estimation = prepare_ising20x20_estimation_with_pessimistic_gate_based(); + let (estimation, budget) = prepare_ising20x20_estimation_with_pessimistic_gate_based(); - let frontier_result = estimation.build_frontier(); + let frontier_result = estimation.build_frontier(&budget); let points = frontier_result.expect("failed to estimate"); assert_eq!(points.len(), 189); @@ -843,7 +855,7 @@ fn build_frontier_test() { ); } - let shortest_runtime_result = estimation.estimate().expect("failed to estimate"); + let shortest_runtime_result = estimation.estimate(&budget).expect("failed to estimate"); assert_eq!(points[0].runtime(), shortest_runtime_result.runtime()); assert_eq!( points[0].physical_qubits(), @@ -864,7 +876,7 @@ fn build_frontier_test() { for _ in 0..num_iterations { max_duration = (max_duration as f64 * coefficient) as u64; let result = estimation - .estimate_with_max_duration(max_duration) + .estimate_with_max_duration(&budget, max_duration) .expect("failed to estimate"); assert!( @@ -882,7 +894,7 @@ fn build_frontier_test() { let coefficient = 1.10; for _ in 0..num_iterations { max_num_qubits = (max_num_qubits as f64 / coefficient) as u64; - let result = estimation.estimate_with_max_num_qubits(max_num_qubits); + let result = estimation.estimate_with_max_num_qubits(&budget, max_num_qubits); if let Ok(result) = result { assert!( @@ -897,8 +909,10 @@ fn build_frontier_test() { } } -fn prepare_bit_flip_code_resources_and_majorana_n6_qubit( -) -> PhysicalResourceEstimation { +fn prepare_bit_flip_code_resources_and_majorana_n6_qubit() -> ( + PhysicalResourceEstimation, + ErrorBudget, +) { let qubit = Rc::new(PhysicalQubit::qubit_maj_ns_e6()); let ftp = floquet_code(); @@ -917,27 +931,23 @@ fn prepare_bit_flip_code_resources_and_majorana_n6_qubit( let partitioning = ErrorBudgetSpecification::Total(1e-3) .partitioning(&counts) .expect("cannot setup error budget partitioning"); - PhysicalResourceEstimation::new( - ftp, - qubit, - create_factory_builder(), - Rc::new(counts), + ( + PhysicalResourceEstimation::new(ftp, qubit, create_factory_builder(), Rc::new(counts)), partitioning, ) } #[test] fn build_frontier_bit_flip_code_test() { - let estimation: PhysicalResourceEstimation = - prepare_bit_flip_code_resources_and_majorana_n6_qubit(); + let (estimation, budget) = prepare_bit_flip_code_resources_and_majorana_n6_qubit(); - let frontier_result = estimation.build_frontier(); + let frontier_result = estimation.build_frontier(&budget); let points = frontier_result.expect("failed to estimate"); assert_eq!(points.len(), 10); let part0 = get_factory(&points[0]); - let shortest_runtime_result = estimation.estimate().expect("failed to estimate"); + let shortest_runtime_result = estimation.estimate(&budget).expect("failed to estimate"); let part_shortest_runtime = get_factory(&shortest_runtime_result); assert_eq!(points[0].runtime(), shortest_runtime_result.runtime());