Repository: microsoft/qsharp
Pull request #977 (2024-01-05 21:58:10)

Updates for latest bug fixes and feature tweaks

github-actions[bot]: ## Benchmark for 690d5c4
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 970.4±30.97µs | **956.5±12.37µs** | **-1.43%** |
| Array update evaluation | 1028.6±5.78µs | **1015.2±6.43µs** | **-1.30%** |
| Deutsch-Jozsa evaluation | 31.0±0.28ms | 31.0±0.24ms | 0.00% |
| Large file parity evaluation | 37.3±0.23ms | 37.2±0.20ms | -0.27% |
| Large input file | **37.5±0.67ms** | 38.3±1.17ms | **+2.13%** |
| Standard library | 20.7±0.38ms | 20.9±0.39ms | +0.97% |
| Teleport evaluation | 112.5±2.23µs | 111.8±2.35µs | -0.62% |

  </details>
  

Patch:
diff --git a/compiler/qsc/src/interpret/stateful/re.rs b/compiler/qsc/src/interpret/stateful/re.rs
index 04e33ec6..d990570e 100644
--- a/compiler/qsc/src/interpret/stateful/re.rs
+++ b/compiler/qsc/src/interpret/stateful/re.rs
@@ -54,7 +54,11 @@ pub fn estimate_expr(
     let mut out = GenericReceiver::new(&mut stdout);
     interpreter
         .run_with_sim(&mut counter, &mut out, expr, 1)
-        .map_err(|e| e.into_iter().map(Error::Interpreter).collect::<Vec<_>>())?;
+        .map_err(|e| e.into_iter().map(Error::Interpreter).collect::<Vec<_>>())?
+        .into_iter()
+        .last()
+        .expect("execution should have at least one result")
+        .map_err(|e| vec![Error::Interpreter(e[0].clone())])?;
     estimate_physical_resources(&counter.logical_resources(), params)
         .map_err(|e| vec![Error::Estimation(e)])
 }
diff --git a/compiler/qsc/src/interpret/stateful/re/counts.rs b/compiler/qsc/src/interpret/stateful/re/counts.rs
index c0879b9f..045f5415 100644
--- a/compiler/qsc/src/interpret/stateful/re/counts.rs
+++ b/compiler/qsc/src/interpret/stateful/re/counts.rs
@@ -47,6 +47,8 @@ pub struct LogicalCounter {
     caching_stack: Vec<String>,
     /// Caching
     caching_layers: FxHashMap<String, LayerCache>,
+    /// Repeating
+    repeats: Vec<RepeatEntry>,
     /// Random number generator
     rnd: RefCell<StdRng>,
 }
@@ -65,6 +67,7 @@ impl Default for LogicalCounter {
             allocation_barrier: 0,
             caching_stack: vec![],
             caching_layers: FxHashMap::default(),
+            repeats: vec![],
             rnd: RefCell::new(StdRng::seed_from_u64(0)),
         }
     }
@@ -222,6 +225,74 @@ impl LogicalCounter {
         Ok(())
     }
 
+    pub fn begin_repeat(&mut self, count: i64) -> Result<(), String> {
+        let start_depth = self.global_barrier();
+
+        self.repeats.push(RepeatEntry {
+            count: count
+                .try_into()
+                .map_err(|_| format!("Estimate count {count} is too large to fit in a usize.",))?,
+            start_depth,
+            m_count: self.m_count,
+        });
+
+        Ok(())
+    }
+
+    #[allow(clippy::similar_names)]
+    pub fn end_repeat(&mut self) {
+        if let Some(RepeatEntry {
+            count,
+            start_depth,
+            m_count,
+        }) = self.repeats.pop()
+        {
+            if count == 0 {
+                return;
+            }
+
+            let end_depth = self.global_barrier();
+
+            let range = &self.layers[start_depth..end_depth];
+            let sum: LayerInfo = range.iter().sum();
+
+            // We skip one iteration, which was already done explicitly between
+            // begin_repeat and end_repeat
+            let r_depth = range.iter().filter(|l| l.r != 0).count();
+            let combined_r_depth = r_depth * (count - 1);
+            let combined_t_count = sum.t * (count - 1);
+            let combined_r_count = sum.r * (count - 1);
+            let combined_ccz_count = sum.ccz * (count - 1);
+            let combined_m_count = (self.m_count - m_count) * (count - 1);
+
+            if r_depth > 0 {
+                let first_layer_r_count = combined_r_count - (combined_r_depth - 1);
+
+                self.layers.push(LayerInfo {
+                    ccz: combined_ccz_count,
+                    r: first_layer_r_count,
+                    t: combined_t_count,
+                });
+                for _ in 1..combined_r_depth {
+                    self.layers.push(LayerInfo::new_with_r());
+                }
+            } else {
+                self.layers.push(LayerInfo {
+                    ccz: combined_ccz_count,
+                    r: combined_r_count,
+                    t: combined_t_count,
+                });
+            }
+
+            self.t_count += combined_t_count;
+            self.r_count += combined_r_count;
+            self.ccz_count += combined_ccz_count;
+            self.m_count += combined_m_count;
+
+            self.global_barrier();
+        }
+    }
+
     fn add_estimate(
         &mut self,
         estimates: &[(i64, i64)],
@@ -467,6 +538,14 @@ impl Backend for LogicalCounter {
                 ))))
             }
             "EndEstimateCaching" => Some(self.end_caching().map(|()| Value::unit())),
+            "BeginRepeatEstimatesInternal" => {
+                let count = arg.unwrap_int();
+                Some(self.begin_repeat(count).map(|()| Value::unit()))
+            }
+            "EndRepeatEstimatesInternal" => {
+                self.end_repeat();
+                Some(Ok(Value::unit()))
+            }
             "AccountForEstimatesInternal" => {
                 let values = arg.unwrap_tuple();
                 let [estimates, layout, qubits] = array::from_fn(|i| values[i].clone());
@@ -544,3 +623,9 @@ enum LayerCache {
         m_count: usize,
     },
 }
+
+struct RepeatEntry {
+    count: usize,
+    start_depth: usize,
+    m_count: usize,
+}
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates.rs b/compiler/qsc/src/interpret/stateful/re/estimates.rs
index 04b91726..1f33fcf5 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates.rs
@@ -8,6 +8,9 @@
     clippy::cast_sign_loss
 )]
 
+#[cfg(test)]
+mod tests;
+
 mod compiled_expression;
 mod constants;
 mod data;
@@ -19,7 +22,7 @@ mod stages;
 
 use self::{modeling::Protocol, stages::physical_estimation::PhysicalResourceEstimation};
 use super::LogicalResources;
-use data::{JobParams, LogicalResourceCounts};
+use data::{EstimateType, JobParams, LogicalResourceCounts};
 pub use error::Error;
 
 type Result<T> = std::result::Result<T, error::Error>;
@@ -41,53 +44,83 @@ pub fn estimate_physical_resources_from_json(
 }
 
 fn estimate(logical_resources: LogicalResourceCounts, params: &str) -> Result<String> {
-    let job_params = if params.is_empty() {
+    let job_params_array = if params.is_empty() {
         vec![JobParams::default()]
     } else {
         serde_json::from_str(params).map_err(|e| error::Error::IO(error::IO::CannotParseJSON(e)))?
     };
 
-    let mut results = Vec::with_capacity(job_params.len());
-    for mut job_params in job_params {
-        let qubit = job_params.qubit_params().clone();
-        let ftp = Protocol::load_from_specification(job_params.qec_scheme_mut(), &qubit)?;
-        let distillation_unit_templates = job_params
-            .distillation_unit_specifications()
-            .as_templates()?;
-        // create error buget partitioning
-        let partitioning = job_params.error_budget().partitioning(&logical_resources)?;
-
-        let mut estimation =
-            PhysicalResourceEstimation::new(ftp, qubit, logical_resources, partitioning);
-        if let Some(logical_depth_factor) = job_params.constraints().logical_depth_factor {
-            estimation.set_logical_depth_factor(logical_depth_factor);
-        }
-        if let Some(max_t_factories) = job_params.constraints().max_t_factories {
-            estimation.set_max_t_factories(max_t_factories);
-        }
-        if let Some(max_duration) = job_params.constraints().max_duration {
-            estimation.set_max_duration(max_duration);
-        }
-        if let Some(max_physical_qubits) = job_params.constraints().max_physical_qubits {
-            estimation.set_max_physical_qubits(max_physical_qubits);
-        }
-        estimation.set_distillation_unit_templates(distillation_unit_templates);
-
-        let estimation_result = estimation.estimate();
-        match estimation_result {
-            Ok(estimation_result) => results.push(
-                serde_json::to_string(&data::Success::new(
-                    logical_resources,
-                    job_params,
-                    estimation_result,
-                ))
-                .expect("serializing to json string should succeed"),
-            ),
-            Err(err) => results.push(
-                serde_json::to_string(&data::Failure::new(err))
-                    .expect("serializing to json string should succeed"),
+    let mut results: Vec<String> = Vec::with_capacity(job_params_array.len());
+    for job_params in job_params_array {
+        let result = estimate_single(logical_resources, job_params);
+        match result {
+            Ok(result) => results.push(
+                serde_json::to_string(&result).expect("serializing to json string should succeed"),
             ),
+            Err(err) => {
+                results.push(serialize_error(err));
+            }
         }
     }
+
     Ok(format!("[{}]", results.join(",")))
 }
+
+fn estimate_single(
+    logical_resources: LogicalResourceCounts,
+    mut job_params: JobParams,
+) -> Result<data::Success> {
+    let qubit = job_params.qubit_params().clone();
+
+    let ftp = Protocol::load_from_specification(job_params.qec_scheme_mut(), &qubit)?;
+    let distillation_unit_templates = job_params
+        .distillation_unit_specifications()
+        .as_templates()?;
+    // create error budget partitioning
+    let partitioning = job_params.error_budget().partitioning(&logical_resources)?;
+
+    let mut estimation =
+        PhysicalResourceEstimation::new(ftp, qubit, logical_resources, partitioning);
+    if let Some(logical_depth_factor) = job_params.constraints().logical_depth_factor {
+        estimation.set_logical_depth_factor(logical_depth_factor);
+    }
+    if let Some(max_t_factories) = job_params.constraints().max_t_factories {
+        estimation.set_max_t_factories(max_t_factories);
+    }
+    if let Some(max_duration) = job_params.constraints().max_duration {
+        estimation.set_max_duration(max_duration);
+    }
+    if let Some(max_physical_qubits) = job_params.constraints().max_physical_qubits {
+        estimation.set_max_physical_qubits(max_physical_qubits);
+    }
+    estimation.set_distillation_unit_templates(distillation_unit_templates);
+
+    match job_params.estimate_type() {
+        EstimateType::Frontier => {
+            if job_params.constraints().max_duration.is_some()
+                || job_params.constraints().max_physical_qubits.is_some()
+                || job_params.constraints().max_t_factories.is_some()
+            {
+                // We can technically handle those scenarios but do not see a practial use case for it.
+                return Err(error::Error::InvalidInput(
+                    error::InvalidInput::ConstraintsProvidedForFrontierEstimation,
+                ));
+            }
+
+            let estimation_result = estimation.build_frontier();
+            estimation_result.map(|result| {
+                data::Success::new_from_multiple(logical_resources, job_params, result)
+            })
+        }
+        EstimateType::SinglePoint => {
+            let estimation_result = estimation.estimate();
+            estimation_result
+                .map(|result| data::Success::new(logical_resources, job_params, result))
+        }
+    }
+}
+
+fn serialize_error(err: error::Error) -> String {
+    serde_json::to_string(&data::Failure::new(err))
+        .expect("serializing to json string should succeed")
+}
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/data.rs b/compiler/qsc/src/interpret/stateful/re/estimates/data.rs
index acac8c88..e3cc6b0e 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/data.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/data.rs
@@ -10,11 +10,16 @@ mod result;
 mod tfactory;
 
 pub use constraints::Constraints;
-pub use job_params::{ErrorBudgetSpecification, JobParams, Profiling};
+pub use job_params::{EstimateType, JobParams};
 pub use logical_counts::LogicalResourceCounts;
 pub use physical_counts::{PhysicalResourceCounts, PhysicalResourceCountsBreakdown};
-pub use report::{format_duration, format_metric_prefix, FormattedPhysicalResourceCounts, Report};
+pub use report::{FormattedPhysicalResourceCounts, Report};
 pub use result::{Failure, Success};
+
+#[cfg(test)]
 pub use tfactory::{
     TFactoryDistillationUnitSpecification, TFactoryProtocolSpecificDistillationUnitSpecification,
 };
+
+#[cfg(test)]
+pub use job_params::ErrorBudgetSpecification;
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/data/job_params.rs b/compiler/qsc/src/interpret/stateful/re/estimates/data/job_params.rs
index be4e66f2..2e5f636e 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/data/job_params.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/data/job_params.rs
@@ -33,6 +33,9 @@ pub struct JobParams {
 
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     distillation_unit_specifications: TFactoryDistillationUnitSpecifications,
+
+    #[serde(default)]
+    estimate_type: EstimateType,
 }
 
 impl JobParams {
@@ -71,6 +74,12 @@ impl JobParams {
     pub fn distillation_unit_specifications(&self) -> &TFactoryDistillationUnitSpecifications {
         &self.distillation_unit_specifications
     }
+
+    #[must_use]
+    #[inline]
+    pub fn estimate_type(&self) -> &EstimateType {
+        &self.estimate_type
+    }
 }
 
 #[derive(Serialize, Deserialize, Default, Eq, PartialEq)]
@@ -171,3 +180,16 @@ impl Default for ErrorBudgetSpecification {
         Self::Total(1e-3)
     }
 }
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
+pub enum EstimateType {
+    Frontier,
+    SinglePoint,
+}
+
+impl Default for EstimateType {
+    fn default() -> Self {
+        Self::SinglePoint
+    }
+}
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/data/report.rs b/compiler/qsc/src/interpret/stateful/re/estimates/data/report.rs
index 97f00b14..61ae2818 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/data/report.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/data/report.rs
@@ -16,7 +16,8 @@ use super::{
             tfactory::TFactory,
         },
     },
-    JobParams, LogicalResourceCounts,
+    job_params::JobParams,
+    LogicalResourceCounts,
 };
 
 #[derive(Serialize)]
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/data/result.rs b/compiler/qsc/src/interpret/stateful/re/estimates/data/result.rs
index 2a810beb..08421830 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/data/result.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/data/result.rs
@@ -14,20 +14,27 @@ use super::{
     PhysicalResourceCountsBreakdown, Report,
 };
 use miette::Diagnostic;
-use serde::{ser::SerializeMap, Serialize};
+use serde::{ser::SerializeMap, Serialize, Serializer};
 
 #[derive(Serialize)]
 #[serde(rename_all(serialize = "camelCase"))]
 pub struct Success {
     status: &'static str,
     job_params: JobParams,
-    physical_counts: PhysicalResourceCounts,
-    physical_counts_formatted: FormattedPhysicalResourceCounts,
-    logical_qubit: LogicalQubit,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    physical_counts: Option<PhysicalResourceCounts>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    physical_counts_formatted: Option<FormattedPhysicalResourceCounts>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    logical_qubit: Option<LogicalQubit>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     tfactory: Option<TFactory>,
-    error_budget: ErrorBudget,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    error_budget: Option<ErrorBudget>,
     logical_counts: LogicalResourceCounts,
     report_data: Report,
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    frontier_entries: Vec<FrontierEntry>,
 }
 
 impl Success {
@@ -36,42 +43,11 @@ impl Success {
         job_params: JobParams,
         result: PhysicalResourceEstimationResult<L>,
     ) -> Self {
-        let num_ts_per_rotation = result
-            .layout_overhead()
-            .num_ts_per_rotation(result.error_budget().rotations());
-
-        let breakdown = PhysicalResourceCountsBreakdown {
-            algorithmic_logical_qubits: result.layout_overhead().logical_qubits(),
-            algorithmic_logical_depth: result
-                .layout_overhead()
-                .logical_depth(num_ts_per_rotation.unwrap_or_default()),
-            logical_depth: result.num_cycles(),
-            clock_frequency: result.logical_qubit().logical_cycles_per_second(),
-            num_tstates: result
-                .layout_overhead()
-                .num_tstates(num_ts_per_rotation.unwrap_or_default()),
-            num_tfactories: result.num_tfactories(),
-            num_tfactory_runs: result.num_tfactory_runs(),
-            physical_qubits_for_tfactories: result.physical_qubits_for_tfactories(),
-            physical_qubits_for_algorithm: result.physical_qubits_for_algorithm(),
-            required_logical_qubit_error_rate: result.required_logical_qubit_error_rate(),
-            required_logical_tstate_error_rate: result.required_logical_tstate_error_rate(),
-            num_ts_per_rotation,
-            clifford_error_rate: result
-                .logical_qubit()
-                .physical_qubit()
-                .clifford_error_rate(),
-        };
-
-        let counts = PhysicalResourceCounts {
-            physical_qubits: result.physical_qubits(),
-            runtime: result.runtime(),
-            rqops: result.rqops(),
-            breakdown,
-        };
+        let counts = create_physical_resource_counts(&result);
 
         let formatted_counts: FormattedPhysicalResourceCounts =
             FormattedPhysicalResourceCounts::new(&result, &logical_resources, &job_params);
+
         let report_data = Report::new(&logical_resources, &job_params, &result, &formatted_counts);
 
         let (logical_qubit, tfactory, error_budget) = result.take();
@@ -79,14 +55,144 @@ impl Success {
         Self {
             status: "success",
             job_params,
-            physical_counts: counts,
-            physical_counts_formatted: formatted_counts,
-            logical_qubit,
+            physical_counts: Some(counts),
+            physical_counts_formatted: Some(formatted_counts),
+            logical_qubit: Some(logical_qubit),
             tfactory,
-            error_budget,
+            error_budget: Some(error_budget),
             logical_counts: logical_resources,
             report_data,
+            frontier_entries: Vec::new(),
+        }
+    }
+
+    pub fn new_from_multiple<L: Overhead + Clone>(
+        logical_resources: LogicalResourceCounts,
+        job_params: JobParams,
+        mut results: Vec<PhysicalResourceEstimationResult<L>>,
+    ) -> Self {
+        let mut report_data: Option<Report> = None;
+
+        let mut frontier_entries: Vec<FrontierEntry> = Vec::new();
+
+        // we will pick the shortest runtime result as the first result.
+        results.sort_by_key(PhysicalResourceEstimationResult::runtime);
+        for result in results {
+            let (frontier_entry, report) = create_frontier_entry(
+                &logical_resources,
+                &job_params,
+                result,
+                report_data.is_none(),
+            );
+
+            if report_data.is_none() {
+                report_data = Some(report.expect("error should have report"));
+            }
+
+            frontier_entries.push(frontier_entry);
         }
+
+        Self {
+            status: "success",
+            job_params,
+            physical_counts: None,
+            physical_counts_formatted: None,
+            logical_qubit: None,
+            tfactory: None,
+            error_budget: None,
+            logical_counts: logical_resources,
+            report_data: report_data.expect("error should have report"), // Here we assume that at least a single solution was found.
+            frontier_entries,
+        }
+    }
+}
+
+#[derive(Serialize)]
+#[serde(rename_all(serialize = "camelCase"))]
+pub struct FrontierEntry {
+    pub logical_qubit: LogicalQubit,
+    pub tfactory: Option<TFactory>,
+    pub error_budget: ErrorBudget,
+    pub physical_counts: PhysicalResourceCounts,
+    pub physical_counts_formatted: FormattedPhysicalResourceCounts,
+}
+
+fn create_frontier_entry<L: Overhead + Clone>(
+    logical_resources: &LogicalResourceCounts,
+    job_params: &JobParams,
+    result: PhysicalResourceEstimationResult<L>,
+    create_report: bool,
+) -> (FrontierEntry, Option<Report>) {
+    let physical_counts = create_physical_resource_counts(&result);
+
+    let physical_counts_formatted: FormattedPhysicalResourceCounts =
+        FormattedPhysicalResourceCounts::new(&result, logical_resources, job_params);
+
+    let report_data = if create_report {
+        Some(Report::new(
+            logical_resources,
+            job_params,
+            &result,
+            &physical_counts_formatted,
+        ))
+    } else {
+        None
+    };
+
+    let (logical_qubit, tfactory, error_budget) = result.take();
+
+    (
+        FrontierEntry {
+            logical_qubit,
+            tfactory,
+            error_budget,
+            physical_counts,
+            physical_counts_formatted,
+        },
+        report_data,
+    )
+}
+
+fn create_physical_resource_counts<L: Overhead + Clone>(
+    result: &PhysicalResourceEstimationResult<L>,
+) -> PhysicalResourceCounts {
+    let breakdown = create_physical_resource_counts_breakdown(result);
+
+    PhysicalResourceCounts {
+        physical_qubits: result.physical_qubits(),
+        runtime: result.runtime(),
+        rqops: result.rqops(),
+        breakdown,
+    }
+}
+
+fn create_physical_resource_counts_breakdown<L: Overhead + Clone>(
+    result: &PhysicalResourceEstimationResult<L>,
+) -> PhysicalResourceCountsBreakdown {
+    let num_ts_per_rotation = result
+        .layout_overhead()
+        .num_ts_per_rotation(result.error_budget().rotations());
+    PhysicalResourceCountsBreakdown {
+        algorithmic_logical_qubits: result.layout_overhead().logical_qubits(),
+        algorithmic_logical_depth: result
+            .layout_overhead()
+            .logical_depth(num_ts_per_rotation.unwrap_or_default()),
+        logical_depth: result.num_cycles(),
+        clock_frequency: result.logical_qubit().logical_cycles_per_second(),
+        num_tstates: result
+            .layout_overhead()
+            .num_tstates(num_ts_per_rotation.unwrap_or_default()),
+        num_tfactories: result.num_tfactories(),
+        num_tfactory_runs: result.num_tfactory_runs(),
+        physical_qubits_for_tfactories: result.physical_qubits_for_tfactories(),
+        physical_qubits_for_algorithm: result.physical_qubits_for_algorithm(),
+        required_logical_qubit_error_rate: result.required_logical_qubit_error_rate(),
+        required_logical_tstate_error_rate: result.required_logical_tstate_error_rate(),
+        num_ts_per_rotation,
+        clifford_error_rate: result
+            .logical_qubit()
+            .physical_qubit()
+            .clifford_error_rate(),
     }
 }
 
@@ -108,7 +214,7 @@ impl Failure {
 impl Serialize for Failure {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
-        S: serde::Serializer,
+        S: Serializer,
     {
         let mut map = serializer.serialize_map(Some(4))?;
 
@@ -126,7 +232,7 @@ impl Serialize for Failure {
                 &format!("[batch index {}] {:?}", batch_index, self.error),
             )?;
         } else {
-            map.serialize_entry("message", &format!("{:?}", self.error))?;
+            map.serialize_entry("message", &self.error.to_string())?;
         }
 
         map.end()
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/error.rs b/compiler/qsc/src/interpret/stateful/re/estimates/error.rs
index db066119..13e90d12 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/error.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/error.rs
@@ -123,6 +123,17 @@ pub enum InvalidInput {
     #[error("No solution found for the provided maximum number of T factories.")]
     #[diagnostic(code("Qsc.Estimates.InvalidInputError.NoSolutionFoundForMaxTFactories"))]
     NoSolutionFoundForMaxTFactories,
+    /// Constraints provided for frontier estimation
+    /// (maximal time, maximal number of qubits, maximal number of T factories)
+    /// are not supported for frontier estimation.
+    ///
+    /// ✅ This does not contain user data and can be logged
+    /// 🧑‍💻 This indicates a user error
+    #[error("Unsupported constraints provided for Frontier Estimation.")]
+    #[diagnostic(code(
+        "Qsc.Estimates.InvalidInputError.ConstraintsProvidedForFrontierEstimation"
+    ))]
+    ConstraintsProvidedForFrontierEstimation,
 }
 
 #[derive(Debug, Error, Diagnostic)]
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/optimization.rs b/compiler/qsc/src/interpret/stateful/re/estimates/optimization.rs
index 9f6fd894..79354d53 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/optimization.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/optimization.rs
@@ -6,4 +6,5 @@ mod distillation_units_map;
 mod population;
 mod tfactory_exhaustive;
 
+pub(crate) use population::{Point2D, Population};
 pub(crate) use tfactory_exhaustive::find_nondominated_tfactories;
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/optimization/distillation_units_map.rs b/compiler/qsc/src/interpret/stateful/re/estimates/optimization/distillation_units_map.rs
index 57587142..ea6ac55c 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/optimization/distillation_units_map.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/optimization/distillation_units_map.rs
@@ -66,7 +66,7 @@ impl<'a> DistillationUnitsMap<'a> {
         physical_distillation_units.append(&mut purely_physical_distillation_units);
 
         let mut is_valid_physical_distillation_units =
-            vec![true; num_code_distances + num_physical_distillation_units];
+            vec![true; num_combined_distillation_units + num_physical_distillation_units];
 
         for idx in 0..num_combined_distillation_units {
             if !physical_distillation_units[idx].is_valid() {
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/optimization/population.rs b/compiler/qsc/src/interpret/stateful/re/estimates/optimization/population.rs
index d8b14f6e..1080a638 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/optimization/population.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/optimization/population.rs
@@ -184,6 +184,10 @@ where
         self.items.sort_by(|a, b| b.cmp(a));
     }
 
+    pub(crate) fn extract(self) -> Vec<P> {
+        self.items
+    }
+
     pub(crate) fn attempt_filter_out_dominated(&mut self) {
         if self.nonexecuted_attempts_to_filter_out_dominated
             <= Self::MAX_NONEXECUTED_ATTEMPTS_TO_FILTER_OUT_DOMINATED
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation.rs b/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation.rs
index 4f28c081..04a531d0 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation.rs
@@ -11,7 +11,7 @@ use super::{
             MaxPhysicalQubitsTooSmall, NoSolutionFoundForMaxTFactories, NoTFactoriesFound,
         },
         modeling::{ErrorBudget, LogicalQubit, PhysicalQubit, Protocol},
-        optimization::find_nondominated_tfactories,
+        optimization::{find_nondominated_tfactories, Point2D, Population},
         Result,
     },
     layout::Overhead,
@@ -250,6 +250,155 @@ impl<L: Overhead + Clone> PhysicalResourceEstimation<L> {
         }
     }
 
+    #[allow(clippy::too_many_lines)]
+    pub fn build_frontier(&self) -> Result<Vec<PhysicalResourceEstimationResult<L>>> {
+        let num_cycles_required_by_layout_overhead = self.compute_num_cycles()?;
+
+        // The required T-state error rate is computed by dividing the total
+        // error budget for T states by the number of T-states required for the
+        // algorithm.
+        let num_ts_per_rotation = self
+            .layout_overhead
+            .num_ts_per_rotation(self.error_budget.rotations());
+        let required_logical_tstate_error_rate = self.error_budget.tstates()
+            / self
+                .layout_overhead
+                .num_tstates(num_ts_per_rotation.unwrap_or_default()) as f64;
+
+        // Required logical error rate (\eps_{\log} / (Q * C) in the paper)
+        let required_logical_qubit_error_rate = self.error_budget.logical()
+            / (self.layout_overhead.logical_qubits() * num_cycles_required_by_layout_overhead)
+                as f64;
+
+        let min_code_distance = self.compute_code_distance(required_logical_qubit_error_rate);
+        let max_code_distance = self.ftp.max_code_distance();
+
+        if min_code_distance > max_code_distance {
+            return Err(InvalidCodeDistance(min_code_distance, max_code_distance).into());
+        }
+
+        if self
+            .layout_overhead
+            .num_tstates(num_ts_per_rotation.unwrap_or_default())
+            == 0
+        {
+            let logical_qubit =
+                LogicalQubit::new(&self.ftp, min_code_distance, self.qubit.clone())?;
+
+            return Ok(vec![PhysicalResourceEstimationResult::new(
+                self,
+                logical_qubit,
+                num_cycles_required_by_layout_overhead,
+                None,
+                0,
+                required_logical_qubit_error_rate,
+                None,
+            )]);
+        }
+
+        let mut best_estimation_results =
+            Population::<Point2D<PhysicalResourceEstimationResult<L>>>::new();
+
+        let max_odd_code_distance = self.get_max_odd_code_distance();
+        let mut last_tfactories: Vec<TFactory> = Vec::new();
+        let mut last_code_distance = max_code_distance + 1;
+
+        for code_distance in (min_code_distance..=max_odd_code_distance).rev().step_by(2) {
+            let logical_qubit = LogicalQubit::new(&self.ftp, code_distance, self.qubit.clone())?;
+
+            let allowed_logical_qubit_error_rate = self.compute_logical_error_rate(code_distance);
+
+            let max_num_cycles_allowed_by_error_rate = (self.error_budget.logical()
+                / (self.layout_overhead.logical_qubits() as f64 * allowed_logical_qubit_error_rate))
+                .floor() as u64;
+
+            if max_num_cycles_allowed_by_error_rate < num_cycles_required_by_layout_overhead {
+                continue;
+            }
+
+            let max_num_cycles_allowed = max_num_cycles_allowed_by_error_rate;
+
+            // The initial value for the last code distance
+            // is max_code_distance + 1 which is larger than any code distance in the loop.
+            // This ensures that the first code distance is always tried.
+            // After that, the last code distance governs the reuse of T-factory.
+            if last_code_distance > code_distance {
+                last_tfactories = find_nondominated_tfactories(
+                    &self.ftp,
+                    &self.qubit,
+                    &self.distillation_unit_templates,
+                    required_logical_tstate_error_rate,
+                    code_distance,
+                );
+
+                last_code_distance = Self::find_highest_code_distance(&last_tfactories);
+            }
+
+            if let Some((tfactory, _)) = Self::try_pick_tfactory_with_num_cycles(
+                &last_tfactories,
+                &logical_qubit,
+                max_num_cycles_allowed,
+            ) {
+                // Here we compute the number of T-factories required limited by the
+                // maximum number of cycles allowed by the duration constraint (and the error rate).
+                let min_num_tfactories =
+                    self.num_tfactories(&logical_qubit, &tfactory, max_num_cycles_allowed);
+
+                let mut num_tfactories = min_num_tfactories;
+
+                loop {
+                    // Based on the num_tfactories we compute the number of cycles required
+                    // which must be smaller than the maximum number of cycles allowed by the
+                    // duration constraint (and the error rate).
+                    let num_cycles_required_for_tstates = self
+                        .compute_num_cycles_required_for_tstates(
+                            num_tfactories,
+                            &tfactory,
+                            &logical_qubit,
+                        );
+
+                    // This num_cycles could be larger than num_cycles_required_by_layout_overhead
+                    // but must still not exceed the maximum number of cycles allowed by the
+                    // duration constraint (and the error rate).
+                    let num_cycles =
+                        num_cycles_required_for_tstates.max(num_cycles_required_by_layout_overhead);
+
+                    let result = PhysicalResourceEstimationResult::new(
+                        self,
+                        LogicalQubit::new(&self.ftp, code_distance, self.qubit.clone())?,
+                        num_cycles,
+                        Some(tfactory.clone()),
+                        num_tfactories,
+                        required_logical_qubit_error_rate,
+                        Some(required_logical_tstate_error_rate),
+                    );
+
+                    let value1 = result.runtime() as f64;
+                    let value2 = result.physical_qubits();
+                    let num_t_factory_runs = result.num_tfactory_runs();
+                    let point = Point2D::new(result, value1, value2);
+                    best_estimation_results.push(point);
+
+                    if num_cycles_required_for_tstates <= num_cycles_required_by_layout_overhead
+                        || num_t_factory_runs <= 1
+                    {
+                        break;
+                    }
+
+                    num_tfactories += 1;
+                }
+            }
+        }
+
+        best_estimation_results.filter_out_dominated();
+
+        Ok(best_estimation_results
+            .extract()
+            .into_iter()
+            .map(|p| p.item)
+            .collect())
+    }
+
     fn estimate_without_restrictions(&self) -> Result<PhysicalResourceEstimationResult<L>> {
         let mut num_cycles = self.compute_num_cycles()?;
 
@@ -704,15 +853,13 @@ impl<L: Overhead + Clone> PhysicalResourceEstimation<L> {
         let num_ts_per_rotation = self
             .layout_overhead
             .num_ts_per_rotation(self.error_budget.rotations());
-        let required_runs = ((self
+        let required_runs = self
             .layout_overhead
             .num_tstates(num_ts_per_rotation.unwrap_or_default())
-            as f64)
-            / tstates_per_run as f64)
-            .floor() as u64;
+            .div_ceil(tstates_per_run);
 
         let required_duration = required_runs * tfactory.duration();
-        (required_duration as f64 / logical_qubit.logical_cycle_time() as f64).floor() as u64
+        required_duration.div_ceil(logical_qubit.logical_cycle_time())
     }
 
     fn try_pick_tfactory_below_or_equal_num_qubits(
diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs
index 7012b2fb..22c7bb84 100644
--- a/compiler/qsc_eval/src/backend.rs
+++ b/compiler/qsc_eval/src/backend.rs
@@ -201,7 +201,10 @@ impl Backend for SparseSim {
     fn custom_intrinsic(&mut self, name: &str, _arg: Value) -> Option<Result<Value, String>> {
         match name {
             "BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
-            "EndEstimateCaching" | "AccountForEstimatesInternal" => Some(Ok(Value::unit())),
+            "EndEstimateCaching"
+            | "AccountForEstimatesInternal"
+            | "BeginRepeatEstimatesInternal"
+            | "EndRepeatEstimatesInternal" => Some(Ok(Value::unit())),
             _ => None,
         }
     }
diff --git a/library/std/re.qs b/library/std/re.qs
index 95106322..68158d3c 100644
--- a/library/std/re.qs
+++ b/library/std/re.qs
@@ -127,4 +127,59 @@ namespace Microsoft.Quantum.ResourceEstimation {
         body intrinsic;
     }
 
+    /// # Summary
+    ///
+    /// Instructs the resource estimator to assume that the resources from the
+    /// call of this operation until a call to `EndRepeatEstimates` are
+    /// accounted for `count` times, without the need to execute the code that many
+    /// times. Calls to `BeginRepeatEstimates` and `EndRepeatEstimates` can be nested.
+    /// A helper operation `RepeatEstimates` allows to call the two functions in a
+    /// `within` block.
+    ///
+    /// # Input
+    /// ## count
+    /// Assumed number of repetitions, factor to multiply the cost with
+    operation BeginRepeatEstimates(count : Int) : Unit {
+        body ... {
+            BeginRepeatEstimatesInternal(count);
+        }
+        adjoint self;
+    }
+
+    internal operation BeginRepeatEstimatesInternal(count : Int) : Unit {
+        body intrinsic;
+    }
+
+    /// # Summary
+    ///
+    /// Companion operation to `BeginRepeatEstimates`.
+    operation EndRepeatEstimates() : Unit {
+        body ... {
+            EndRepeatEstimatesInternal();
+        }
+        adjoint self;
+    }
+
+    internal operation EndRepeatEstimatesInternal() : Unit {
+        body intrinsic;
+    }
+
+    /// # Summary
+    ///
+    /// Instructs the resource estimator to assume that the resources from the
+    /// call of this operation until a call to `Adjoint RepeatEstimates` are
+    /// accounted for `count` times, without the need to execute the code that many
+    /// times.
+    ///
+    /// # Input
+    /// ## count
+    /// Assumed number of repetitions, factor to multiply the cost with
+    operation RepeatEstimates(count : Int) : Unit is Adj {
+        body ... {
+            BeginRepeatEstimates(count);
+        }
+        adjoint ... {
+            EndRepeatEstimates();
+        }
+    }
 }
diff --git a/npm/ux/index.ts b/npm/ux/index.ts
index 5f2ba6e1..e877c0db 100644
--- a/npm/ux/index.ts
+++ b/npm/ux/index.ts
@@ -8,4 +8,4 @@ import "./qsharp-ux.css";
 export { Histogram } from "./histogram.js";
 export { ReTable, type ReData } from "./reTable.js";
 export { SpaceChart } from "./spaceChart.js";
-export { ResultsTable, type CellValue } from "./resultsTable.js";
+export { ResultsTable } from "./resultsTable.js";
diff --git a/npm/ux/reTable.tsx b/npm/ux/reTable.tsx
index d8ae424d..afbdd97c 100644
--- a/npm/ux/reTable.tsx
+++ b/npm/ux/reTable.tsx
@@ -25,6 +25,7 @@ export type ReData = {
     }[];
     assumptions: string[];
   };
+  new: boolean;
 };
 
 export function ReTable(props: {
diff --git a/npm/ux/resultsTable.tsx b/npm/ux/resultsTable.tsx
index dca370ea..ca7ec0ec 100644
--- a/npm/ux/resultsTable.tsx
+++ b/npm/ux/resultsTable.tsx
@@ -2,13 +2,14 @@
 // Licensed under the MIT License.
 
 import { useRef, useState } from "preact/hooks";
+import { type ReData } from "./reTable.js";
 
-export type CellValue = string | number | { value: string; sortBy: number };
+type CellValue = string | number | { value: string; sortBy: number };
 
 // Note: column 0 is expected to be unique amongst all rows
 export function ResultsTable(props: {
   columnNames: string[];
-  rows: CellValue[][];
+  data: ReData[];
   initialColumns: number[];
   ensureSelected: boolean;
   onRowSelected(rowId: string): void;
@@ -23,8 +24,20 @@ export function ResultsTable(props: {
   const [showColumnMenu, setShowColumnMenu] = useState(false);
   const [showRowMenu, setShowRowMenu] = useState("");
 
-  if (!selectedRow && props.ensureSelected && props.rows.length > 0) {
-    const rowId = props.rows[0][0].toString();
+  const rows = props.data.map(ReDataToRow);
+
+  // Find the first row that is new in the current sort order
+  const newest = getSortedRows(rows).find(
+    (row) => (row[row.length - 1] as string) === "New",
+  );
+
+  // Select the first of the newest rows, otherwise preserve the existing selection
+  if (newest && props.ensureSelected) {
+    const rowId = newest[0].toString();
+    setSelectedRow(rowId);
+    props.onRowSelected(rowId);
+  } else if (!selectedRow && props.ensureSelected && rows.length > 0) {
+    const rowId = rows[0][0].toString();
     setSelectedRow(rowId);
     props.onRowSelected(rowId);
   }
@@ -331,7 +344,7 @@ export function ResultsTable(props: {
         </tr>
       </thead>
       <tbody>
-        {getSortedRows(props.rows).map((row) => {
+        {getSortedRows(rows).map((row) => {
           const rowId = row[0].toString();
           return (
             <tr
@@ -383,3 +396,25 @@ export function ResultsTable(props: {
     </table>
   );
 }
+
+function ReDataToRow(data: ReData): CellValue[] {
+  return [
+    data.jobParams.runName,
+    data.jobParams.qubitParams.name,
+    data.jobParams.qecScheme.name,
+    data.jobParams.errorBudget,
+    data.physicalCounts.breakdown.algorithmicLogicalQubits,
+    data.physicalCounts.breakdown.algorithmicLogicalDepth,
+    data.logicalQubit.codeDistance,
+    data.physicalCounts.breakdown.numTstates,
+    data.physicalCounts.breakdown.numTfactories,
+    data.physicalCountsFormatted.physicalQubitsForTfactoriesPercentage,
+    {
+      value: data.physicalCountsFormatted.runtime,
+      sortBy: data.physicalCounts.runtime,
+    },
+    data.physicalCounts.rqops,
+    data.physicalCounts.physicalQubits,
+    data.new ? "New" : "Cached",
+  ];
+}
diff --git a/pip/qsharp/estimator/__init__.py b/pip/qsharp/estimator/__init__.py
index 749ae22b..58dfeeea 100644
--- a/pip/qsharp/estimator/__init__.py
+++ b/pip/qsharp/estimator/__init__.py
@@ -1,9 +1,10 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from ._estimator import LogicalCounts, EstimatorResult, QubitParams, QECScheme, MeasurementErrorRate, EstimatorQubitParams, EstimatorQecScheme, ProtocolSpecificDistillationUnitSpecification, DistillationUnitSpecification, ErrorBudgetPartition, EstimatorConstraints, EstimatorInputParamsItem, EstimatorParams
+from ._estimator import EstimatorError, LogicalCounts, EstimatorResult, QubitParams, QECScheme, MeasurementErrorRate, EstimatorQubitParams, EstimatorQecScheme, ProtocolSpecificDistillationUnitSpecification, DistillationUnitSpecification, ErrorBudgetPartition, EstimatorConstraints, EstimatorInputParamsItem, EstimatorParams
 
 __all__ = [
+    "EstimatorError",
     "LogicalCounts",
     "EstimatorResult",
     "QubitParams",
diff --git a/pip/qsharp/estimator/_estimator.py b/pip/qsharp/estimator/_estimator.py
index 28e7cd4d..c6c31d81 100644
--- a/pip/qsharp/estimator/_estimator.py
+++ b/pip/qsharp/estimator/_estimator.py
@@ -8,11 +8,27 @@ from .._native import physical_estimates
 import json
 
 try:
+    # Both markdown and mdx_math (from python-markdown-math) must be present for our markdown
+    # rendering logic to work. If either is missing, we'll fall back to plain text.
     import markdown
+    import mdx_math
+
     has_markdown = True
 except ImportError:
     has_markdown = False
 
+class EstimatorError(BaseException):
+    """
+    An error returned from the resource estimation.
+    """
+
+    def __init__(self, code: str, message: str):
+        self.message = f"Error estimating resources ({code}):\n{message}"
+        self.code = code
+
+    def __str__(self):
+        return self.message
+
 @dataclass
 class AutoValidatingParams:
     """
@@ -24,6 +40,7 @@ class AutoValidatingParams:
     to camel case, and if validate is True and if the field has a validation
     function, the field is validated beforehand.
     """
+
     def as_dict(self, validate=True):
         result = {}
 
@@ -40,7 +57,7 @@ class AutoValidatingParams:
 
                 # translate field name to camel case
                 s = re.sub(r"(_|-)+", " ", name).title().replace(" ", "")
-                attribute = ''.join([s[0].lower(), s[1:]])
+                attribute = "".join([s[0].lower(), s[1:]])
                 result[attribute] = field_value
 
         if validate:
@@ -57,12 +74,14 @@ class AutoValidatingParams:
         """
         pass
 
+
 def validating_field(validation_func, default=None):
     """
     A helper method to declare field for an AutoValidatingParams data class.
     """
     return field(default=default, metadata={"validate": validation_func})
 
+
 class QubitParams:
     GATE_US_E3 = "qubit_gate_us_e3"
     GATE_US_E4 = "qubit_gate_us_e4"
@@ -71,14 +90,17 @@ class QubitParams:
     MAJ_NS_E4 = "qubit_maj_ns_e4"
     MAJ_NS_E6 = "qubit_maj_ns_e6"
 
+
 class QECScheme:
     SURFACE_CODE = "surface_code"
     FLOQUET_CODE = "floquet_code"
 
+
 def _check_error_rate(name, value):
     if value <= 0.0 or value >= 1.0:
         raise ValueError(f"{name} must be between 0 and 1")
 
+
 def _check_error_rate_or_process_and_readout(name, value):
     if value is None:
         return
@@ -88,58 +110,75 @@ def _check_error_rate_or_process_and_readout(name, value):
         return
 
     if not isinstance(value, MeasurementErrorRate):
-        raise ValueError(f"{name} must be either a float or "
-                         "MeasurementErrorRate with two fields: 'process' and 'readout'")
+        raise ValueError(
+            f"{name} must be either a float or "
+            "MeasurementErrorRate with two fields: 'process' and 'readout'"
+        )
+
 
 def check_time(name, value):
     pat = r"^(\+?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(s|ms|μs|µs|us|ns)$"
     if re.match(pat, value) is None:
-        raise ValueError(f"{name} is not a valid time string; use a "
-                         "suffix s, ms, us, or ns")
+        raise ValueError(
+            f"{name} is not a valid time string; use a " "suffix s, ms, us, or ns"
+        )
+
 
 @dataclass
 class MeasurementErrorRate(AutoValidatingParams):
     process: float = field(metadata={"validate": _check_error_rate})
     readout: float = field(metadata={"validate": _check_error_rate})
 
+
 @dataclass
 class EstimatorQubitParams(AutoValidatingParams):
     @staticmethod
     def check_instruction_set(name, value):
-        if value not in ["gate-based", "gate_based", "GateBased", "gateBased",
-                         "Majorana", "majorana"]:
+        if value not in [
+            "gate-based",
+            "gate_based",
+            "GateBased",
+            "gateBased",
+            "Majorana",
+            "majorana",
+        ]:
             raise ValueError(f"{name} must be GateBased or Majorana")
 
     name: Optional[str] = None
     instruction_set: Optional[str] = validating_field(check_instruction_set)
     one_qubit_measurement_time: Optional[str] = validating_field(check_time)
-    two_qubit_joint_measurement_time: Optional[str] = \
-        validating_field(check_time)
+    two_qubit_joint_measurement_time: Optional[str] = validating_field(check_time)
     one_qubit_gate_time: Optional[str] = validating_field(check_time)
     two_qubit_gate_time: Optional[str] = validating_field(check_time)
     t_gate_time: Optional[str] = validating_field(check_time)
-    one_qubit_measurement_error_rate: Union[None, float, MeasurementErrorRate] = \
-        validating_field(_check_error_rate_or_process_and_readout)
-    two_qubit_joint_measurement_error_rate: Union[None, float, MeasurementErrorRate] = \
-        validating_field(_check_error_rate_or_process_and_readout)
-    one_qubit_gate_error_rate: Optional[float] = \
-        validating_field(_check_error_rate)
-    two_qubit_gate_error_rate: Optional[float] = \
-        validating_field(_check_error_rate)
+    one_qubit_measurement_error_rate: Union[
+        None, float, MeasurementErrorRate
+    ] = validating_field(_check_error_rate_or_process_and_readout)
+    two_qubit_joint_measurement_error_rate: Union[
+        None, float, MeasurementErrorRate
+    ] = validating_field(_check_error_rate_or_process_and_readout)
+    one_qubit_gate_error_rate: Optional[float] = validating_field(_check_error_rate)
+    two_qubit_gate_error_rate: Optional[float] = validating_field(_check_error_rate)
     t_gate_error_rate: Optional[float] = validating_field(_check_error_rate)
     idle_error_rate: Optional[float] = validating_field(_check_error_rate)
 
-    _default_models = [QubitParams.GATE_US_E3, QubitParams.GATE_US_E4,
-                       QubitParams.GATE_NS_E3, QubitParams.GATE_NS_E4,
-                       QubitParams.MAJ_NS_E4, QubitParams.MAJ_NS_E6]
+    _default_models = [
+        QubitParams.GATE_US_E3,
+        QubitParams.GATE_US_E4,
+        QubitParams.GATE_NS_E3,
+        QubitParams.GATE_NS_E4,
+        QubitParams.MAJ_NS_E4,
+        QubitParams.MAJ_NS_E6,
+    ]
     _gate_based = ["gate-based", "gate_based", "GateBased", "gateBased"]
     _maj_based = ["Majorana", "majorana"]
 
     def post_validation(self, result):
         # check whether all fields have been specified in case a custom qubit
         # model is specified
-        custom = result != {} and \
-            (self.name is None or self.name not in self._default_models)
+        custom = result != {} and (
+            self.name is None or self.name not in self._default_models
+        )
 
         # no further validation needed for non-custom models
         if not custom:
@@ -147,8 +186,9 @@ class EstimatorQubitParams(AutoValidatingParams):
 
         # instruction set must be set
         if self.instruction_set is None:
-            raise LookupError("instruction_set must be set for custom qubit "
-                              "parameters")
+            raise LookupError(
+                "instruction_set must be set for custom qubit " "parameters"
+            )
 
         # NOTE at this point, we know that instruction set must have valid
         # value
@@ -166,24 +206,29 @@ class EstimatorQubitParams(AutoValidatingParams):
         qubit_params = super().as_dict(validate)
         if len(qubit_params) != 0:
             if isinstance(self.one_qubit_measurement_error_rate, MeasurementErrorRate):
-                qubit_params["oneQubitMeasurementErrorRate"] = \
-                self.one_qubit_measurement_error_rate.as_dict(validate)
+                qubit_params[
+                    "oneQubitMeasurementErrorRate"
+                ] = self.one_qubit_measurement_error_rate.as_dict(validate)
 
-            if isinstance(self.two_qubit_joint_measurement_error_rate, MeasurementErrorRate):
-                qubit_params["twoQubitJointMeasurementErrorRate"] = \
-                self.two_qubit_joint_measurement_error_rate.as_dict(validate)
+            if isinstance(
+                self.two_qubit_joint_measurement_error_rate, MeasurementErrorRate
+            ):
+                qubit_params[
+                    "twoQubitJointMeasurementErrorRate"
+                ] = self.two_qubit_joint_measurement_error_rate.as_dict(validate)
 
         return qubit_params
 
+
 @dataclass
 class EstimatorQecScheme(AutoValidatingParams):
     name: Optional[str] = None
-    error_correction_threshold: Optional[float] = \
-        validating_field(_check_error_rate)
+    error_correction_threshold: Optional[float] = validating_field(_check_error_rate)
     crossing_prefactor: Optional[float] = None
     logical_cycle_time: Optional[str] = None
     physical_qubits_per_logical_qubit: Optional[str] = None
 
+
 @dataclass
 class ProtocolSpecificDistillationUnitSpecification(AutoValidatingParams):
     num_unit_qubits: Optional[int] = None
@@ -196,45 +241,56 @@ class ProtocolSpecificDistillationUnitSpecification(AutoValidatingParams):
         if self.duration_in_qubit_cycle_time is None:
             raise LookupError("duration_in_qubit_cycle_time must be set")
 
+
 @dataclass
 class DistillationUnitSpecification(AutoValidatingParams):
     name: Optional[str] = None
     display_name: Optional[str] = None
     num_input_ts: Optional[int] = None
     num_output_ts: Optional[int] = None
-    failure_probability_formula:  Optional[str] = None
-    output_error_rate_formula:  Optional[str] = None
-    physical_qubit_specification: Optional[ProtocolSpecificDistillationUnitSpecification] = None
-    logical_qubit_specification: Optional[ProtocolSpecificDistillationUnitSpecification] = None
-    logical_qubit_specification_first_round_override: \
-        Optional[ProtocolSpecificDistillationUnitSpecification] = None
+    failure_probability_formula: Optional[str] = None
+    output_error_rate_formula: Optional[str] = None
+    physical_qubit_specification: Optional[
+        ProtocolSpecificDistillationUnitSpecification
+    ] = None
+    logical_qubit_specification: Optional[
+        ProtocolSpecificDistillationUnitSpecification
+    ] = None
+    logical_qubit_specification_first_round_override: Optional[
+        ProtocolSpecificDistillationUnitSpecification
+    ] = None
 
     def has_custom_specification(self):
-        return \
-        self.display_name is not None \
-        or self.num_input_ts is not None \
-        or self.num_output_ts is not None \
-        or self.failure_probability_formula is not None \
-        or self.output_error_rate_formula is not None \
-        or self.physical_qubit_specification is not None \
-        or self.logical_qubit_specification is not None \
-        or self.logical_qubit_specification_first_round_override is not None
+        return (
+            self.display_name is not None
+            or self.num_input_ts is not None
+            or self.num_output_ts is not None
+            or self.failure_probability_formula is not None
+            or self.output_error_rate_formula is not None
+            or self.physical_qubit_specification is not None
+            or self.logical_qubit_specification is not None
+            or self.logical_qubit_specification_first_round_override is not None
+        )
 
     def has_predefined_name(self):
         return self.name is not None
 
     def post_validation(self, result):
         if not self.has_custom_specification() and not self.has_predefined_name():
-            raise LookupError("name must be set or custom specification must be provided")
+            raise LookupError(
+                "name must be set or custom specification must be provided"
+            )
 
         if self.has_custom_specification() and self.has_predefined_name():
-            raise LookupError("If predefined name is provided, "
-                            "custom specification is not allowed. "
-                            "Either remove name or remove all other "
-                            "specification of the distillation unit")
+            raise LookupError(
+                "If predefined name is provided, "
+                "custom specification is not allowed. "
+                "Either remove name or remove all other "
+                "specification of the distillation unit"
+            )
 
         if self.has_predefined_name():
-            return # all other validation is on the server side
+            return  # all other validation is on the server side
 
         if self.num_input_ts is None:
             raise LookupError("num_input_ts must be set")
@@ -255,40 +311,52 @@ class DistillationUnitSpecification(AutoValidatingParams):
             self.logical_qubit_specification.post_validation(result)
 
         if self.logical_qubit_specification_first_round_override is not None:
-            self.logical_qubit_specification_first_round_override.post_validation(result)
+            self.logical_qubit_specification_first_round_override.post_validation(
+                result
+            )
 
     def as_dict(self, validate=True) -> Dict[str, Any]:
         specification_dict = super().as_dict(validate)
         if len(specification_dict) != 0:
             if self.physical_qubit_specification is not None:
-                physical_qubit_specification_dict = \
-                self.physical_qubit_specification.as_dict(validate)
+                physical_qubit_specification_dict = (
+                    self.physical_qubit_specification.as_dict(validate)
+                )
                 if len(physical_qubit_specification_dict) != 0:
-                    specification_dict["physicalQubitSpecification"] = \
-                        physical_qubit_specification_dict
+                    specification_dict[
+                        "physicalQubitSpecification"
+                    ] = physical_qubit_specification_dict
 
             if self.logical_qubit_specification is not None:
-                logical_qubit_specification_dict = \
-                self.logical_qubit_specification.as_dict(validate)
+                logical_qubit_specification_dict = (
+                    self.logical_qubit_specification.as_dict(validate)
+                )
                 if len(logical_qubit_specification_dict) != 0:
-                    specification_dict["logicalQubitSpecification"] = \
-                        logical_qubit_specification_dict
+                    specification_dict[
+                        "logicalQubitSpecification"
+                    ] = logical_qubit_specification_dict
 
             if self.logical_qubit_specification_first_round_override is not None:
-                logical_qubit_specification_first_round_override_dict = \
-                self.logical_qubit_specification_first_round_override.as_dict(validate)
+                logical_qubit_specification_first_round_override_dict = (
+                    self.logical_qubit_specification_first_round_override.as_dict(
+                        validate
+                    )
+                )
                 if len(logical_qubit_specification_first_round_override_dict) != 0:
-                    specification_dict["logicalQubitSpecificationFirstRoundOverride"] = \
-                        logical_qubit_specification_first_round_override_dict
+                    specification_dict[
+                        "logicalQubitSpecificationFirstRoundOverride"
+                    ] = logical_qubit_specification_first_round_override_dict
 
         return specification_dict
 
+
 @dataclass
 class ErrorBudgetPartition(AutoValidatingParams):
     logical: float = 0.001 / 3
     t_states: float = 0.001 / 3
     rotations: float = 0.001 / 3
 
+
 @dataclass
 class EstimatorConstraints(AutoValidatingParams):
     @staticmethod
@@ -303,7 +371,10 @@ class EstimatorConstraints(AutoValidatingParams):
 
     def post_validation(self, result):
         if self.max_duration is not None and self.max_physical_qubits is not None:
-            raise LookupError("Both duration and number of physical qubits constraints are provided, but only one is allowe at a time.")
+            raise LookupError(
+                "Both duration and number of physical qubits constraints are provided, but only one is allowe at a time."
+            )
+
 
 class EstimatorInputParamsItem:
     """
@@ -315,16 +386,16 @@ class EstimatorInputParamsItem:
     def __init__(self):
         super().__init__()
 
-        self.qubit_params: EstimatorQubitParams = \
-            EstimatorQubitParams()
-        self.qec_scheme: EstimatorQecScheme = \
-            EstimatorQecScheme()
-        self.distillation_unit_specifications = []  # type: List[DistillationUnitSpecification]
-        self.constraints: EstimatorConstraints = \
-            EstimatorConstraints()
+        self.qubit_params: EstimatorQubitParams = EstimatorQubitParams()
+        self.qec_scheme: EstimatorQecScheme = EstimatorQecScheme()
+        self.distillation_unit_specifications = (
+            []
+        )  # type: List[DistillationUnitSpecification]
+        self.constraints: EstimatorConstraints = EstimatorConstraints()
         self.error_budget: Optional[Union[float, ErrorBudgetPartition]] = None
+        self.estimate_type: Optional[str] = None
 
-    def as_dict(self, validate=True, additional_params = None) -> Dict[str, Any]:
+    def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]:
         result = {}
 
         qubit_params = self.qubit_params.as_dict(validate)
@@ -350,8 +421,9 @@ class EstimatorInputParamsItem:
                     result["distillationUnitSpecifications"] = []
 
                 result["distillationUnitSpecifications"].append(specification_dict)
-        if result.get("distillationUnitSpecifications") is not None and \
-            hasattr(additional_params, "distillation_unit_specifications"):
+        if result.get("distillationUnitSpecifications") is not None and hasattr(
+            additional_params, "distillation_unit_specifications"
+        ):
             for specification in additional_params.distillation_unit_specifications:
                 specification_dict = specification.as_dict(validate)
                 if len(specification_dict) != 0:
@@ -369,33 +441,43 @@ class EstimatorInputParamsItem:
                 result["constraints"] = constraints
 
         if self.error_budget is not None:
-            if isinstance(self.error_budget, float) or \
-                    isinstance(self.error_budget, int):
-                if validate and \
-                        (self.error_budget <= 0 or self.error_budget >= 1):
+            if isinstance(self.error_budget, float) or isinstance(
+                self.error_budget, int
+            ):
+                if validate and (self.error_budget <= 0 or self.error_budget >= 1):
                     message = "error_budget must be value between 0 and 1"
                     raise ValueError(message)
                 result["errorBudget"] = self.error_budget
             elif isinstance(self.error_budget, ErrorBudgetPartition):
                 result["errorBudget"] = self.error_budget.as_dict(validate)
         elif hasattr(additional_params, "error_budget"):
-            if isinstance(additional_params.error_budget, float) or \
-                    isinstance(additional_params.error_budget, int):
-                if validate and \
-                        (additional_params.error_budget <= 0 or additional_params.error_budget >= 1):
+            if isinstance(additional_params.error_budget, float) or isinstance(
+                additional_params.error_budget, int
+            ):
+                if validate and (
+                    additional_params.error_budget <= 0
+                    or additional_params.error_budget >= 1
+                ):
                     message = "error_budget must be value between 0 and 1"
                     raise ValueError(message)
                 result["errorBudget"] = additional_params.error_budget
             elif isinstance(additional_params.error_budget, ErrorBudgetPartition):
                 result["errorBudget"] = additional_params.error_budget.as_dict(validate)
 
+        if self.estimate_type is not None:
+            if self.estimate_type not in ["frontier", "singlePoint"]:
+                raise ValueError(
+                    "estimate_type must be either 'frontier' or 'singlePoint'"
+                )
+            result["estimateType"] = self.estimate_type
+
         return result
 
+
 class EstimatorParams(EstimatorInputParamsItem):
     MAX_NUM_ITEMS: int = 1000
 
     def __init__(self, num_items: Optional[int] = None):
-
         EstimatorInputParamsItem.__init__(self)
 
         if num_items is not None:
@@ -403,7 +485,8 @@ class EstimatorParams(EstimatorInputParamsItem):
             if num_items <= 0 or num_items > self.MAX_NUM_ITEMS:
                 raise ValueError(
                     "num_items must be a positive value less or equal to "
-                    f"{self.MAX_NUM_ITEMS}")
+                    f"{self.MAX_NUM_ITEMS}"
+                )
             self._items = [EstimatorInputParamsItem() for _ in range(num_items)]
         else:
             self.has_items = False
@@ -413,8 +496,10 @@ class EstimatorParams(EstimatorInputParamsItem):
         if self.has_items:
             return self._items
         else:
-            raise Exception("Cannot access items in a non-batching job, call "
-                            "make_params with num_items parameter")
+            raise Exception(
+                "Cannot access items in a non-batching job, call "
+                "make_params with num_items parameter"
+            )
 
     def as_dict(self, validate=True) -> Dict[str, Any]:
         """
@@ -434,6 +519,7 @@ class EstimatorParams(EstimatorInputParamsItem):
 
         return result
 
+
 class HTMLWrapper:
     """
     Simple HTML wrapper to expose _repr_html_ for Jupyter clients.
@@ -458,9 +544,14 @@ class EstimatorResult(dict):
     MAX_DEFAULT_ITEMS_IN_TABLE = 5
 
     def __init__(self, data: Union[Dict, List]):
-        if isinstance(data, dict) or (isinstance(data, list) and len(data) == 1):
-            if isinstance(data, list):
-                data = data[0]
+        self._error = None
+
+        if isinstance(data, list) and len(data) == 1:
+            data = data[0]
+            if not EstimatorResult._is_succeeded(data):
+                raise EstimatorError(data['code'] ,data['message'])
+
+        if isinstance(data, dict):
             self._data = data
             super().__init__(data)
 
@@ -469,6 +560,8 @@ class EstimatorResult(dict):
                 self._repr = self._item_result_table()
                 self.summary = HTMLWrapper(self._item_result_summary_table())
                 self.diagram = EstimatorResultDiagram(self.data().copy())
+            else:
+                self._error = EstimatorError(data['code'] ,data['message'])
 
         elif isinstance(data, list):
             super().__init__(
@@ -495,7 +588,7 @@ class EstimatorResult(dict):
             self.summary_data_frame = self._summary_data_frame
 
     def _is_succeeded(self):
-        return 'status' in self and self['status'] == "success"
+        return "status" in self and self["status"] == "success"
 
     def data(self, idx: Optional[int] = None) -> Any:
         """
@@ -512,6 +605,13 @@ class EstimatorResult(dict):
             msg = "Cannot pass parameter 'idx' to 'data' for non-batching job"
             raise ValueError(msg)
 
+    @property
+    def error(self) -> Optional[EstimatorError]:
+        """
+        Returns the error object if the result is an error.
+        """
+        return self._error
+
     @property
     def logical_counts(self):
         """
@@ -526,6 +626,8 @@ class EstimatorResult(dict):
         """
         HTML table representation of the result.
         """
+        if self._error:
+            raise self._error
         return self._repr
 
     def __getitem__(self, key):
@@ -542,7 +644,12 @@ class EstimatorResult(dict):
                 raise ValueError(msg)
             return HTMLWrapper(self._batch_result_table(range(len(self))[key]))
         else:
-            return super().__getitem__(key)
+            if super().__contains__(key):
+                return super().__getitem__(key)
+            elif super().__contains__("frontierEntries"):
+                return super().__getitem__("frontierEntries")[0].__getitem__(key)
+            else:
+                raise KeyError(key)
 
     def _plot(self, **kwargs):
         """
@@ -678,10 +785,10 @@ class EstimatorResult(dict):
                     formatted["physicalQubitsForTfactoriesPercentage"],
                     formatted["physicalQubits"],
                     formatted["rqops"],
-                    formatted["runtime"]
+                    formatted["runtime"],
                 )
             else:
-                return ['No solution found'] * 9
+                return ["No solution found"] * 9
 
         data = [get_row(self.data(index)) for index in range(len(self))]
         columns = [
@@ -712,6 +819,8 @@ class EstimatorResult(dict):
             for entry in group["entries"]:
                 val = self
                 for key in entry["path"].split("/"):
+                    if key not in val and "frontierEntries" in val:
+                        val = val["frontierEntries"][0]
                     val = val[key]
                 if has_markdown:
                     explanation = md.convert(entry["explanation"])
@@ -814,7 +923,9 @@ class EstimatorResult(dict):
         return html
 
     def _batch_result_table(self, indices):
-        succeeded_item_indices = [i for i in indices if EstimatorResult._is_succeeded(self[i])]
+        succeeded_item_indices = [
+            i for i in indices if EstimatorResult._is_succeeded(self[i])
+        ]
         if len(succeeded_item_indices) == 0:
             print("None of the jobs succeeded")
             return ""
@@ -828,7 +939,9 @@ class EstimatorResult(dict):
 
         item_headers = "".join(f"<th>{i}</th>" for i in indices)
 
-        for group_index, group in enumerate(self[first_succeeded_item_index]['reportData']['groups']):
+        for group_index, group in enumerate(
+            self[first_succeeded_item_index]["reportData"]["groups"]
+        ):
             html += f"""
                 <details {"open" if group['alwaysVisible'] else ""}>
                     <summary style="display:list-item">
@@ -839,8 +952,12 @@ class EstimatorResult(dict):
 
             visited_entries = set()
 
-            for entry in [entry for index in succeeded_item_indices for entry in self[index]['reportData']['groups'][group_index]['entries']]:
-                label = entry['label']
+            for entry in [
+                entry
+                for index in succeeded_item_indices
+                for entry in self[index]["reportData"]["groups"][group_index]["entries"]
+            ]:
+                label = entry["label"]
                 if label in visited_entries:
                     continue
                 visited_entries.add(label)
@@ -853,7 +970,7 @@ class EstimatorResult(dict):
                 for index in indices:
                     val = self[index]
                     if index in succeeded_item_indices:
-                        for key in entry['path'].split("/"):
+                        for key in entry["path"].split("/"):
                             if key in val:
                                 val = val[key]
                             else:
@@ -880,7 +997,7 @@ class EstimatorResult(dict):
 
     @staticmethod
     def _is_succeeded(obj):
-        return 'status' in obj and obj['status'] == "success"
+        return "status" in obj and obj["status"] == "success"
 
 
 class EstimatorResultDiagram:
@@ -903,6 +1020,7 @@ class EstimatorResultDiagram:
             <re-time-diagram data={self.data_json}></re-time-diagram>"""
         return html
 
+
 class LogicalCounts(dict):
     """
     Microsoft Resource Estimator Logical Counts.
@@ -934,7 +1052,9 @@ class LogicalCounts(dict):
 
         return self._json
 
-    def estimate(self, params: Union[dict, List, EstimatorParams] = None) -> EstimatorResult:
+    def estimate(
+        self, params: Union[dict, List, EstimatorParams] = None
+    ) -> EstimatorResult:
         """
         Estimates resources for the current logical counts, using the
         Parallel Synthesis Sequential Pauli Computation (PSSPC) layout method.
@@ -954,4 +1074,5 @@ class LogicalCounts(dict):
         elif isinstance(params, dict):
             params = [params]
         return EstimatorResult(
-            json.loads(physical_estimates(self.json, json.dumps(params))))
+            json.loads(physical_estimates(self.json, json.dumps(params)))
+        )
diff --git a/vscode/package.json b/vscode/package.json
index 90f2e497..a708b005 100644
--- a/vscode/package.json
+++ b/vscode/package.json
@@ -163,7 +163,7 @@
         },
         {
           "command": "qsharp-vscode.showRe",
-          "when": "resourceLangId == qsharp && config.Q#.experimental-re"
+          "when": "resourceLangId == qsharp"
         },
         {
           "command": "qsharp-vscode.showHelp",
diff --git a/vscode/src/telemetry.ts b/vscode/src/telemetry.ts
index 41ab1c89..6b522654 100644
--- a/vscode/src/telemetry.ts
+++ b/vscode/src/telemetry.ts
@@ -29,6 +29,12 @@ export enum EventType {
   DebugSessionEvent = "Qsharp.DebugSessionEvent",
   Launch = "Qsharp.Launch",
   OpenedDocument = "Qsharp.OpenedDocument",
+  TriggerResourceEstimation = "Qsharp.TriggerResourceEstimation",
+  ResourceEstimationStart = "Qsharp.ResourceEstimationStart",
+  ResourceEstimationEnd = "Qsharp.ResourceEstimationEnd",
+  TriggerHistogram = "Qsharp.TriggerHistogram",
+  HistogramStart = "Qsharp.HistogramStart",
+  HistogramEnd = "Qsharp.HistogramEnd",
 }
 
 type Empty = { [K in any]: never };
@@ -171,6 +177,30 @@ type EventTypes = {
     properties: { documentType: QsharpDocumentType };
     measurements: { linesOfCode: number };
   };
+  [EventType.TriggerResourceEstimation]: {
+    properties: { associationId: string };
+    measurements: Empty;
+  };
+  [EventType.ResourceEstimationStart]: {
+    properties: { associationId: string };
+    measurements: Empty;
+  };
+  [EventType.ResourceEstimationEnd]: {
+    properties: { associationId: string };
+    measurements: { timeToCompleteMs: number };
+  };
+  [EventType.TriggerHistogram]: {
+    properties: { associationId: string };
+    measurements: Empty;
+  };
+  [EventType.HistogramStart]: {
+    properties: { associationId: string };
+    measurements: Empty;
+  };
+  [EventType.HistogramEnd]: {
+    properties: { associationId: string };
+    measurements: { timeToCompleteMs: number };
+  };
 };
 
 export enum QsharpDocumentType {
diff --git a/vscode/src/webview/rePage.tsx b/vscode/src/webview/rePage.tsx
index e1ad3fad..c5b9c508 100644
--- a/vscode/src/webview/rePage.tsx
+++ b/vscode/src/webview/rePage.tsx
@@ -2,13 +2,7 @@
 // Licensed under the MIT License.
 
 import { useState } from "preact/hooks";
-import {
-  ResultsTable,
-  SpaceChart,
-  ReTable,
-  type ReData,
-  type CellValue,
-} from "qsharp-lang/ux";
+import { ResultsTable, SpaceChart, ReTable, type ReData } from "qsharp-lang/ux";
 
 const columnNames = [
   "Run name",
@@ -28,27 +22,6 @@ const columnNames = [
 
 const initialColumns = [0, 1, 2, 3, 4, 10, 11, 12];
 
-function ReDataToRow(data: ReData): CellValue[] {
-  return [
-    data.jobParams.runName,
-    data.jobParams.qubitParams.name,
-    data.jobParams.qecScheme.name,
-    data.jobParams.errorBudget,
-    data.physicalCounts.breakdown.algorithmicLogicalQubits,
-    data.physicalCounts.breakdown.algorithmicLogicalDepth,
-    data.logicalQubit.codeDistance,
-    data.physicalCounts.breakdown.numTstates,
-    data.physicalCounts.breakdown.numTfactories,
-    data.physicalCountsFormatted.physicalQubitsForTfactoriesPercentage,
-    {
-      value: data.physicalCountsFormatted.runtime,
-      sortBy: data.physicalCounts.runtime,
-    },
-    data.physicalCounts.rqops,
-    data.physicalCounts.physicalQubits,
-  ];
-}
-
 export function RePage(props: {
   estimatesData: ReData[];
   calculating: boolean;
@@ -58,6 +31,9 @@ export function RePage(props: {
   const [estimate, setEstimate] = useState<ReData | null>(null);
 
   function onRowSelected(rowId: string) {
+    // On any selection, clear the "new" flag on all rows. This ensures that
+    // new rows do not steal focus from the user selected row.
+    props.estimatesData.forEach((data) => (data.new = false));
     if (!rowId) {
       setEstimate(null);
     } else {
@@ -126,7 +102,7 @@ export function RePage(props: {
         </summary>
         <ResultsTable
           columnNames={columnNames}
-          rows={props.estimatesData.map(ReDataToRow)}
+          data={props.estimatesData}
           initialColumns={initialColumns}
           ensureSelected={true}
           onRowSelected={onRowSelected}
diff --git a/vscode/src/webview/webview.tsx b/vscode/src/webview/webview.tsx
index ae7f8506..6468eb58 100644
--- a/vscode/src/webview/webview.tsx
+++ b/vscode/src/webview/webview.tsx
@@ -84,6 +84,10 @@ function onMessage(event: any) {
         };
         // Copy over any existing estimates
         if ((state as EstimatesState).estimatesData?.estimates) {
+          // Clear the new flag on any existing estimates
+          (state as EstimatesState).estimatesData.estimates.map(
+            (estimate) => (estimate.new = false),
+          );
           newState.estimatesData.estimates.push(
             ...(state as EstimatesState).estimatesData.estimates,
           );
@@ -91,8 +95,11 @@ function onMessage(event: any) {
         // Append any new estimates
         if (message.estimates) {
           if (Array.isArray(message.estimates)) {
+            // Mark all the new estimates
+            message.estimates.map((estimate: ReData) => (estimate.new = true));
             newState.estimatesData.estimates.push(...message.estimates);
           } else {
+            message.estimates.new = true;
             newState.estimatesData.estimates.push(message.estimates);
           }
         }
diff --git a/vscode/src/webviewPanel.ts b/vscode/src/webviewPanel.ts
index 827c41f2..f07427bf 100644
--- a/vscode/src/webviewPanel.ts
+++ b/vscode/src/webviewPanel.ts
@@ -19,6 +19,8 @@ import {
 } from "vscode";
 import { isQsharpDocument } from "./common";
 import { loadProject } from "./projectSystem";
+import { EventType, sendTelemetryEvent } from "./telemetry";
+import { getRandomGuid } from "./utils";
 
 const QSharpWebViewType = "qsharp-webview";
 const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes
@@ -36,11 +38,14 @@ export function registerWebViewCommands(context: ExtensionContext) {
     "./out/compilerWorker.js",
   ).toString();
 
-  // Stub for now to prototype the UX
-  // Add the following to settings.json to enable the command:
-  //   "Q#.experimental-re": true
   context.subscriptions.push(
     commands.registerCommand("qsharp-vscode.showRe", async () => {
+      const associationId = getRandomGuid();
+      sendTelemetryEvent(
+        EventType.TriggerResourceEstimation,
+        { associationId },
+        {},
+      );
       const editor = window.activeTextEditor;
       if (!editor || !isQsharpDocument(editor.document)) {
         throw new Error("The currently active window is not a Q# file");
@@ -49,34 +54,35 @@ export function registerWebViewCommands(context: ExtensionContext) {
       const qubitType = await window.showQuickPick(
         [
           {
-            label: "qubit_gate_us_e3",
-            detail: "Ion-based qubit with 1e-3 error rate",
+            label: "qubit_gate_ns_e3",
+            detail: "Superconducting/spin qubit with 1e-3 error rate",
+            picked: true,
             params: {
-              qubitParams: { name: "qubit_gate_us_e3" },
+              qubitParams: { name: "qubit_gate_ns_e3" },
               qecScheme: { name: "surface_code" },
             },
           },
           {
-            label: "qubit_gate_us_e4",
-            detail: "Ion-based qubit with 1e-4 error rate",
+            label: "qubit_gate_ns_e4",
+            detail: "Superconducting/spin qubit with 1e-4 error rate",
             params: {
-              qubitParams: { name: "qubit_gate_us_e4" },
+              qubitParams: { name: "qubit_gate_ns_e4" },
               qecScheme: { name: "surface_code" },
             },
           },
           {
-            label: "qubit_gate_ns_e3",
-            detail: "Superconducting/spin qubit with 1e-3 error rate",
+            label: "qubit_gate_us_e3",
+            detail: "Trapped ion qubit with 1e-3 error rate",
             params: {
-              qubitParams: { name: "qubit_gate_ns_e3" },
+              qubitParams: { name: "qubit_gate_us_e3" },
               qecScheme: { name: "surface_code" },
             },
           },
           {
-            label: "qubit_gate_ns_e4",
-            detail: "Superconducting/spin qubit with 1e-4 error rate",
+            label: "qubit_gate_us_e4",
+            detail: "Trapped ion qubit with 1e-4 error rate",
             params: {
-              qubitParams: { name: "qubit_gate_ns_e4" },
+              qubitParams: { name: "qubit_gate_us_e4" },
               qecScheme: { name: "surface_code" },
             },
           },
@@ -116,6 +122,8 @@ export function registerWebViewCommands(context: ExtensionContext) {
         {
           canPickMany: true,
           title: "Qubit types",
+          placeHolder: "Superconducting/spin qubit with 1e-3 error rate",
+          matchOnDetail: true,
         },
       );
 
@@ -126,7 +134,7 @@ export function registerWebViewCommands(context: ExtensionContext) {
       // Prompt for error budget (default to 0.001)
       const validateErrorBudget = (input: string) => {
         const result = parseFloat(input);
-        if (isNaN(result) || result < 0 || result > 1) {
+        if (isNaN(result) || result <= 0.0 || result >= 1.0) {
           return "Error budgets must be between 0 and 1";
         }
       };
@@ -142,8 +150,11 @@ export function registerWebViewCommands(context: ExtensionContext) {
         return;
       }
 
+      // use document uri path to get the project name, since it is normalized to `/` separators
+      // see https://code.visualstudio.com/api/references/vscode-api#Uri for difference between
+      // path and fsPath
       const projectName =
-        editor.document.fileName.split("/").pop()?.split(".")[0] || "program";
+        editor.document.uri.path.split("/").pop()?.split(".")[0] || "program";
 
       let runName = await window.showInputBox({
         title: "Friendly name for run",
@@ -192,15 +203,35 @@ export function registerWebViewCommands(context: ExtensionContext) {
 
       try {
         const sources = await loadProject(editor.document.uri);
+        const start = performance.now();
+        sendTelemetryEvent(
+          EventType.ResourceEstimationStart,
+          { associationId },
+          {},
+        );
         const estimatesStr = await worker.getEstimates(
           sources,
           JSON.stringify(params),
         );
+        sendTelemetryEvent(
+          EventType.ResourceEstimationEnd,
+          { associationId },
+          { timeToCompleteMs: performance.now() - start },
+        );
         log.debug("Estimates result", estimatesStr);
 
         // Should be an array of one ReData object returned
         const estimates = JSON.parse(estimatesStr);
 
+        for (const item of estimates) {
+          // if item doesn't have a status property, it's an error
+          if (!("status" in item) || item.status !== "success") {
+            log.error("Estimates error code: ", item.code);
+            log.error("Estimates error message: ", item.message);
+            throw item.message;
+          }
+        }
+
         (estimates as Array<any>).forEach(
           (item, idx) =>
             (item.jobParams.runName = `${runName} (${String.fromCharCode(
@@ -231,8 +262,8 @@ export function registerWebViewCommands(context: ExtensionContext) {
             "The resource estimation timed out. Please try again.",
           );
         } else {
-          log.error("getEstimates error. ", e.toString());
-          throw new Error("Estimating failed. " + e.toString());
+          log.error("getEstimates error: ", e.toString());
+          throw new Error("Estimating failed with error: " + e.toString());
         }
       } finally {
         if (!timedOut) {
@@ -254,6 +285,8 @@ export function registerWebViewCommands(context: ExtensionContext) {
 
   context.subscriptions.push(
     commands.registerCommand("qsharp-vscode.showHistogram", async () => {
+      const associationId = getRandomGuid();
+      sendTelemetryEvent(EventType.TriggerHistogram, { associationId }, {});
       function resultToLabel(result: string | VSDiagnostic): string {
         if (typeof result !== "string") return "ERROR";
         return result;
@@ -310,7 +343,14 @@ export function registerWebViewCommands(context: ExtensionContext) {
           sendMessageToPanel("histogram", false, message);
         });
         const sources = await loadProject(editor.document.uri);
+        const start = performance.now();
+        sendTelemetryEvent(EventType.HistogramStart, { associationId }, {});
         await worker.run(sources, "", parseInt(numberOfShots), evtTarget);
+        sendTelemetryEvent(
+          EventType.HistogramEnd,
+          { associationId },
+          { timeToCompleteMs: performance.now() - start },
+        );
         clearTimeout(compilerTimeout);
       } catch (e: any) {
         log.error("Histogram error. ", e.toString());
diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs
index 3693c100..df203a4c 100644
--- a/wasm/src/lib.rs
+++ b/wasm/src/lib.rs
@@ -13,7 +13,10 @@ use qsc::{
     hir::PackageId,
     interpret::{
         output::{self, Receiver},
-        stateful::{self, re::estimate_entry},
+        stateful::{
+            self,
+            re::{self, estimate_entry},
+        },
     },
     target::Profile,
     PackageStore, PackageType, SourceContents, SourceMap, SourceName, SparseSim,
@@ -109,7 +112,11 @@ pub fn get_estimates(sources: Vec<js_sys::Array>, params: &str) -> Result<String
     )
     .map_err(|e| e[0].to_string())?;
 
-    estimate_entry(&mut interpreter, params).map_err(|e| e[0].to_string())
+    estimate_entry(&mut interpreter, params).map_err(|e| match &e[0] {
+        re::Error::Interpreter(stateful::Error::Eval(e)) => e.to_string(),
+        re::Error::Interpreter(_) => unreachable!("interpreter errors should be eval errors"),
+        re::Error::Estimation(e) => e.to_string(),
+    })
 }
 
 #[wasm_bindgen]
diff --git a/wasm/src/project_system.rs b/wasm/src/project_system.rs
index d6b7a61e..c750b209 100644
--- a/wasm/src/project_system.rs
+++ b/wasm/src/project_system.rs
@@ -86,12 +86,12 @@ where
 
 pub(crate) fn to_js_function(val: JsValue, help_text_panic: &'static str) -> js_sys::Function {
     let js_ty = val.js_typeof();
-    TryInto::<js_sys::Function>::try_into(val).unwrap_or_else(|_| {
-        panic!(
-            "expected a valid JS function ({help_text_panic}), received {:?}",
-            js_ty
-        )
-    })
+    assert!(
+        val.is_function(),
+        "expected a valid JS function ({help_text_panic}), received {:?}",
+        js_ty
+    );
+    Into::<js_sys::Function>::into(val)
 }
 pub(crate) use into_async_rust_fn_with;
 


Test patch:
diff --git a/compiler/qsc/src/interpret/stateful/re/counts/tests.rs b/compiler/qsc/src/interpret/stateful/re/counts/tests.rs
index cd5db3d2..c11e2b3c 100644
--- a/compiler/qsc/src/interpret/stateful/re/counts/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/re/counts/tests.rs
@@ -15,7 +15,7 @@ use indoc::indoc;
 
 use super::LogicalCounter;
 
-fn veryify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) {
+fn verify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) {
     let source_map = SourceMap::new([("test".into(), source.into())], entry.map(Into::into));
     let mut interpreter = Interpreter::new(
         true,
@@ -35,8 +35,8 @@ fn veryify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) {
 
 #[test]
 fn gates_are_counted() {
-    veryify_logical_counts(
-        indoc! {r#"
+    verify_logical_counts(
+        indoc! {"
             namespace Test {
                 open Microsoft.Quantum.Measurement;
 
@@ -61,9 +61,9 @@ fn gates_are_counted() {
                     MResetEachZ(qs)
                 }
             }
-        "#},
+        "},
         None,
-        &expect![[r#"
+        &expect![["
             LogicalResources {
                 num_qubits: 10,
                 t_count: 2,
@@ -72,13 +72,13 @@ fn gates_are_counted() {
                 ccz_count: 2,
                 measurement_count: 10,
             }
-        "#]],
+        "]],
     );
 }
 
 #[test]
 fn estimate_caching_works() {
-    veryify_logical_counts(
+    verify_logical_counts(
         indoc! {r#"
             namespace Test {
                 open Microsoft.Quantum.ResourceEstimation;
@@ -109,6 +109,52 @@ fn estimate_caching_works() {
             }
         "#},
         None,
+        &expect![["
+            LogicalResources {
+                num_qubits: 10,
+                t_count: 1,
+                rotation_count: 300,
+                rotation_depth: 30,
+                ccz_count: 0,
+                measurement_count: 0,
+            }
+        "]],
+    );
+}
+
+#[test]
+fn estimate_repeat_works() {
+    verify_logical_counts(
+        indoc! {r#"
+            namespace Test {
+                open Microsoft.Quantum.ResourceEstimation;
+
+                operation Rotate(qs: Qubit[]) : Unit {
+                    for q in qs {
+                        Rx(1.0, q);
+                        Ry(1.0, q);
+                        Rz(1.0, q);
+                    }
+                }
+
+                @EntryPoint()
+                operation Main() : Unit {
+                    use qs = Qubit[10];
+                    mutable count = 0;
+                    within {
+                        RepeatEstimates(10);
+                    }
+                    apply {
+                        Rotate(qs);
+                        set count += 1;
+                    }
+                    for _ in 1..count {
+                        T(qs[0]);
+                    }
+                }
+            }
+        "#},
+        None,
         &expect![[r#"
             LogicalResources {
                 num_qubits: 10,
@@ -124,8 +170,8 @@ fn estimate_caching_works() {
 
 #[test]
 fn account_for_estimates_works() {
-    veryify_logical_counts(
-        indoc! {r#"
+    verify_logical_counts(
+        indoc! {"
             namespace Test {
                 open Microsoft.Quantum.ResourceEstimation;
 
@@ -145,9 +191,9 @@ fn account_for_estimates_works() {
                         qs);
                 }
             }
-        "#},
+        "},
         None,
-        &expect![[r#"
+        &expect![["
             LogicalResources {
                 num_qubits: 11,
                 t_count: 2,
@@ -156,6 +202,6 @@ fn account_for_estimates_works() {
                 ccz_count: 5,
                 measurement_count: 6,
             }
-        "#]],
+        "]],
     );
 }
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation/tests.rs b/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation/tests.rs
index d7618142..2ce054ce 100644
--- a/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/stages/physical_estimation/tests.rs
@@ -344,7 +344,7 @@ pub fn test_hubbard_e2e_increasing_max_num_qubits() -> super::super::super::Resu
     assert!(result2.physical_qubits() <= max_num_qubits2);
     assert!(result1.runtime() >= result2.runtime());
 
-    assert_eq!(result1.runtime(), 329_008_500_u64);
+    assert_eq!(result1.runtime(), 329_010_000_u64);
     assert_eq!(result2.runtime(), 33_934_500_u64);
     Ok(())
 }
@@ -423,7 +423,7 @@ pub fn test_chemistry_based_max_duration() -> super::super::super::Result<()> {
     assert_eq!(result.physical_qubits_for_algorithm(), 4_351_120);
     assert_eq!(result.physical_qubits(), 4_923_120);
 
-    assert_eq!(result.runtime(), 22_371_634_030_828_800_u64);
+    assert_eq!(result.runtime(), 22_371_634_030_834_500_u64);
 
     assert_eq!(tfactory.physical_qubits(), 286_000);
     assert_eq!(tfactory.num_rounds(), 4);
@@ -482,7 +482,7 @@ pub fn test_chemistry_based_max_num_qubits() -> super::super::super::Result<()>
     assert_eq!(result.physical_qubits_for_tfactories(), 572_000);
     assert_eq!(result.physical_qubits_for_algorithm(), 4_351_120);
     assert_eq!(result.physical_qubits(), 4_923_120);
-    assert_eq!(result.runtime(), 22_371_634_030_828_800_u64);
+    assert_eq!(result.runtime(), 22_371_634_030_834_500_u64);
 
     assert_eq!(tfactory.physical_qubits(), 286_000);
     assert_eq!(tfactory.num_rounds(), 4);
@@ -649,6 +649,174 @@ pub fn test_factorization_2048_max_num_qubits_matches_regular_estimate(
     Ok(())
 }
 
+fn prepare_ising20x20_estimation_with_pessimistic_gate_based(
+) -> PhysicalResourceEstimation<LogicalResourceCounts> {
+    let ftp = Protocol::surface_code_gate_based();
+    let qubit = Rc::new(PhysicalQubit::qubit_gate_us_e3());
+
+    let value = r#"{
+        "numQubits": 100,
+        "tCount": 0,
+        "rotationCount": 112110,
+        "rotationDepth": 2001,
+        "cczCount": 0,
+        "ccixCount": 0,
+        "measurementCount": 0
+    }"#;
+
+    let counts: LogicalResourceCounts = serde_json::from_str(value).expect("cannot parse json");
+
+    let partitioning = ErrorBudgetSpecification::Total(1e-3)
+        .partitioning(&counts)
+        .expect("cannot setup error budget partitioning");
+    PhysicalResourceEstimation::new(ftp, qubit, counts, partitioning)
+}
+
+#[test]
+fn build_frontier_test() {
+    let estimation = prepare_ising20x20_estimation_with_pessimistic_gate_based();
+
+    let frontier_result = estimation.build_frontier();
+
+    let points = frontier_result.expect("failed to estimate");
+    assert_eq!(points.len(), 195);
+
+    for i in 0..points.len() - 1 {
+        assert!(points[i].runtime() <= points[i + 1].runtime());
+        assert!(points[i].physical_qubits() >= points[i + 1].physical_qubits());
+        assert!(points[i].num_tfactories() >= points[i + 1].num_tfactories());
+        assert!(
+            points[i].logical_qubit().code_distance()
+                <= points[i + 1].logical_qubit().code_distance()
+        );
+    }
+
+    let shortest_runtime_result = estimation.estimate().expect("failed to estimate");
+    assert_eq!(points[0].runtime(), shortest_runtime_result.runtime());
+    assert_eq!(
+        points[0].physical_qubits(),
+        shortest_runtime_result.physical_qubits()
+    );
+    assert_eq!(
+        points[0].num_tfactories(),
+        shortest_runtime_result.num_tfactories()
+    );
+    assert_eq!(
+        points[0].logical_qubit().code_distance(),
+        shortest_runtime_result.logical_qubit().code_distance()
+    );
+
+    let mut max_duration = shortest_runtime_result.runtime();
+    let num_iterations = 100;
+    let coefficient = 1.05;
+    for _ in 0..num_iterations {
+        max_duration = (max_duration as f64 * coefficient) as u64;
+        let result = estimation
+            .estimate_with_max_duration(max_duration)
+            .expect("failed to estimate");
+
+        assert!(
+            points
+                .iter()
+                .filter(|point| point.runtime() <= result.runtime()
+                    && point.physical_qubits() == result.physical_qubits())
+                .count()
+                == 1
+        );
+    }
+
+    let mut max_num_qubits = shortest_runtime_result.physical_qubits();
+    let num_iterations = 100;
+    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);
+
+        if let Ok(result) = result {
+            assert!(
+                points
+                    .iter()
+                    .filter(|point| point.runtime() <= result.runtime()
+                        && point.physical_qubits() == result.physical_qubits())
+                    .count()
+                    == 1
+            );
+        }
+    }
+}
+
+fn prepare_bit_flip_code_resources_and_majorana_n6_qubit(
+) -> PhysicalResourceEstimation<LogicalResourceCounts> {
+    let qubit = Rc::new(PhysicalQubit::qubit_maj_ns_e6());
+    let ftp = Protocol::floquet_code();
+
+    let value = r#"{
+        "numQubits": 5,
+        "tCount": 0,
+        "rotationCount": 1,
+        "rotationDepth": 1,
+        "cczCount": 0,
+        "ccixCount": 0,
+        "measurementCount": 3
+    }"#;
+
+    let counts: LogicalResourceCounts = serde_json::from_str(value).expect("cannot parse json");
+
+    let partitioning = ErrorBudgetSpecification::Total(1e-3)
+        .partitioning(&counts)
+        .expect("cannot setup error budget partitioning");
+    PhysicalResourceEstimation::new(ftp, qubit, counts, partitioning)
+}
+
+#[test]
+fn build_frontier_bit_flip_code_test() {
+    let estimation = prepare_bit_flip_code_resources_and_majorana_n6_qubit();
+
+    let frontier_result = estimation.build_frontier();
+
+    let points = frontier_result.expect("failed to estimate");
+    assert_eq!(points.len(), 7);
+
+    let shortest_runtime_result = estimation.estimate().expect("failed to estimate");
+
+    assert_eq!(points[0].runtime(), shortest_runtime_result.runtime());
+    assert_eq!(
+        points[0]
+            .tfactory()
+            .expect("point should have valid tfactory value")
+            .duration(),
+        shortest_runtime_result
+            .tfactory()
+            .expect("shortest result should have valid tfactory value")
+            .duration()
+    );
+
+    assert_eq!(
+        points[0]
+            .tfactory()
+            .expect("point should have valid tfactory value")
+            .physical_qubits(),
+        shortest_runtime_result
+            .tfactory()
+            .expect("shortest result should have valid tfactory value")
+            .physical_qubits()
+    );
+
+    assert_eq!(
+        points[0].num_tfactories(),
+        shortest_runtime_result.num_tfactories()
+    );
+
+    assert_eq!(
+        points[0].physical_qubits(),
+        shortest_runtime_result.physical_qubits()
+    );
+    assert_eq!(
+        points[0].num_tfactories(),
+        shortest_runtime_result.num_tfactories()
+    );
+}
+
 #[allow(clippy::cast_lossless)]
 #[test]
 fn code_distance_tests() {
diff --git a/compiler/qsc/src/interpret/stateful/re/estimates/tests.rs b/compiler/qsc/src/interpret/stateful/re/estimates/tests.rs
new file mode 100644
index 00000000..a4fa0e8e
--- /dev/null
+++ b/compiler/qsc/src/interpret/stateful/re/estimates/tests.rs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use serde_json::Value;
+
+use crate::interpret::stateful::re::LogicalResources;
+
+use super::estimate_physical_resources;
+
+#[test]
+fn estimate_single() {
+    let logical_resources = LogicalResources {
+        num_qubits: 100,
+        t_count: 0,
+        rotation_count: 112_110,
+        rotation_depth: 2001,
+        ccz_count: 0,
+        measurement_count: 0,
+    };
+
+    let params: &str = "[{}]";
+    let result = estimate_physical_resources(&logical_resources, params);
+
+    let json_value: Vec<Value> =
+        serde_json::from_str(&result.expect("result is err")).expect("Failed to parse JSON");
+    assert_eq!(json_value.len(), 1);
+
+    let map = json_value[0].as_object().expect("Failed build map");
+    assert!(!map.contains_key("frontierEntries"));
+    assert!(map.contains_key("logicalQubit"));
+    assert!(map.contains_key("physicalCounts"));
+    assert!(map.contains_key("physicalCountsFormatted"));
+}
+
+#[test]
+fn estimate_frontier() {
+    let logical_resources = LogicalResources {
+        num_qubits: 100,
+        t_count: 0,
+        rotation_count: 112_110,
+        rotation_depth: 2001,
+        ccz_count: 0,
+        measurement_count: 0,
+    };
+
+    let params: &str = r#"[{
+        "estimateType": "frontier"
+    }]"#;
+
+    let result = estimate_physical_resources(&logical_resources, params);
+
+    let json_value: Vec<Value> =
+        serde_json::from_str(&result.expect("result is err")).expect("Failed to parse JSON");
+    assert_eq!(json_value.len(), 1);
+
+    let map = json_value[0].as_object().expect("Failed build map");
+    assert!(map.contains_key("frontierEntries"));
+    assert!(!map.contains_key("logicalQubit"));
+    assert!(!map.contains_key("physicalCounts"));
+    assert!(!map.contains_key("physicalCountsFormatted"));
+}
+
+#[test]
+fn physical_estimates_crash() {
+    let result = estimate_physical_resources(
+        &LogicalResources {
+            num_qubits: 9,
+            t_count: 160,
+            rotation_count: 0,
+            rotation_depth: 0,
+            ccz_count: 8,
+            measurement_count: 5,
+        },
+        r#"[{"qubitParams": {"name": "qubit_maj_ns_e6"},
+            "qecScheme": {"name": "floquet_code"},
+            "errorBudget": 0.075}]"#,
+    );
+
+    assert!(result
+        .expect("estimation should succeed")
+        .contains(r#""status":"success"#));
+}
diff --git a/compiler/qsc/src/interpret/stateful/stepping_tests.rs b/compiler/qsc/src/interpret/stateful/stepping_tests.rs
index f1033db9..958f3e3b 100644
--- a/compiler/qsc/src/interpret/stateful/stepping_tests.rs
+++ b/compiler/qsc/src/interpret/stateful/stepping_tests.rs
@@ -10,6 +10,104 @@ use qsc_frontend::compile::{RuntimeCapabilityFlags, SourceMap};
 use qsc_passes::PackageType;
 use std::io::Cursor;
 
+fn get_breakpoint_ids(interpreter: &Interpreter, path: &str) -> Vec<StmtId> {
+    let mut bps = interpreter.get_breakpoints(path);
+    bps.sort_by_key(|f| f.id);
+    let ids = bps.iter().map(|f| f.id.into()).collect::<Vec<_>>();
+    ids
+}
+
+fn expect_return(mut interpreter: Interpreter, expected: &str) {
+    let r = step_next(&mut interpreter, &[]);
+    match r.0 {
+        Ok(StepResult::Return(value)) => assert_eq!(value.to_string(), expected),
+        Ok(v) => panic!("Expected Return, got {v:?}"),
+        Err(e) => panic!("Expected Return, got {e:?}"),
+    }
+}
+
+fn expect_bp(interpreter: &mut Interpreter, ids: &[StmtId], expected_id: StmtId) {
+    let r = step_next(interpreter, ids);
+    match r.0 {
+        Ok(StepResult::BreakpointHit(actual_id)) => assert!(actual_id == expected_id),
+        Ok(v) => panic!("Expected BP, got {v:?}"),
+        Err(e) => panic!("Expected BP, got {e:?}"),
+    }
+}
+
+fn step_in(
+    interpreter: &mut Interpreter,
+    breakpoints: &[StmtId],
+) -> (
+    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
+    String,
+) {
+    step(interpreter, breakpoints, qsc_eval::StepAction::In)
+}
+
+fn step_next(
+    interpreter: &mut Interpreter,
+    breakpoints: &[StmtId],
+) -> (
+    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
+    String,
+) {
+    step(interpreter, breakpoints, qsc_eval::StepAction::Next)
+}
+
+fn step_out(
+    interpreter: &mut Interpreter,
+    breakpoints: &[StmtId],
+) -> (
+    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
+    String,
+) {
+    step(interpreter, breakpoints, qsc_eval::StepAction::Out)
+}
+
+fn step(
+    interpreter: &mut Interpreter,
+    breakpoints: &[StmtId],
+    step: StepAction,
+) -> (
+    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
+    String,
+) {
+    let mut cursor = Cursor::new(Vec::<u8>::new());
+    let mut receiver = CursorReceiver::new(&mut cursor);
+    (
+        interpreter.eval_step(&mut receiver, breakpoints, step),
+        receiver.dump(),
+    )
+}
+
+fn expect_next(interpreter: &mut Interpreter) {
+    let result = step_next(interpreter, &[]);
+    match result.0 {
+        Ok(StepResult::Next) => (),
+        Ok(v) => panic!("Expected Next, got {v:?}"),
+        Err(e) => panic!("Expected Next, got {e:?}"),
+    }
+}
+
+fn expect_in(interpreter: &mut Interpreter) {
+    let result = step_in(interpreter, &[]);
+    match result.0 {
+        Ok(StepResult::StepIn) => (),
+        Ok(v) => panic!("Expected StepIn, got {v:?}"),
+        Err(e) => panic!("Expected StepIn, got {e:?}"),
+    }
+}
+
+fn expect_out(interpreter: &mut Interpreter) {
+    let result = step_out(interpreter, &[]);
+    match result.0 {
+        Ok(StepResult::StepOut) => (),
+        Ok(v) => panic!("Expected StepOut, got {v:?}"),
+        Err(e) => panic!("Expected StepOut, got {e:?}"),
+    }
+}
+
 #[cfg(test)]
 mod given_interpreter_with_sources {
     use super::*;
@@ -135,101 +233,3 @@ mod given_interpreter_with_sources {
         }
     }
 }
-
-fn get_breakpoint_ids(interpreter: &Interpreter, path: &str) -> Vec<StmtId> {
-    let mut bps = interpreter.get_breakpoints(path);
-    bps.sort_by_key(|f| f.id);
-    let ids = bps.iter().map(|f| f.id.into()).collect::<Vec<_>>();
-    ids
-}
-
-fn expect_return(mut interpreter: Interpreter, expected: &str) {
-    let r = step_next(&mut interpreter, &[]);
-    match r.0 {
-        Ok(StepResult::Return(value)) => assert_eq!(value.to_string(), expected),
-        Ok(v) => panic!("Expected Return, got {v:?}"),
-        Err(e) => panic!("Expected Return, got {e:?}"),
-    }
-}
-
-fn expect_bp(interpreter: &mut Interpreter, ids: &[StmtId], expected_id: StmtId) {
-    let r = step_next(interpreter, ids);
-    match r.0 {
-        Ok(StepResult::BreakpointHit(actual_id)) => assert!(actual_id == expected_id),
-        Ok(v) => panic!("Expected BP, got {v:?}"),
-        Err(e) => panic!("Expected BP, got {e:?}"),
-    }
-}
-
-fn step_in(
-    interpreter: &mut Interpreter,
-    breakpoints: &[StmtId],
-) -> (
-    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
-    String,
-) {
-    step(interpreter, breakpoints, qsc_eval::StepAction::In)
-}
-
-fn step_next(
-    interpreter: &mut Interpreter,
-    breakpoints: &[StmtId],
-) -> (
-    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
-    String,
-) {
-    step(interpreter, breakpoints, qsc_eval::StepAction::Next)
-}
-
-fn step_out(
-    interpreter: &mut Interpreter,
-    breakpoints: &[StmtId],
-) -> (
-    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
-    String,
-) {
-    step(interpreter, breakpoints, qsc_eval::StepAction::Out)
-}
-
-fn step(
-    interpreter: &mut Interpreter,
-    breakpoints: &[StmtId],
-    step: StepAction,
-) -> (
-    Result<StepResult, Vec<crate::interpret::stateful::Error>>,
-    String,
-) {
-    let mut cursor = Cursor::new(Vec::<u8>::new());
-    let mut receiver = CursorReceiver::new(&mut cursor);
-    (
-        interpreter.eval_step(&mut receiver, breakpoints, step),
-        receiver.dump(),
-    )
-}
-
-fn expect_next(interpreter: &mut Interpreter) {
-    let result = step_next(interpreter, &[]);
-    match result.0 {
-        Ok(StepResult::Next) => (),
-        Ok(v) => panic!("Expected Next, got {v:?}"),
-        Err(e) => panic!("Expected Next, got {e:?}"),
-    }
-}
-
-fn expect_in(interpreter: &mut Interpreter) {
-    let result = step_in(interpreter, &[]);
-    match result.0 {
-        Ok(StepResult::StepIn) => (),
-        Ok(v) => panic!("Expected StepIn, got {v:?}"),
-        Err(e) => panic!("Expected StepIn, got {e:?}"),
-    }
-}
-
-fn expect_out(interpreter: &mut Interpreter) {
-    let result = step_out(interpreter, &[]);
-    match result.0 {
-        Ok(StepResult::StepOut) => (),
-        Ok(v) => panic!("Expected StepOut, got {v:?}"),
-        Err(e) => panic!("Expected StepOut, got {e:?}"),
-    }
-}
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index db62b034..a885c800 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -1045,6 +1045,91 @@ mod given_interpreter {
         }
     }
 
+    fn get_interpreter() -> Interpreter {
+        Interpreter::new(
+            true,
+            SourceMap::default(),
+            PackageType::Lib,
+            RuntimeCapabilityFlags::all(),
+        )
+        .expect("interpreter should be created")
+    }
+
+    fn is_only_value(result: &InterpretResult, output: &str, value: &Value) {
+        assert_eq!("", output);
+
+        match result {
+            Ok(v) => assert_eq!(value, v),
+            Err(e) => panic!("Expected unit value, got {e:?}"),
+        }
+    }
+
+    fn is_unit_with_output_eval_entry(
+        result: &Result<Value, Vec<crate::interpret::stateful::Error>>,
+        output: &str,
+        expected_output: &str,
+    ) {
+        assert_eq!(expected_output, output);
+
+        match result {
+            Ok(value) => assert_eq!(Value::unit(), *value),
+            Err(e) => panic!("Expected unit value, got {e:?}"),
+        }
+    }
+
+    fn is_unit_with_output(result: &InterpretResult, output: &str, expected_output: &str) {
+        assert_eq!(expected_output, output);
+
+        match result {
+            Ok(value) => assert_eq!(Value::unit(), *value),
+            Err(e) => panic!("Expected unit value, got {e:?}"),
+        }
+    }
+
+    fn is_only_error<E>(result: &Result<Value, Vec<E>>, output: &str, expected_errors: &Expect)
+    where
+        E: Diagnostic,
+    {
+        assert_eq!("", output);
+
+        match result {
+            Ok(value) => panic!("Expected error , got {value:?}"),
+            Err(errors) => is_error(errors, expected_errors),
+        }
+    }
+
+    fn is_error<E>(errors: &Vec<E>, expected_errors: &Expect)
+    where
+        E: Diagnostic,
+    {
+        let mut actual = String::new();
+        for error in errors {
+            write!(actual, "{error}").expect("writing should succeed");
+            for s in iter::successors(error.source(), |&s| s.source()) {
+                write!(actual, ": {s}").expect("writing should succeed");
+            }
+            for label in error.labels().into_iter().flatten() {
+                let span = error
+                    .source_code()
+                    .expect("expected valid source code")
+                    .read_span(label.inner(), 0, 0)
+                    .expect("expected to be able to read span");
+
+                write!(
+                    actual,
+                    "\n  {} [{}] [{}]",
+                    label.label().unwrap_or(""),
+                    span.name().expect("expected source file name"),
+                    from_utf8(span.data()).expect("expected valid utf-8 string"),
+                )
+                .expect("writing should succeed");
+            }
+            writeln!(actual).expect("writing should succeed");
+        }
+
+        expected_errors.assert_eq(&actual);
+    }
+
     #[cfg(test)]
     mod with_sources {
         use std::sync::Arc;
@@ -1233,89 +1318,4 @@ mod given_interpreter {
             );
         }
     }
-
-    fn get_interpreter() -> Interpreter {
-        Interpreter::new(
-            true,
-            SourceMap::default(),
-            PackageType::Lib,
-            RuntimeCapabilityFlags::all(),
-        )
-        .expect("interpreter should be created")
-    }
-
-    fn is_only_value(result: &InterpretResult, output: &str, value: &Value) {
-        assert_eq!("", output);
-
-        match result {
-            Ok(v) => assert_eq!(value, v),
-            Err(e) => panic!("Expected unit value, got {e:?}"),
-        }
-    }
-
-    fn is_unit_with_output_eval_entry(
-        result: &Result<Value, Vec<crate::interpret::stateful::Error>>,
-        output: &str,
-        expected_output: &str,
-    ) {
-        assert_eq!(expected_output, output);
-
-        match result {
-            Ok(value) => assert_eq!(Value::unit(), *value),
-            Err(e) => panic!("Expected unit value, got {e:?}"),
-        }
-    }
-
-    fn is_unit_with_output(result: &InterpretResult, output: &str, expected_output: &str) {
-        assert_eq!(expected_output, output);
-
-        match result {
-            Ok(value) => assert_eq!(Value::unit(), *value),
-            Err(e) => panic!("Expected unit value, got {e:?}"),
-        }
-    }
-
-    fn is_only_error<E>(result: &Result<Value, Vec<E>>, output: &str, expected_errors: &Expect)
-    where
-        E: Diagnostic,
-    {
-        assert_eq!("", output);
-
-        match result {
-            Ok(value) => panic!("Expected error , got {value:?}"),
-            Err(errors) => is_error(errors, expected_errors),
-        }
-    }
-
-    fn is_error<E>(errors: &Vec<E>, expected_errors: &Expect)
-    where
-        E: Diagnostic,
-    {
-        let mut actual = String::new();
-        for error in errors {
-            write!(actual, "{error}").expect("writing should succeed");
-            for s in iter::successors(error.source(), |&s| s.source()) {
-                write!(actual, ": {s}").expect("writing should succeed");
-            }
-            for label in error.labels().into_iter().flatten() {
-                let span = error
-                    .source_code()
-                    .expect("expected valid source code")
-                    .read_span(label.inner(), 0, 0)
-                    .expect("expected to be able to read span");
-
-                write!(
-                    actual,
-                    "\n  {} [{}] [{}]",
-                    label.label().unwrap_or(""),
-                    span.name().expect("expected source file name"),
-                    from_utf8(span.data()).expect("expected valid utf-8 string"),
-                )
-                .expect("writing should succeed");
-            }
-            writeln!(actual).expect("writing should succeed");
-        }
-
-        expected_errors.assert_eq(&actual);
-    }
 }
diff --git a/pip/tests/test_re.py b/pip/tests/test_re.py
index 5e1fa8c2..748aa6d7 100644
--- a/pip/tests/test_re.py
+++ b/pip/tests/test_re.py
@@ -187,6 +187,7 @@ def test_estimation_from_logical_counts_with_single_params() -> None:
     assert res["physicalCounts"] is not None
     assert res["jobParams"]["qubitParams"]["name"] == "qubit_maj_ns_e4"
     assert res.logical_counts == logical_counts
+    assert "frontierEntries" not in res
 
 
 def test_estimation_from_logical_counts_with_multiple_params() -> None:
@@ -219,3 +220,32 @@ def test_estimation_from_logical_counts_with_multiple_params() -> None:
         )
         assert res[idx]["logicalCounts"] == logical_counts
     assert res[2]["jobParams"]["qecScheme"]["name"] == QECScheme.FLOQUET_CODE
+
+
+def test_building_frontier_from_logical_counts_with_single_params() -> None:
+    logical_counts = LogicalCounts(
+        {
+            "numQubits": 12581,
+            "tCount": 12,
+            "rotationCount": 12,
+            "rotationDepth": 12,
+            "cczCount": 3731607428,
+            "measurementCount": 1078154040,
+        }
+    )
+    params = EstimatorParams()
+    params.error_budget = 0.333
+    params.qubit_params.name = QubitParams.MAJ_NS_E4
+    params.estimate_type = "frontier"
+    res = logical_counts.estimate(params=params)
+
+    assert res["status"] == "success"
+    assert "physicalCounts" not in res
+    assert "physicalCountsFormatted" not in res
+    assert res["jobParams"]["qubitParams"]["name"] == "qubit_maj_ns_e4"
+    assert res.logical_counts == logical_counts
+    assert res["frontierEntries"] is not None
+    assert len(res["frontierEntries"]) > 0
+    first_entry = res["frontierEntries"][0]
+    assert first_entry["physicalCounts"] is not None
+    assert first_entry["physicalCountsFormatted"] is not None