Repository: microsoft/qsharp
Pull request #1029 (2024-01-17 16:09:45)

Turn core/std lib into a crate
This change makes core/std lib sources available via a crate instead of loading through a macro in the compiler. General effect is that the sources should only be compiled when changes are made to the library vs the compiler speeding up workflows in the compiler.

This can also help in future use of the language services if needed to have direct access to the library sources.

The tests have been updated to follow more established patterns for tests used in the rest of the repository.


Patch:
diff --git a/.ado/publish.yml b/.ado/publish.yml
index a908b996..7b213815 100644
--- a/.ado/publish.yml
+++ b/.ado/publish.yml
@@ -5,7 +5,7 @@ pr: none
 
 variables:
   CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.73"
+  RUST_TOOLCHAIN_VERSION: "1.75"
 
 # variables set by pipeline
 # - BASE_IMAGE
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 12156e60..7a28dfcd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ env:
   CARGO_TERM_COLOR: always
   NODE_VERSION: "18.17.1"
   PYTHON_VERSION: "3.11"
-  RUST_TOOLCHAIN_VERSION: "1.73"
+  RUST_TOOLCHAIN_VERSION: "1.75"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 jobs:
diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml
index 4e5d6d98..9e757498 100644
--- a/.github/workflows/publish-playground.yml
+++ b/.github/workflows/publish-playground.yml
@@ -11,7 +11,7 @@ on:
 
 env:
   CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.73"
+  RUST_TOOLCHAIN_VERSION: "1.75"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
diff --git a/Cargo.lock b/Cargo.lock
index a7b215d3..9d74eb54 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -577,6 +577,15 @@ version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
+[[package]]
+name = "library"
+version = "0.0.0"
+dependencies = [
+ "indoc",
+ "num-bigint",
+ "qsc",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.10"
@@ -973,6 +982,7 @@ dependencies = [
  "enum-iterator",
  "expect-test",
  "indoc",
+ "library",
  "miette",
  "num-bigint",
  "num-traits",
@@ -1342,15 +1352,6 @@ dependencies = [
  "libm",
 ]
 
-[[package]]
-name = "stdlib_tests"
-version = "0.0.0"
-dependencies = [
- "indoc",
- "num-bigint",
- "qsc",
-]
-
 [[package]]
 name = "strsim"
 version = "0.10.0"
diff --git a/Cargo.toml b/Cargo.toml
index 069f3f22..70fe31de 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,7 @@ members = [
     "fuzz",
     "katas",
     "language_service",
-    "library/tests",
+    "library",
     "pip",
     "resource_estimator",
     "wasm",
diff --git a/compiler/qsc/src/interpret/stateful.rs b/compiler/qsc/src/interpret/stateful.rs
index 9ebaf982..0e31dc8a 100644
--- a/compiler/qsc/src/interpret/stateful.rs
+++ b/compiler/qsc/src/interpret/stateful.rs
@@ -295,9 +295,8 @@ impl Interpreter {
         &mut self,
         receiver: &mut impl Receiver,
         expr: &str,
-        shots: u32,
-    ) -> Result<Vec<InterpretResult>, Vec<Error>> {
-        self.run_with_sim(&mut SparseSim::new(), receiver, expr, shots)
+    ) -> Result<InterpretResult, Vec<Error>> {
+        self.run_with_sim(&mut SparseSim::new(), receiver, expr)
     }
 
     /// Gets the current quantum state of the simulator.
@@ -316,11 +315,7 @@ impl Interpreter {
         let mut stdout = std::io::sink();
         let mut out = GenericReceiver::new(&mut stdout);
 
-        let val = self
-            .run_with_sim(&mut sim, &mut out, expr, 1)?
-            .into_iter()
-            .last()
-            .expect("execution should have at least one result")?;
+        let val = self.run_with_sim(&mut sim, &mut out, expr)??;
 
         Ok(sim.finish(&val))
     }
@@ -332,11 +327,10 @@ impl Interpreter {
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
         expr: &str,
-        shots: u32,
-    ) -> Result<Vec<InterpretResult>, Vec<Error>> {
+    ) -> Result<InterpretResult, Vec<Error>> {
         let stmt_id = self.compile_expr_to_stmt(expr)?;
 
-        Ok(self.run_internal(sim, receiver, stmt_id, shots))
+        Ok(self.run_internal(sim, receiver, stmt_id))
     }
 
     fn run_internal(
@@ -344,37 +338,23 @@ impl Interpreter {
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
         stmt_id: StmtId,
-        shots: u32,
-    ) -> Vec<InterpretResult> {
-        let mut results: Vec<InterpretResult> = Vec::new();
-        for i in 0..shots {
-            results.push(
-                match eval_stmt(
-                    stmt_id,
-                    &self.fir_store,
-                    &mut Env::with_empty_scope(),
-                    sim,
-                    self.package,
-                    receiver,
-                ) {
-                    Ok(value) => Ok(value),
-                    Err((error, call_stack)) => Err(eval_error(
-                        self.compiler.package_store(),
-                        &self.fir_store,
-                        call_stack,
-                        error,
-                    )),
-                },
-            );
-
-            if i != 0 {
-                // If running more than one shot, re-initialize the simulator to start the next shot
-                // from a clean state.
-                sim.reinit();
-            }
+    ) -> InterpretResult {
+        match eval_stmt(
+            stmt_id,
+            &self.fir_store,
+            &mut Env::with_empty_scope(),
+            sim,
+            self.package,
+            receiver,
+        ) {
+            Ok(value) => Ok(value),
+            Err((error, call_stack)) => Err(eval_error(
+                self.compiler.package_store(),
+                &self.fir_store,
+                call_stack,
+                error,
+            )),
         }
-
-        results
     }
 
     fn compile_expr_to_stmt(&mut self, expr: &str) -> Result<StmtId, Vec<Error>> {
diff --git a/compiler/qsc_codegen/src/qir_base.rs b/compiler/qsc_codegen/src/qir_base.rs
index dbc0671c..82591cb7 100644
--- a/compiler/qsc_codegen/src/qir_base.rs
+++ b/compiler/qsc_codegen/src/qir_base.rs
@@ -437,10 +437,6 @@ impl Backend for BaseProfSim {
         // true to avoid a panic.
         true
     }
-
-    fn reinit(&mut self) {
-        *self = Self::default();
-    }
 }
 
 struct Qubit(HardwareId);
diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs
index 22c7bb84..2cc80d31 100644
--- a/compiler/qsc_eval/src/backend.rs
+++ b/compiler/qsc_eval/src/backend.rs
@@ -38,7 +38,6 @@ pub trait Backend {
     fn qubit_release(&mut self, q: usize);
     fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex<f64>)>, usize);
     fn qubit_is_zero(&mut self, q: usize) -> bool;
-    fn reinit(&mut self);
 
     fn custom_intrinsic(&mut self, _name: &str, _arg: Value) -> Option<Result<Value, String>> {
         None
@@ -194,10 +193,6 @@ impl Backend for SparseSim {
         self.sim.qubit_is_zero(q)
     }
 
-    fn reinit(&mut self) {
-        *self = Self::new();
-    }
-
     fn custom_intrinsic(&mut self, name: &str, _arg: Value) -> Option<Result<Value, String>> {
         match name {
             "BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
diff --git a/compiler/qsc_fir/src/fir.rs b/compiler/qsc_fir/src/fir.rs
index 4390dc1a..c3b721d7 100644
--- a/compiler/qsc_fir/src/fir.rs
+++ b/compiler/qsc_fir/src/fir.rs
@@ -521,6 +521,15 @@ impl PackageStore {
     }
 }
 
+impl<'a> IntoIterator for &'a PackageStore {
+    type IntoIter = qsc_data_structures::index_map::Iter<'a, PackageId, Package>;
+    type Item = (PackageId, &'a Package);
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
 /// A trait to find elements in a package.
 pub trait PackageLookup {
     /// Gets a block.
diff --git a/compiler/qsc_frontend/Cargo.toml b/compiler/qsc_frontend/Cargo.toml
index 61b09045..7d210d7d 100644
--- a/compiler/qsc_frontend/Cargo.toml
+++ b/compiler/qsc_frontend/Cargo.toml
@@ -18,6 +18,7 @@ qsc_ast = { path = "../qsc_ast" }
 qsc_data_structures = { path = "../qsc_data_structures" }
 qsc_hir = { path = "../qsc_hir" }
 qsc_parse = { path = "../qsc_parse" }
+library = { path = "../../library" }
 rustc-hash = { workspace = true }
 thiserror = { workspace = true }
 
diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs
index 12801371..909afefc 100644
--- a/compiler/qsc_frontend/src/compile.rs
+++ b/compiler/qsc_frontend/src/compile.rs
@@ -39,9 +39,9 @@ use thiserror::Error;
 bitflags! {
     #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     pub struct RuntimeCapabilityFlags: u32 {
-        const ConditionalForwardBranching = 0b0000_0001;
+        const ForwardBranching = 0b0000_0001;
         const IntegerComputations = 0b0000_0010;
-        const FloatingPointComputation = 0b0000_0100;
+        const FloatingPointComputations = 0b0000_0100;
         const BackwardsBranching = 0b0000_1000;
         const HigherLevelConstructs = 0b0001_0000;
     }
@@ -405,19 +405,11 @@ pub fn core() -> CompileUnit {
         next_id: PackageId::CORE,
     };
 
-    let sources = SourceMap::new(
-        [
-            (
-                "core/core.qs".into(),
-                include_str!("../../../library/core/core.qs").into(),
-            ),
-            (
-                "core/qir.qs".into(),
-                include_str!("../../../library/core/qir.qs").into(),
-            ),
-        ],
-        None,
-    );
+    let core: Vec<(SourceName, SourceContents)> = library::CORE_LIB
+        .iter()
+        .map(|(name, contents)| ((*name).into(), (*contents).into()))
+        .collect();
+    let sources = SourceMap::new(core, None);
 
     let mut unit = compile(&store, &[], sources, RuntimeCapabilityFlags::empty());
     assert_no_errors(&unit.sources, &mut unit.errors);
@@ -431,71 +423,11 @@ pub fn core() -> CompileUnit {
 /// Panics if the standard library does not compile without errors.
 #[must_use]
 pub fn std(store: &PackageStore, capabilities: RuntimeCapabilityFlags) -> CompileUnit {
-    let sources = SourceMap::new(
-        [
-            (
-                "arrays.qs".into(),
-                include_str!("../../../library/std/arrays.qs").into(),
-            ),
-            (
-                "canon.qs".into(),
-                include_str!("../../../library/std/canon.qs").into(),
-            ),
-            (
-                "convert.qs".into(),
-                include_str!("../../../library/std/convert.qs").into(),
-            ),
-            (
-                "core.qs".into(),
-                include_str!("../../../library/std/core.qs").into(),
-            ),
-            (
-                "diagnostics.qs".into(),
-                include_str!("../../../library/std/diagnostics.qs").into(),
-            ),
-            (
-                "internal.qs".into(),
-                include_str!("../../../library/std/internal.qs").into(),
-            ),
-            (
-                "intrinsic.qs".into(),
-                include_str!("../../../library/std/intrinsic.qs").into(),
-            ),
-            (
-                "math.qs".into(),
-                include_str!("../../../library/std/math.qs").into(),
-            ),
-            (
-                "measurement.qs".into(),
-                include_str!("../../../library/std/measurement.qs").into(),
-            ),
-            (
-                "qir.qs".into(),
-                include_str!("../../../library/std/qir.qs").into(),
-            ),
-            (
-                "random.qs".into(),
-                include_str!("../../../library/std/random.qs").into(),
-            ),
-            (
-                "unstable_arithmetic.qs".into(),
-                include_str!("../../../library/std/unstable_arithmetic.qs").into(),
-            ),
-            (
-                "unstable_arithmetic_internal.qs".into(),
-                include_str!("../../../library/std/unstable_arithmetic_internal.qs").into(),
-            ),
-            (
-                "unstable_table_lookup.qs".into(),
-                include_str!("../../../library/std/unstable_table_lookup.qs").into(),
-            ),
-            (
-                "re.qs".into(),
-                include_str!("../../../library/std/re.qs").into(),
-            ),
-        ],
-        None,
-    );
+    let std: Vec<(SourceName, SourceContents)> = library::STD_LIB
+        .iter()
+        .map(|(name, contents)| ((*name).into(), (*contents).into()))
+        .collect();
+    let sources = SourceMap::new(std, None);
 
     let mut unit = compile(store, &[PackageId::CORE], sources, capabilities);
     assert_no_errors(&unit.sources, &mut unit.errors);
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index d7b5f4b1..8ba1b6f6 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -231,9 +231,10 @@ impl With<'_> {
                     if matches!(inner.kind.as_ref(), ast::ExprKind::Path(path)
                         if ConfigAttr::from_str(path.name.name.as_ref()).is_ok()))
                 {
-                    self.lowerer
-                        .errors
-                        .push(Error::InvalidAttrArgs("Full or Base", attr.arg.span));
+                    self.lowerer.errors.push(Error::InvalidAttrArgs(
+                        "Unrestricted or Base",
+                        attr.arg.span,
+                    ));
                 }
                 None
             }
diff --git a/library/src/lib.rs b/library/src/lib.rs
new file mode 100644
index 00000000..cb69dd68
--- /dev/null
+++ b/library/src/lib.rs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![warn(clippy::mod_module_files, clippy::pedantic, clippy::unwrap_used)]
+#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
+
+#[cfg(test)]
+mod tests;
+
+// The core prefix on the name is needed to disambiguate from the std
+// files of the same name. This comes in during debugging when we need
+// to load a core/std file from the library.
+pub const CORE_LIB: &[(&str, &str)] = &[
+    ("core/core.qs", include_str!("../core/core.qs")),
+    ("core/qir.qs", include_str!("../core/qir.qs")),
+];
+
+pub const STD_LIB: &[(&str, &str)] = &[
+    ("arrays.qs", include_str!("../std/arrays.qs")),
+    ("canon.qs", include_str!("../std/canon.qs")),
+    ("convert.qs", include_str!("../std/convert.qs")),
+    ("core.qs", include_str!("../std/core.qs")),
+    ("diagnostics.qs", include_str!("../std/diagnostics.qs")),
+    ("internal.qs", include_str!("../std/internal.qs")),
+    ("intrinsic.qs", include_str!("../std/intrinsic.qs")),
+    ("math.qs", include_str!("../std/math.qs")),
+    ("measurement.qs", include_str!("../std/measurement.qs")),
+    ("qir.qs", include_str!("../std/qir.qs")),
+    ("random.qs", include_str!("../std/random.qs")),
+    ("re.qs", include_str!("../std/re.qs")),
+    (
+        "unstable_arithmetic.qs",
+        include_str!("../std/unstable_arithmetic.qs"),
+    ),
+    (
+        "unstable_arithmetic_internal.qs",
+        include_str!("../std/unstable_arithmetic_internal.qs"),
+    ),
+    (
+        "unstable_table_lookup.qs",
+        include_str!("../std/unstable_table_lookup.qs"),
+    ),
+];
diff --git a/npm/src/ux/ticks.ts b/npm/src/ux/ticks.ts
new file mode 100644
index 00000000..495b832a
--- /dev/null
+++ b/npm/src/ux/ticks.ts
@@ -0,0 +1,164 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export type Tick = {
+  value: number;
+  label: string;
+};
+
+// Should be redesigned in case of localization
+type TickTimeDefinition = { tick: Tick; plural: string };
+
+export function CreateIntegerTicks(min: number, max: number): Tick[] {
+  if (max < min || min <= 0) {
+    return [];
+  }
+
+  const numberFormat = new Intl.NumberFormat();
+
+  const l = Math.round(10 ** Math.floor(Math.log10(min)));
+  const r = Math.round(10 ** Math.ceil(Math.log10(max)));
+
+  const result: Tick[] = [];
+
+  for (let i = l; i <= r; i *= 10) {
+    if (i >= min && i <= max) {
+      result.push({ value: i, label: numberFormat.format(i) });
+    }
+  }
+  if (result.length == 0) {
+    let step = l;
+    while (step >= 1) {
+      let i = Math.ceil(min / step) * step;
+      while (i <= max) {
+        result.push({ value: i, label: numberFormat.format(i) });
+        i += step;
+      }
+      if (result.length > 0) {
+        break;
+      }
+      step /= 10;
+    }
+  }
+
+  return result;
+}
+
+const predefinedTimeTicks: TickTimeDefinition[] = [
+  { tick: { value: 1, label: "1 nanosecond" }, plural: "nanoseconds" },
+  { tick: { value: 1e3, label: "1 microsecond" }, plural: "microseconds" },
+  { tick: { value: 1e6, label: "1 millisecond" }, plural: "milliseconds" },
+  { tick: { value: 1e9, label: "1 second" }, plural: "seconds" },
+  { tick: { value: 6e10, label: "1 minute" }, plural: "minutes" },
+  { tick: { value: 3.6e12, label: "1 hour" }, plural: "hours" },
+  { tick: { value: 8.64e13, label: "1 day" }, plural: "days" },
+  { tick: { value: 6.048e14, label: "1 week" }, plural: "weeks" },
+  { tick: { value: 2.592e15, label: "1 month" }, plural: "months" },
+  { tick: { value: 3.1536e16, label: "1 year" }, plural: "years" },
+  { tick: { value: 3.1536e17, label: "1 decade" }, plural: "decades" },
+  { tick: { value: 3.1536e18, label: "1 century" }, plural: "centuries" },
+];
+
+export function CreateTimeTicks(min: number, max: number): Tick[] {
+  if (max < min || min <= 0) {
+    return [];
+  }
+
+  let l = 0;
+  while (
+    l < predefinedTimeTicks.length &&
+    predefinedTimeTicks[l].tick.value <= min
+  ) {
+    l++;
+  }
+
+  let r = l;
+  if (l > 0) {
+    l--;
+  }
+
+  if (r >= predefinedTimeTicks.length) {
+    r = predefinedTimeTicks.length - 1;
+  }
+
+  while (
+    r < predefinedTimeTicks.length - 1 &&
+    predefinedTimeTicks[r].tick.value <= max
+  ) {
+    r++;
+  }
+
+  const result: Tick[] = [];
+
+  for (let i = l; i <= r; i++) {
+    if (
+      predefinedTimeTicks[i].tick.value >= min &&
+      predefinedTimeTicks[i].tick.value <= max
+    ) {
+      result.push(predefinedTimeTicks[i].tick);
+    }
+  }
+
+  if (result.length == 0) {
+    if (l < predefinedTimeTicks.length - 1) {
+      let coeff =
+        10 **
+        Math.floor(
+          Math.log10(
+            predefinedTimeTicks[l + 1].tick.value /
+              predefinedTimeTicks[l].tick.value,
+          ),
+        );
+      do {
+        let i = 1;
+        let val = 0;
+        do {
+          val = predefinedTimeTicks[l].tick.value * i * coeff;
+          if (val >= min && val <= max) {
+            result.push({
+              value: val,
+              label:
+                (i * coeff).toString() + " " + predefinedTimeTicks[l].plural,
+            });
+          }
+          i++;
+        } while (
+          // just a single tick in case of 1, 10 or 100
+          (i > 2 || result.length < 1) &&
+          val < predefinedTimeTicks[l + 1].tick.value
+        );
+        coeff /= 10;
+      } while (result.length < 1 && coeff >= 1);
+    } else {
+      // > century
+      let l = predefinedTimeTicks.length - 1;
+      do {
+        let coeff =
+          10 ** Math.floor(Math.log10(max / predefinedTimeTicks[l].tick.value));
+        do {
+          let i = 1;
+          let val = 0;
+          do {
+            val = predefinedTimeTicks[l].tick.value * i * coeff;
+            if (val >= min && val <= max) {
+              result.push({
+                value: val,
+                label:
+                  (i * coeff).toString() + " " + predefinedTimeTicks[l].plural,
+              });
+            }
+            i++;
+          } while (
+            // just a single tick in case of 1, 10 or 100
+            (i > 2 || result.length < 1) &&
+            val < max
+          );
+          coeff /= 10;
+        } while (result.length < 1 && coeff >= 1);
+        l--;
+      } while (result.length < 1 && l >= 0);
+    }
+  }
+
+  return result;
+}
diff --git a/npm/ux/colormap.ts b/npm/ux/colormap.ts
new file mode 100644
index 00000000..fef64077
--- /dev/null
+++ b/npm/ux/colormap.ts
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export function ColorMap(colorsCount: number): string[] {
+  const colors: string[] = [];
+  const saturationIncrement = 1.4142135623730951; // square root of 2
+  const hueIncrement = 0.618033988749895; // golden ratio
+  const lightnessIncrement = 2.718281828459045; // euler's number
+
+  for (let i = 0; i < colorsCount; i++) {
+    const hue = (i * hueIncrement) % 1;
+    const saturation = (1 - ((i * saturationIncrement) % 1)) * 0.5 + 0.5;
+    const lightness = ((i * lightnessIncrement) % 1) * 0.3 + 0.35;
+    const rgbColor = hslToRgb(hue, saturation, lightness);
+    const hexColor = rgbToHex(rgbColor[0], rgbColor[1], rgbColor[2]);
+    colors.push(hexColor);
+  }
+  return colors;
+}
+
+function hslToRgb(h: number, s: number, l: number): [number, number, number] {
+  let r, g, b;
+
+  if (s === 0) {
+    r = g = b = l; // Grayscale
+  } else {
+    const hue2rgb = (p: number, q: number, t: number) => {
+      if (t < 0) t += 1;
+      if (t > 1) t -= 1;
+      if (t < 1 / 6) return p + (q - p) * 6 * t;
+      if (t < 1 / 2) return q;
+      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+      return p;
+    };
+
+    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+    const p = 2 * l - q;
+    r = hue2rgb(p, q, h + 1 / 3);
+    g = hue2rgb(p, q, h);
+    b = hue2rgb(p, q, h - 1 / 3);
+  }
+
+  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+function rgbToHex(r: number, g: number, b: number): string {
+  return `#${((1 << 24) | (r << 16) | (g << 8) | b)
+    .toString(16)
+    .slice(1)
+    .toUpperCase()}`;
+}
diff --git a/npm/ux/data.ts b/npm/ux/data.ts
new file mode 100644
index 00000000..c6b9c9f6
--- /dev/null
+++ b/npm/ux/data.ts
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export type ReData = {
+  status: string;
+  jobParams: any;
+  physicalCounts: any;
+  physicalCountsFormatted: any;
+  logicalQubit: any;
+  tfactory: any;
+  errorBudget: any;
+  logicalCounts: any;
+  new: boolean;
+  frontierEntries: FrontierEntry[];
+};
+
+export type SingleEstimateResult = {
+  status: string;
+  jobParams: any;
+  physicalCounts: any;
+  physicalCountsFormatted: any;
+  logicalQubit: any;
+  tfactory: any;
+  errorBudget: any;
+  logicalCounts: any;
+  new: boolean;
+};
+
+export type FrontierEntry = {
+  logicalQubit: any;
+  tfactory: any;
+  errorBudget: any;
+  physicalCounts: any;
+  physicalCountsFormatted: any;
+};
+
+export function CreateSingleEstimateResult(
+  input: ReData,
+  frontierEntryIndex?: number,
+): SingleEstimateResult {
+  if (
+    frontierEntryIndex == undefined ||
+    input.frontierEntries == null ||
+    input.frontierEntries.length === 0
+  ) {
+    return input;
+  } else {
+    if (
+      frontierEntryIndex < 0 ||
+      frontierEntryIndex >= input.frontierEntries.length
+    ) {
+      frontierEntryIndex = 0;
+    }
+
+    const entry = input.frontierEntries[frontierEntryIndex];
+    return {
+      status: input.status,
+      jobParams: input.jobParams,
+      physicalCounts: entry.physicalCounts,
+      physicalCountsFormatted: entry.physicalCountsFormatted,
+      logicalQubit: entry.logicalQubit,
+      tfactory: entry.tfactory,
+      errorBudget: entry.errorBudget,
+      logicalCounts: input.logicalCounts,
+      new: input.new,
+    };
+  }
+}
diff --git a/npm/ux/estimatesOverview.tsx b/npm/ux/estimatesOverview.tsx
new file mode 100644
index 00000000..6a0069e2
--- /dev/null
+++ b/npm/ux/estimatesOverview.tsx
@@ -0,0 +1,227 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// A component including the results table and the scatter chart together.
+// The results table is also a legend for the scatter chart.
+
+import { ColorMap } from "./colormap.js";
+import {
+  CreateSingleEstimateResult,
+  FrontierEntry,
+  ReData,
+  SingleEstimateResult,
+} from "./data.js";
+import { ResultsTable, Row } from "./resultsTable.js";
+import {
+  Axis,
+  HideTooltip,
+  PlotItem,
+  ScatterChart,
+  ScatterSeries,
+} from "./scatterChart.js";
+
+const columnNames = [
+  "Run name",
+  "Estimate type",
+  "Qubit type",
+  "QEC scheme",
+  "Error budget",
+  "Logical qubits",
+  "Logical depth",
+  "Code distance",
+  "T states",
+  "T factories",
+  "T factory fraction",
+  "Runtime",
+  "rQOPS",
+  "Physical qubits",
+];
+
+const initialColumns = [0, 1, 2, 3, 4, 10, 11, 12];
+
+const xAxis: Axis = {
+  isTime: true,
+  label: "Runtime",
+};
+
+const yAxis: Axis = {
+  isTime: false,
+  label: "Physical Qubits",
+};
+
+function reDataToRow(input: ReData, color: string): Row {
+  const data = CreateSingleEstimateResult(input, 0);
+  const estimateType =
+    input.frontierEntries == null
+      ? "Single"
+      : "Frontier (" + input.frontierEntries.length + "  items)";
+
+  return {
+    cells: [
+      data.jobParams.runName,
+      estimateType,
+      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",
+    ],
+    color: color,
+  };
+}
+
+function frontierEntryToPlotEntry(entry: FrontierEntry): PlotItem {
+  return {
+    x: entry.physicalCounts.runtime,
+    y: entry.physicalCounts.physicalQubits,
+    label:
+      entry.physicalCountsFormatted.runtime +
+      " " +
+      entry.physicalCountsFormatted.physicalQubits,
+  };
+}
+
+function reDataToRowScatter(data: ReData, color: string): ScatterSeries {
+  if (data.frontierEntries == null || data.frontierEntries.length === 0) {
+    return {
+      color: color,
+      items: [
+        {
+          x: data.physicalCounts.runtime,
+          y: data.physicalCounts.physicalQubits,
+          label:
+            data.physicalCountsFormatted.runtime +
+            " " +
+            data.physicalCountsFormatted.physicalQubits,
+        },
+      ],
+    };
+  }
+
+  return {
+    color: color,
+    items: data.frontierEntries.map(frontierEntryToPlotEntry),
+  };
+}
+
+export function EstimatesOverview(props: {
+  estimatesData: ReData[];
+  colors: string[] | null;
+  runNames: string[] | null;
+  isSimplifiedView: boolean;
+  onRowDeleted: (rowId: string) => void;
+  setEstimate: (estimate: SingleEstimateResult | null) => void;
+}) {
+  props.estimatesData.forEach((item, idx) => {
+    if (
+      props.runNames != null &&
+      props.runNames.length == props.estimatesData.length
+    ) {
+      item.jobParams.runName = props.runNames[idx];
+    } else {
+      if (item.jobParams.runName == null) {
+        // Start indexing with 0 to match with the original object indexing.
+        item.jobParams.runName = `(${idx})`;
+      }
+    }
+  });
+
+  function onPointSelected(seriesIndex: number, pointIndex: number): void {
+    const data = props.estimatesData[seriesIndex];
+    props.setEstimate(CreateSingleEstimateResult(data, pointIndex));
+  }
+
+  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) {
+      props.setEstimate(null);
+    } else {
+      const estimateFound =
+        props.estimatesData.find((data) => data.jobParams.runName === rowId) ||
+        null;
+      if (estimateFound == null) {
+        props.setEstimate(null);
+      } else {
+        props.setEstimate(CreateSingleEstimateResult(estimateFound, 0));
+      }
+    }
+
+    HideTooltip();
+  }
+
+  const colormap =
+    props.colors != null && props.colors.length == props.estimatesData.length
+      ? props.colors
+      : ColorMap(props.estimatesData.length);
+
+  if (props.isSimplifiedView) {
+    return (
+      <>
+        <ResultsTable
+          columnNames={columnNames}
+          rows={props.estimatesData.map((dataItem, index) =>
+            reDataToRow(dataItem, colormap[index]),
+          )}
+          initialColumns={initialColumns}
+          ensureSelected={true}
+          onRowSelected={onRowSelected}
+          onRowDeleted={props.onRowDeleted}
+        />
+        <ScatterChart
+          xAxis={xAxis}
+          yAxis={yAxis}
+          data={props.estimatesData.map((dataItem, index) =>
+            reDataToRowScatter(dataItem, colormap[index]),
+          )}
+          onPointSelected={onPointSelected}
+        />
+      </>
+    );
+  }
+
+  return (
+    <>
+      <details open>
+        <summary style="font-size: 1.5em; font-weight: bold; margin: 24px 8px;">
+          Results
+        </summary>
+        <ResultsTable
+          columnNames={columnNames}
+          rows={props.estimatesData.map((dataItem, index) =>
+            reDataToRow(dataItem, colormap[index]),
+          )}
+          initialColumns={initialColumns}
+          ensureSelected={true}
+          onRowSelected={onRowSelected}
+          onRowDeleted={props.onRowDeleted}
+        />
+      </details>
+      <details open>
+        <summary style="font-size: 1.5em; font-weight: bold; margin: 24px 8px;">
+          Qubit-time diagram
+        </summary>
+        <ScatterChart
+          xAxis={xAxis}
+          yAxis={yAxis}
+          data={props.estimatesData.map((dataItem, index) =>
+            reDataToRowScatter(dataItem, colormap[index]),
+          )}
+          onPointSelected={onPointSelected}
+        />
+      </details>
+    </>
+  );
+}
diff --git a/npm/ux/generate_report_code.py b/npm/ux/generate_report_code.py
new file mode 100644
index 00000000..fa4abbef
--- /dev/null
+++ b/npm/ux/generate_report_code.py
@@ -0,0 +1,243 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# This script generates the code for the report page from the output_data.md file.
+# To run, simply execute `python generate_report_code.py` in the npm/ux folder.
+# copy the output and paste it into the report.tsx file.
+# It provides a code for the CreateReport function.
+
+import re
+
+parse = False
+
+title = ""
+always_visible = True
+first_entry = True
+entries = []
+
+label = ""
+path = ""
+value = ""
+description = ""
+explanation = ""
+
+assumptions = []
+
+ignore_paths = [
+    "floquet_code",
+    "surface_code",
+    "qubit_gate_ns_e3",
+    "qubit_gate_ns_e4",
+    "qubit_gate_us_e3",
+    "qubit_gate_us_e4",
+    "qubit_maj_ns_e4",
+    "qubit_maj_ns_e6",
+]
+
+path_map = {
+    "errorBudget/rotations": "result.errorBudget.rotations",
+    "jobParams/qecScheme/crossingPrefactor": "result.jobParams.qecScheme.crossingPrefactor",
+    "jobParams/qecScheme/errorCorrectionThreshold": "result.jobParams.qecScheme.errorCorrectionThreshold",
+    "jobParams/qecScheme/logicalCycleTime": "result.jobParams.qecScheme.logicalCycleTime",
+    "jobParams/qecScheme/physicalQubitsPerLogicalQubit": "result.jobParams.qecScheme.physicalQubitsPerLogicalQubit",
+    "jobParams/qubitParams/tGateErrorRate": "result.jobParams.qubitParams.tGateErrorRate",
+    "logicalCounts/ccixCount": "numberFormat.format(result.logicalCounts.ccixCount)",
+    "logicalCounts/cczCount": "numberFormat.format(result.logicalCounts.cczCount)",
+    "logicalCounts/measurementCount": "numberFormat.format(result.logicalCounts.measurementCount)",
+    "logicalCounts/numQubits": "numberFormat.format(result.logicalCounts.numQubits)",
+    "logicalCounts/rotationCount": "numberFormat.format(result.logicalCounts.rotationCount)",
+    "logicalCounts/rotationDepth": "numberFormat.format(result.logicalCounts.rotationDepth)",
+    "logicalCounts/tCount": "numberFormat.format(result.logicalCounts.tCount)",
+    "logicalQubit/codeDistance": "result.logicalQubit.codeDistance",
+    "logicalQubit/logicalCyclesPerSecond": "numberFormatF64.format(result.physicalCounts.breakdown.clockFrequency)",
+    "logicalQubit/logicalCycleTime": "numberFormat.format(result.logicalQubit.logicalCycleTime)",
+    "logicalQubit/physicalQubits": "numberFormat.format(result.logicalQubit.physicalQubits)",
+    "physicalCounts/breakdown/algorithmicLogicalDepth": "numberFormat.format(result.physicalCountsFormatted.algorithmicLogicalDepth)",
+    "physicalCounts/breakdown/algorithmicLogicalQubits": "numberFormat.format(result.physicalCounts.breakdown.algorithmicLogicalQubits)",
+    "physicalCounts/breakdown/cliffordErrorRate": "result.physicalCounts.breakdown.cliffordErrorRate",
+    "physicalCounts/breakdown/logicalDepth": "numberFormat.format(result.physicalCounts.breakdown.logicalDepth)",
+    "physicalCounts/breakdown/physicalQubitsForAlgorithm": "numberFormat.format(result.physicalCounts.breakdown.physicalQubitsForAlgorithm)",
+    "physicalCounts/breakdown/physicalQubitsForTfactories": "numberFormat.format(result.physicalCounts.breakdown.physicalQubitsForTfactories)",
+    "physicalCounts/breakdown/numTfactories": "numberFormat.format(result.physicalCounts.breakdown.numTfactories)",
+    "physicalCounts/breakdown/numTfactoryRuns": "numberFormat.format(result.physicalCounts.breakdown.numTfactoryRuns)",
+    "physicalCounts/breakdown/numTstates": "numberFormat.format(result.physicalCounts.breakdown.numTstates)",
+    "physicalCounts/breakdown/requiredLogicalQubitErrorRate": "result.physicalCounts.breakdown.requiredLogicalQubitErrorRate",
+    "physicalCounts/physicalQubits": "numberFormat.format(result.physicalCounts.physicalQubits)",
+    "physicalCounts/runtime": "numberFormat.format(result.physicalCounts.runtime)",
+    "physicalCountsFormatted/clockFrequency": "result.physicalCountsFormatted.clockFrequency",
+    "physicalCountsFormatted/errorBudget": "result.physicalCountsFormatted.errorBudget",
+    "physicalCountsFormatted/errorBudgetLogical": "result.physicalCountsFormatted.errorBudgetLogical",
+    "physicalCountsFormatted/errorBudgetRotations": "result.physicalCountsFormatted.errorBudgetRotations",
+    "physicalCountsFormatted/errorBudgetTstates": "result.physicalCountsFormatted.errorBudgetTstates",
+    "physicalCountsFormatted/logicalCycleTime": "result.physicalCountsFormatted.logicalCycleTime",
+    "physicalCountsFormatted/numTsPerRotation": "result.physicalCountsFormatted.numTsPerRotation",
+    "physicalCountsFormatted/requiredLogicalQubitErrorRate": "result.physicalCountsFormatted.requiredLogicalQubitErrorRate",
+    "physicalCountsFormatted/requiredLogicalTstateErrorRate": "result.physicalCountsFormatted.requiredLogicalTstateErrorRate",
+    "physicalCountsFormatted/runtime": "result.physicalCountsFormatted.runtime",
+    "physicalCountsFormatted/tfactoryRuntime": "result.physicalCountsFormatted.tfactoryRuntime",
+    "physicalCountsFormatted/tstateLogicalErrorRate": "result.physicalCountsFormatted.tstateLogicalErrorRate",
+    "tfactory/physicalQubits": "numberFormat.format(result.tfactory == null ? 0 : result.tfactory.physicalQubits)",
+    "tfactory/numInputTstates": "numberFormat.format(result.tfactory == null ? 0 : result.tfactory.numInputTstates)",
+    "tfactory/numTstates": "numberFormat.format(result.tfactory == null ? 0 : result.tfactory.numTstates)",
+    "tfactory/runtime": "numberFormat.format(result.tfactory == null ? 0 : result.tfactory.runtime)",
+}
+
+print()
+print()
+print(
+    "// THIS CODE HAS BEEN AUTOMATICALLY GENERATED WITH generate_report_code.py from output_data.md"
+)
+print("export function CreateReport(result: ReData): ReportData {")
+print("    const groups = [] as ReportGroup[];")
+print("    let entries = [] as ReportEntry[];")
+print("    const numberFormat = new Intl.NumberFormat();")
+print(
+    "    const numberFormatF64 = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2,});"
+)
+print()
+
+
+def add_group():
+    global always_visible, entries, title
+
+    if len(entries) != 0:
+        if title == "T factory parameters":
+            print("    if (result.tfactory != null) {")
+
+        always_visible_str = "true" if always_visible else "false"
+        print("    entries = [];")
+        for path, label, description, explanation in entries:
+            if path in [
+                "jobParams/qubitParams/oneQubitGateTime",
+                "jobParams/qubitParams/twoQubitGateTime",
+                "jobParams/qubitParams/oneQubitGateErrorRate",
+                "jobParams/qubitParams/twoQubitGateErrorRate",
+            ]:
+                print(
+                    '    if (result.jobParams.qubitParams.instructionSet == "GateBased") {'
+                )
+                print(
+                    f'        entries.push({{path: "{path}", label: "{label}", description: {description}, explanation: {explanation}}});'
+                )
+                print("        }")
+            elif path in [
+                "jobParams/qubitParams/twoQubitJointMeasurementTime",
+                "jobParams/qubitParams/twoQubitJointMeasurementErrorRate",
+            ]:
+                print(
+                    '    if (result.jobParams.qubitParams.instructionSet == "Majorana") {'
+                )
+                print(
+                    f'        entries.push({{path: "{path}", label: "{label}", description: {description}, explanation: {explanation}}});'
+                )
+                print("    }")
+            else:
+                print(
+                    f'        entries.push({{path: "{path}", label: "{label}", description: {description}, explanation: {explanation}}});'
+                )
+        print(
+            f'    groups.push({{ title: "{title}", alwaysVisible: {always_visible_str}, entries: entries }});'
+        )
+        print()
+
+        if title == "T factory parameters":
+            print("    }")
+
+        always_visible = False
+        entries.clear()
+
+
+def create_fmt_string(string):
+    args = []
+
+    # Find in-text `paths`
+    pos = string.find("`")
+    while pos != -1:
+        pos2 = string.find("`", pos + 1)
+        path = string[pos + 1 : pos2]
+
+        if path in ignore_paths:
+            pos = string.find("`", pos2 + 1)
+        else:
+            string = string[:pos] + "${" + path_map[path] + "}" + string[pos2 + 1 :]
+            pos = string.find("`", pos + 1)
+
+    # Find in-math \mathtt{paths}
+    pos = string.find("\\mathtt{")
+    while pos != -1:
+        pos2 = string.find("}", pos + 1)
+        path = string[pos + 8 : pos2]
+
+        string = string[:pos] + "${" + path_map[path] + "}" + string[pos2 + 1 :]
+        pos = string.find("\\mathtt{", pos + 1)
+
+    if len(args) != 0:
+        args_list = ", ".join(args)
+
+        cur = 0
+        while cur < len(string):
+            if string[cur] == "{" and string[cur + 1] != "}":
+                string = string[:cur] + "{" + string[cur:]
+                cur += 2
+            elif string[cur] == "}" and string[cur - 1] != "{":
+                string = string[:cur] + "}" + string[cur:]
+                cur += 2
+            else:
+                cur += 1
+
+    string = string.replace("\\", "\\\\")
+    string = string.replace("`", "\\`")
+    return f"`{string}`"
+
+
+with open("output_data.md", "r") as f:
+    for line in f.readlines():
+        line = line.strip()
+
+        if line == "":
+            continue
+
+        if not parse:
+            if line.startswith("## "):
+                parse = True
+            else:
+                continue
+
+        if line.startswith("## "):
+            # Finish previous group?
+            if len(entries) != 0:
+                add_group()
+
+            title = line[3:].strip()
+        elif line.startswith("### "):
+            label = line[4:].strip()
+        elif line.startswith("[//]: #"):
+            path = line[9:-1]
+        elif line.startswith("_"):
+            description = line[1:-1]
+        elif line.startswith("-"):
+            assumptions.append(line[2:])
+        else:
+            explanation = line
+
+            entries.append(
+                (
+                    path,
+                    label,
+                    create_fmt_string(description),
+                    create_fmt_string(explanation),
+                )
+            )
+
+# Add assumptions
+assert title == "Assumptions"
+print("    const assumptions = [")
+for assumption in assumptions:
+    print(f"        '{assumption}',")
+print("    ];")
+print()
+print("    return { groups: groups, assumptions: assumptions };")
+print("}")
+print("// END OF AUTOMATICALLY GENERATED CODE")
+print()
+print()
diff --git a/npm/ux/index.ts b/npm/ux/index.ts
index e877c0db..6506ea2f 100644
--- a/npm/ux/index.ts
+++ b/npm/ux/index.ts
@@ -5,7 +5,9 @@
 // to a CSS file adjacent to the JS bundle and with the same name.
 import "./qsharp-ux.css";
 
+export { CreateSingleEstimateResult, type ReData } from "./data.js";
 export { Histogram } from "./histogram.js";
-export { ReTable, type ReData } from "./reTable.js";
+export { ReTable } from "./reTable.js";
 export { SpaceChart } from "./spaceChart.js";
-export { ResultsTable } from "./resultsTable.js";
+export { ScatterChart } from "./scatterChart.js";
+export { EstimatesOverview } from "./estimatesOverview.js";
diff --git a/npm/ux/output_data.md b/npm/ux/output_data.md
new file mode 100644
index 00000000..5f9adf4f
--- /dev/null
+++ b/npm/ux/output_data.md
@@ -0,0 +1,537 @@
+---
+geometry:
+  - left=1cm
+  - right=1cm
+  - top=1cm
+  - bottom=2cm
+classoption:
+  - 10pt
+...
+
+# Output data
+
+In this document, we describe all output parameters which are printed by the Azure Quantum Resource Estimator. Second-level headings contain one group in the output that consists of a header and a table with resource estimation data. Each third-level heading corresponds to an entry, which is preceded by a short description text in italic and a long description text. The short description text will be always visible in the third column next to the value, whereas the long description text will be visible as a tooltip when hovering over the corresponding row in the table. The description texts may contain some variable names such as `variable` or $\mathtt{variable}$ in formulas. These will be substituted by the actual computed values extracted from the resource estimates.
+
+## Physical resource estimates
+
+### Runtime
+
+[//]: # "physicalCountsFormatted/runtime"
+
+_Total runtime_
+
+This is a runtime estimate for the execution time of the algorithm. In general, the execution time corresponds to the duration of one logical cycle (`logicalQubit/logicalCycleTime` nanosecs) multiplied by the `physicalCounts/breakdown/algorithmicLogicalDepth` logical cycles to run the algorithm. If however the duration of a single T factory (here: `tfactory/runtime` nanosecs) is larger than the algorithm runtime, we extend the number of logical cycles artificially in order to exceed the runtime of a single T factory.
+
+### rQOPS
+
+[//]: # "physicalCountsFormatted/rqops"
+
+_Reliable quantum operations per second_
+
+The value is computed as the number of logical qubits after layout (`physicalCounts/breakdown/algorithmicLogicalQubits`) (with a logical error rate of `physicalCountsFormatted/requiredLogicalQubitErrorRate`) multiplied by the clock frequency (`logicalQubit/logicalCyclesPerSecond`), which is the number of logical cycles per second.
+
+### Physical qubits
+
+[//]: # "physicalCountsFormatted/physicalQubits"
+
+_Number of physical qubits_
+
+This value represents the total number of physical qubits, which is the sum of `physicalCounts/breakdown/physicalQubitsForAlgorithm` physical qubits to implement the algorithm logic, and `physicalCounts/breakdown/physicalQubitsForTfactories` physical qubits to execute the T factories that are responsible to produce the T states that are consumed by the algorithm.
+
+## Resource estimates breakdown
+
+### Logical algorithmic qubits
+
+[//]: # "physicalCountsFormatted/algorithmicLogicalQubits"
+
+_Number of logical qubits for the algorithm after layout_
+
+Laying out the logical qubits in the presence of nearest-neighbor constraints requires additional logical qubits. In particular, to layout the $Q_{\rm alg} = \mathtt{logicalCounts/numQubits}$ logical qubits in the input algorithm, we require in total $2 \cdot Q_{\rm alg} + \lceil \sqrt{8 \cdot Q_{\rm alg}}\rceil + 1 = \mathtt{physicalCounts/breakdown/algorithmicLogicalQubits}$ logical qubits.
+
+### Algorithmic depth
+
+[//]: # "physicalCountsFormatted/algorithmicLogicalDepth"
+
+_Number of logical cycles for the algorithm_
+
+To execute the algorithm using _Parallel Synthesis Sequential Pauli Computation_ (PSSPC), operations are scheduled in terms of multi-qubit Pauli measurements, for which assume an execution time of one logical cycle. Based on the input algorithm, we require one multi-qubit measurement for the `logicalCounts/measurementCount` single-qubit measurements, the `logicalCounts/rotationCount` arbitrary single-qubit rotations, and the `logicalCounts/tCount` T gates, three multi-qubit measurements for each of the `logicalCounts/cczCount` CCZ and `logicalCounts/ccixCount` CCiX gates in the input program, as well as `physicalCountsFormatted/numTsPerRotation` multi-qubit measurements for each of the `logicalCounts/rotationDepth` non-Clifford layers in which there is at least one single-qubit rotation with an arbitrary angle rotation.
+
+### Logical depth
+
+[//]: # "physicalCountsFormatted/logicalDepth"
+
+_Number of logical cycles performed_
+
+This number is usually equal to the logical depth of the algorithm, which is `physicalCounts/breakdown/algorithmicLogicalDepth`. However, in the case in which a single T factory is slower than the execution time of the algorithm, we adjust the logical cycle depth to exceed the T factory's execution time.
+
+### Clock frequency
+
+[//]: # "physicalCountsFormatted/clockFrequency"
+
+_Number of logical cycles per second_
+
+This is the number of logical cycles that can be performed within one second. The logical cycle time is `physicalCountsFormatted/logicalCycleTime`.
+
+### Number of T states
+
+[//]: # "physicalCountsFormatted/numTstates"
+
+_Number of T states consumed by the algorithm_
+
+To execute the algorithm, we require one T state for each of the `logicalCounts/tCount` T gates, four T states for each of the `logicalCounts/cczCount` CCZ and `logicalCounts/ccixCount` CCiX gates, as well as `physicalCountsFormatted/numTsPerRotation` for each of the `logicalCounts/rotationCount` single-qubit rotation gates with arbitrary angle rotation.
+
+### Number of T factories
+
+[//]: # "physicalCountsFormatted/numTfactories"
+
+_Number of T factories capable of producing the demanded `physicalCounts/breakdown/numTstates` T states during the algorithm's runtime_
+
+The total number of T factories `physicalCounts/breakdown/numTfactories` that are executed in parallel is computed as $\left\lceil\dfrac{\text{T states}\cdot\text{T factory duration}}{\text{T states per T factory}\cdot\text{algorithm runtime}}\right\rceil = \left\lceil\dfrac{\mathtt{physicalCounts/breakdown/numTstates} \cdot \mathtt{tfactory/runtime}\;\text{ns}}{\mathtt{tfactory/numTstates} \cdot \mathtt{physicalCounts/runtime}\;\text{ns}}\right\rceil$
+
+### Number of T factory invocations
+
+[//]: # "physicalCountsFormatted/numTfactoryRuns"
+
+_Number of times all T factories are invoked_
+
+In order to prepare the `physicalCounts/breakdown/numTstates` T states, the `physicalCounts/breakdown/numTfactories` copies of the T factory are repeatedly invoked `physicalCounts/breakdown/numTfactoryRuns` times.
+
+### Physical algorithmic qubits
+
+[//]: # "physicalCountsFormatted/physicalQubitsForAlgorithm"
+
+_Number of physical qubits for the algorithm after layout_
+
+The `physicalCounts/breakdown/physicalQubitsForAlgorithm` are the product of the `physicalCounts/breakdown/algorithmicLogicalQubits` logical qubits after layout and the `logicalQubit/physicalQubits` physical qubits that encode a single logical qubit.
+
+### Physical T factory qubits
+
+[//]: # "physicalCountsFormatted/physicalQubitsForTfactories"
+
+_Number of physical qubits for the T factories_
+
+Each T factory requires `tfactory/physicalQubits` physical qubits and we run `physicalCounts/breakdown/numTfactories` in parallel, therefore we need $\mathtt{physicalCounts/breakdown/physicalQubitsForTfactories} = \mathtt{tfactory/physicalQubits} \cdot \mathtt{physicalCounts/breakdown/numTfactories}$ qubits.
+
+### Required logical qubit error rate
+
+[//]: # "physicalCountsFormatted/requiredLogicalQubitErrorRate"
+
+_The minimum logical qubit error rate required to run the algorithm within the error budget_
+
+The minimum logical qubit error rate is obtained by dividing the logical error probability `physicalCountsFormatted/errorBudgetLogical` by the product of `physicalCounts/breakdown/algorithmicLogicalQubits` logical qubits and the total cycle count `physicalCounts/breakdown/logicalDepth`.
+
+### Required logical T state error rate
+
+[//]: # "physicalCountsFormatted/requiredLogicalTstateErrorRate"
+
+_The minimum T state error rate required for distilled T states_
+
+The minimum T state error rate is obtained by dividing the T distillation error probability `physicalCountsFormatted/errorBudgetTstates` by the total number of T states `physicalCounts/breakdown/numTstates`.
+
+### Number of T states per rotation
+
+[//]: # "physicalCountsFormatted/numTsPerRotation"
+
+_Number of T states to implement a rotation with an arbitrary angle_
+
+The number of T states to implement a rotation with an arbitrary angle is $\lceil 0.53 \log_2(\mathtt{logicalCounts/rotationCount} / \mathtt{errorBudget/rotations}) + 5.3\rceil$ [[arXiv:2203.10064](https://arxiv.org/abs/2203.10064)]. For simplicity, we use this formula for all single-qubit arbitrary angle rotations, and do not distinguish between best, worst, and average cases.
+
+## Logical qubit parameters
+
+### QEC scheme
+
+[//]: # "jobParams/qecScheme/name"
+
+_Name of QEC scheme_
+
+You can load pre-defined QEC schemes by using the name `surface_code` or `floquet_code`. The latter only works with Majorana qubits.
+
+### Code distance
+
+[//]: # "logicalQubit/codeDistance"
+
+_Required code distance for error correction_
+
+The code distance is the smallest odd integer greater or equal to $\dfrac{2\log(\mathtt{jobParams/qecScheme/crossingPrefactor} / \mathtt{physicalCounts/breakdown/requiredLogicalQubitErrorRate})}{\log(\mathtt{jobParams/qecScheme/errorCorrectionThreshold}/\mathtt{physicalCounts/breakdown/cliffordErrorRate})} - 1$
+
+### Physical qubits
+
+[//]: # "physicalCountsFormatted/physicalQubitsPerLogicalQubit"
+
+_Number of physical qubits per logical qubit_
+
+The number of physical qubits per logical qubit are evaluated using the formula `jobParams/qecScheme/physicalQubitsPerLogicalQubit` that can be user-specified.
+
+### Logical cycle time
+
+[//]: # "physicalCountsFormatted/logicalCycleTime"
+
+_Duration of a logical cycle in nanoseconds_
+
+The runtime of one logical cycle in nanoseconds is evaluated using the formula `jobParams/qecScheme/logicalCycleTime` that can be user-specified.
+
+### Logical qubit error rate
+
+[//]: # "physicalCountsFormatted/logicalErrorRate"
+
+_Logical qubit error rate_
+
+The logical qubit error rate is computed as $\mathtt{jobParams/qecScheme/crossingPrefactor} \cdot \left(\dfrac{\mathtt{physicalCounts/breakdown/cliffordErrorRate}}{\mathtt{jobParams/qecScheme/errorCorrectionThreshold}}\right)^\frac{\mathtt{logicalQubit/codeDistance} + 1}{2}$
+
+### Crossing prefactor
+
+[//]: # "jobParams/qecScheme/crossingPrefactor"
+
+_Crossing prefactor used in QEC scheme_
+
+The crossing prefactor is usually extracted numerically from simulations when fitting an exponential curve to model the relationship between logical and physical error rate.
+
+### Error correction threshold
+
+[//]: # "jobParams/qecScheme/errorCorrectionThreshold"
+
+_Error correction threshold used in QEC scheme_
+
+The error correction threshold is the physical error rate below which the error rate of the logical qubit is less than the error rate of the physical qubit that constitute it. This value is usually extracted numerically from simulations of the logical error rate.
+
+### Logical cycle time formula
+
+[//]: # "jobParams/qecScheme/logicalCycleTime"
+
+_QEC scheme formula used to compute logical cycle time_
+
+This is the formula that is used to compute the logical cycle time `logicalQubit/logicalCycleTime` ns.
+
+### Physical qubits formula
+
+[//]: # "jobParams/qecScheme/physicalQubitsPerLogicalQubit"
+
+_QEC scheme formula used to compute number of physical qubits per logical qubit_
+
+This is the formula that is used to compute the number of physical qubits per logical qubits `logicalQubit/physicalQubits`.
+
+## T factory parameters
+
+### Physical qubits
+
+[//]: # "physicalCountsFormatted/tfactoryPhysicalQubits"
+
+_Number of physical qubits for a single T factory_
+
+This corresponds to the maximum number of physical qubits over all rounds of T distillation units in a T factory. A round of distillation contains of multiple copies of distillation units to achieve the required success probability of producing a T state with the expected logical T state error rate.
+
+### Runtime
+
+[//]: # "physicalCountsFormatted/tfactoryRuntime"
+
+_Runtime of a single T factory_
+
+The runtime of a single T factory is the accumulated runtime of executing each round in a T factory.
+
+### Number of output T states per run
+
+[//]: # "tfactory/numTstates"
+
+_Number of output T states produced in a single run of T factory_
+
+The T factory takes as input `tfactory/numInputTstates` noisy physical T states with an error rate of `jobParams/qubitParams/tGateErrorRate` and produces `tfactory/numTstates` T states with an error rate of `physicalCountsFormatted/tstateLogicalErrorRate`.
+
+### Number of input T states per run
+
+[//]: # "physicalCountsFormatted/numInputTstates"
+
+_Number of physical input T states consumed in a single run of a T factory_
+
+This value includes the physical input T states of all copies of the distillation unit in the first round.
+
+### Distillation rounds
+
+[//]: # "tfactory/numRounds"
+
+_The number of distillation rounds_
+
+This is the number of distillation rounds. In each round one or multiple copies of some distillation unit is executed.
+
+### Distillation units per round
+
+[//]: # "physicalCountsFormatted/numUnitsPerRound"
+
+_The number of units in each round of distillation_
+
+This is the number of copies for the distillation units per round.
+
+### Distillation units
+
+[//]: # "physicalCountsFormatted/unitNamePerRound"
+
+_The types of distillation units_
+
+These are the types of distillation units that are executed in each round. The units can be either physical or logical, depending on what type of qubit they are operating. Space-efficient units require fewer qubits for the cost of longer runtime compared to Reed-Muller preparation units.
+
+### Distillation code distances
+
+[//]: # "physicalCountsFormatted/codeDistancePerRound"
+
+_The code distance in each round of distillation_
+
+This is the code distance used for the units in each round. If the code distance is 1, then the distillation unit operates on physical qubits instead of error-corrected logical qubits.
+
+### Number of physical qubits per round
+
+[//]: # "physicalCountsFormatted/physicalQubitsPerRound"
+
+_The number of physical qubits used in each round of distillation_
+
+The maximum number of physical qubits over all rounds is the number of physical qubits for the T factory, since qubits are reused by different rounds.
+
+### Runtime per round
+
+[//]: # "physicalCountsFormatted/tfactoryRuntimePerRound"
+
+_The runtime of each distillation round_
+
+The runtime of the T factory is the sum of the runtimes in all rounds.
+
+### Logical T state error rate
+
+[//]: # "physicalCountsFormatted/tstateLogicalErrorRate"
+
+_Logical T state error rate_
+
+This is the logical T state error rate achieved by the T factory which is equal or smaller than the required error rate `physicalCountsFormatted/requiredLogicalTstateErrorRate`.
+
+## Pre-layout logical resources
+
+### Logical qubits (pre-layout)
+
+[//]: # "physicalCountsFormatted/logicalCountsNumQubits"
+
+_Number of logical qubits in the input quantum program_
+
+We determine `physicalCounts/breakdown/algorithmicLogicalQubits` algorithmic logical qubits from this number by assuming to align them in a 2D grid. Auxiliary qubits are added to allow for sufficient space to execute multi-qubit Pauli measurements on all or a subset of the logical qubits.
+
+### T gates
+
+[//]: # "physicalCountsFormatted/logicalCountsTCount"
+
+_Number of T gates in the input quantum program_
+
+This includes all T gates and adjoint T gates, but not T gates used to implement rotation gates with arbitrary angle, CCZ gates, or CCiX gates.
+
+### Rotation gates
+
+[//]: # "physicalCountsFormatted/logicalCountsRotationCount"
+
+_Number of rotation gates in the input quantum program_
+
+This is the number of all rotation gates. If an angle corresponds to a Pauli, Clifford, or T gate, it is not accounted for in this number.
+
+### Rotation depth
+
+[//]: # "physicalCountsFormatted/logicalCountsRotationDepth"
+
+_Depth of rotation gates in the input quantum program_
+
+This is the number of all non-Clifford layers that include at least one single-qubit rotation gate with an arbitrary angle.
+
+### CCZ gates
+
+[//]: # "physicalCountsFormatted/logicalCountsCczCount"
+
+_Number of CCZ-gates in the input quantum program_
+
+This is the number of CCZ gates.
+
+### CCiX gates
+
+[//]: # "physicalCountsFormatted/logicalCountsCcixCount"
+
+_Number of CCiX-gates in the input quantum program_
+
+This is the number of CCiX gates, which applies $-iX$ controlled on two control qubits [[1212.5069](https://arxiv.org/abs/1212.5069)].
+
+### Measurement operations
+
+[//]: # "physicalCountsFormatted/logicalCountsMeasurementCount"
+
+_Number of single qubit measurements in the input quantum program_
+
+This is the number of single qubit measurements in Pauli basis that are used in the input program. Note that all measurements are counted, however, the measurement result is is determined randomly (with a fixed seed) to be 0 or 1 with a probability of 50%.
+
+## Assumed error budget
+
+### Total error budget
+
+[//]: # "physicalCountsFormatted/errorBudget"
+
+_Total error budget for the algorithm_
+
+The total error budget sets the overall allowed error for the algorithm, i.e., the number of times it is allowed to fail. Its value must be between 0 and 1 and the default value is 0.001, which corresponds to 0.1%, and means that the algorithm is allowed to fail once in 1000 executions. This parameter is highly application specific. For example, if one is running Shor's algorithm for factoring integers, a large value for the error budget may be tolerated as one can check that the output are indeed the prime factors of the input. On the other hand, a much smaller error budget may be needed for an algorithm solving a problem with a solution which cannot be efficiently verified. This budget $\epsilon = \epsilon_{\log} + \epsilon_{\rm dis} + \epsilon_{\rm syn}$ is uniformly distributed and applies to errors $\epsilon_{\log}$ to implement logical qubits, an error budget $\epsilon_{\rm dis}$ to produce T states through distillation, and an error budget $\epsilon_{\rm syn}$ to synthesize rotation gates with arbitrary angles. Note that for distillation and rotation synthesis, the respective error budgets $\epsilon_{\rm dis}$ and $\epsilon_{\rm syn}$ are uniformly distributed among all T states and all rotation gates, respectively. If there are no rotation gates in the input algorithm, the error budget is uniformly distributed to logical errors and T state errors.
+
+### Logical error probability
+
+[//]: # "physicalCountsFormatted/errorBudgetLogical"
+
+_Probability of at least one logical error_
+
+This is one third of the total error budget `physicalCountsFormatted/errorBudget` if the input algorithm contains rotation with gates with arbitrary angles, or one half of it, otherwise.
+
+### T distillation error probability
+
+[//]: # "physicalCountsFormatted/errorBudgetTstates"
+
+_Probability of at least one faulty T distillation_
+
+This is one third of the total error budget `physicalCountsFormatted/errorBudget` if the input algorithm contains rotation with gates with arbitrary angles, or one half of it, otherwise.
+
+### Rotation synthesis error probability
+
+[//]: # "physicalCountsFormatted/errorBudgetRotations"
+
+_Probability of at least one failed rotation synthesis_
+
+This is one third of the total error budget `physicalCountsFormatted/errorBudget`.
+
+## Physical qubit parameters
+
+_Info:_ Note that not all values are shown depending on the instruction set of the qubit parameters.
+
+### Qubit name
+
+[//]: # "jobParams/qubitParams/name"
+
+_Some descriptive name for the qubit model_
+
+You can load pre-defined qubit parameters by using the names `qubit_gate_ns_e3`, `qubit_gate_ns_e4`, `qubit_gate_us_e3`, `qubit_gate_us_e4`, `qubit_maj_ns_e4`, or `qubit_maj_ns_e6`. The names of these pre-defined qubit parameters indicate the instruction set (gate-based or Majorana), the operation speed (ns or µs regime), as well as the fidelity (e.g., e3 for $10^{-3}$ gate error rates).
+
+### Instruction set
+
+[//]: # "jobParams/qubitParams/instructionSet"
+
+_Underlying qubit technology (gate-based or Majorana)_
+
+When modeling the physical qubit abstractions, we distinguish between two different physical instruction sets that are used to operate the qubits. The physical instruction set can be either _gate-based_ or _Majorana_. A gate-based instruction set provides single-qubit measurement, single-qubit gates (incl. T gates), and two-qubit gates. A Majorana instruction set provides a physical T gate, single-qubit measurement and two-qubit joint measurement operations.
+
+### Single-qubit measurement time
+
+[//]: # "jobParams/qubitParams/oneQubitMeasurementTime"
+
+_Operation time for single-qubit measurement (t_meas) in ns_
+
+This is the operation time in nanoseconds to perform a single-qubit measurement in the Pauli basis.
+
+### Two-qubit measurement time
+
+[//]: # "jobParams/qubitParams/twoQubitJointMeasurementTime"
+
+_Operation time for two-qubit measurement in ns_
+
+This is the operation time in nanoseconds to perform a non-destructive two-qubit joint Pauli measurement.
+
+### Single-qubit gate time
+
+[//]: # "jobParams/qubitParams/oneQubitGateTime"
+
+_Operation time for single-qubit gate (t_gate) in ns_
+
+This is the operation time in nanoseconds to perform a single-qubit Clifford operation, e.g., Hadamard or Phase gates.
+
+### Two-qubit gate time
+
+[//]: # "jobParams/qubitParams/twoQubitGateTime"
+
+_Operation time for two-qubit gate in ns_
+
+This is the operation time in nanoseconds to perform a two-qubit Clifford operation, e.g., a CNOT or CZ gate.
+
+### T gate time
+
+[//]: # "jobParams/qubitParams/tGateTime"
+
+_Operation time for a T gate_
+
+This is the operation time in nanoseconds to execute a T gate.
+
+### Single-qubit measurement error rate
+
+[//]: # "jobParams/qubitParams/oneQubitMeasurementErrorRate"
+
+_Error rate for single-qubit measurement_
+
+This is the probability in which a single-qubit measurement in the Pauli basis may fail.
+
+### Two-qubit measurement error rate
+
+[//]: # "jobParams/qubitParams/twoQubitJointMeasurementErrorRate"
+
+_Error rate for two-qubit measurement_
+
+This is the probability in which a non-destructive two-qubit joint Pauli measurement may fail.
+
+### Single-qubit error rate
+
+[//]: # "jobParams/qubitParams/oneQubitGateErrorRate"
+
+_Error rate for single-qubit Clifford gate (p)_
+
+This is the probability in which a single-qubit Clifford operation, e.g., Hadamard or Phase gates, may fail.
+
+### Two-qubit error rate
+
+[//]: # "jobParams/qubitParams/twoQubitGateErrorRate"
+
+_Error rate for two-qubit Clifford gate_
+
+This is the probability in which a two-qubit Clifford operation, e.g., CNOT or CZ gates, may fail.
+
+### T gate error rate
+
+[//]: # "jobParams/qubitParams/tGateErrorRate"
+
+_Error rate to prepare single-qubit T state or apply a T gate (p_T)_
+
+This is the probability in which executing a single T gate may fail.
+
+## Constraints
+
+### Logical depth factor
+
+[//]: # "physicalCountsFormatted/logicalDepthFactor"
+
+_Factor, the initial number of logical cycles is multiplied by_
+
+This is the factor takes into account a potential overhead to the initial number of logical cycles.
+
+### Maximum number of T factories
+
+[//]: # "physicalCountsFormatted/maxTFactories"
+
+_The maximum number of T factories can be utilized during the algorithm's runtime_
+
+This is the maximum number of T factories used for producing the demanded T states, which can be created and executed by the algorithm in parallel.
+
+### Maximum runtime duration
+
+[//]: # "physicalCountsFormatted/maxDuration"
+
+_The maximum runtime duration allowed for the algorithm runtime_
+
+This is the maximum time allowed to the algorithm. If specified, the estimator targets to minimize the number of physical qubits consumed by the algorithm for runtimes under the maximum allowed.
+
+### Maximum number of physical qubits
+
+[//]: # "physicalCountsFormatted/maxPhysicalQubits"
+
+_The maximum number of physical qubits allowed for utilization to the algorithm_
+
+This is the maximum number of physical qubits available to the algorithm. If specified, the estimator targets to minimize the runtime of the algorithm with number of physical qubits consumed not exceeding this maximum.
+
+## Assumptions
+
+- _More details on the following lists of assumptions can be found in the paper [Accessing requirements for scaling quantum computers and their applications](https://aka.ms/AQ/RE/Paper)._
+- **Uniform independent physical noise.** We assume that the noise on physical qubits and physical qubit operations is the standard circuit noise model. In particular we assume error events at different space-time locations are independent and that error rates are uniform across the system in time and space.
+- **Efficient classical computation.** We assume that classical overhead (compilation, control, feedback, readout, decoding, etc.) does not dominate the overall cost of implementing the full quantum algorithm.
+- **Extraction circuits for planar quantum ISA.** We assume that stabilizer extraction circuits with similar depth and error correction performance to those for standard surface and Hastings-Haah code patches can be constructed to implement all operations of the planar quantum ISA (instruction set architecture).
+- **Uniform independent logical noise.** We assume that the error rate of a logical operation is approximately equal to its space-time volume (the number of tiles multiplied by the number of logical time steps) multiplied by the error rate of a logical qubit in a standard one-tile patch in one logical time step.
+- **Negligible Clifford costs for synthesis.** We assume that the space overhead for synthesis and space and time overhead for transport of magic states within magic state factories and to synthesis qubits are all negligible.
+- **Smooth magic state consumption rate.** We assume that the rate of T state consumption throughout the compiled algorithm is almost constant, or can be made almost constant without significantly increasing the number of logical time steps for the algorithm.
diff --git a/npm/ux/qsharp-ux.css b/npm/ux/qsharp-ux.css
index cc7ea25e..5e0fe6bb 100644
--- a/npm/ux/qsharp-ux.css
+++ b/npm/ux/qsharp-ux.css
@@ -615,3 +615,38 @@ html {
   fill: var(--vscode-icon-foreground, gray);
   animation: codicon-spin 1.5s steps(45) infinite;
 }
+
+.qs-scatterChart {
+  font-size: 14px;
+}
+
+.qs-scatterChart-x-axisTitle,
+.qs-scatterChart-y-axisTitle {
+  text-anchor: middle;
+  font-size: 16px;
+  fill: var(--main-color);
+}
+
+.qs-scatterChart-y-axisTitle {
+  writing-mode: vertical-lr;
+  transform: rotate(180deg);
+}
+
+.qs-scatterChart-line {
+  stroke: var(--border-color);
+}
+
+.qs-scatterChart-point {
+  r: 2px;
+  stroke-width: 4px;
+}
+
+.qs-scatterChart-point:hover {
+  r: 3px;
+  stroke-width: 6px;
+}
+
+.qs-scatterChart-watermark {
+  font-size: 16px;
+  fill: var(--main-color);
+}
diff --git a/npm/ux/reTable.tsx b/npm/ux/reTable.tsx
index afbdd97c..fa6596bd 100644
--- a/npm/ux/reTable.tsx
+++ b/npm/ux/reTable.tsx
@@ -2,41 +2,20 @@
 // Licensed under the MIT License.
 
 import { useState } from "preact/hooks";
-
-export type ReData = {
-  status: string;
-  jobParams: any;
-  physicalCounts: any;
-  physicalCountsFormatted: any;
-  logicalQubit: any;
-  tfactory: any;
-  errorBudget: any;
-  logicalCounts: any;
-  reportData: {
-    groups: {
-      title: string;
-      alwaysVisible: boolean;
-      entries: {
-        path: string;
-        label: string;
-        description: string;
-        explanation: string;
-      }[];
-    }[];
-    assumptions: string[];
-  };
-  new: boolean;
-};
+import { SingleEstimateResult } from "./data.js";
+import { CreateReport } from "./report.js";
 
 export function ReTable(props: {
   mdRenderer: (input: string) => string;
-  estimatesData: ReData;
+  estimatesData: SingleEstimateResult;
 }) {
   const [showDetail, setShowDetail] = useState(false);
   const toggleDetail = () => {
     setShowDetail(!showDetail);
   };
 
+  const reportData = CreateReport(props.estimatesData);
+
   return (
     <div>
       <div>
@@ -48,7 +27,7 @@ export function ReTable(props: {
         />
         <label htmlFor="showDetail"> Show detailed rows</label>
       </div>
-      {props.estimatesData.reportData.groups.map((group) => {
+      {reportData.groups.map((group) => {
         return (
           <details className="estimate-details">
             <summary>
@@ -101,7 +80,7 @@ export function ReTable(props: {
           <strong>Assumptions</strong>
         </summary>
         <ul className="estimate-table">
-          {props.estimatesData.reportData.assumptions.map((assumption) => (
+          {reportData.assumptions.map((assumption) => (
             <li
               className="estimate-assumption"
               dangerouslySetInnerHTML={{ __html: props.mdRenderer(assumption) }}
diff --git a/npm/ux/report.ts b/npm/ux/report.ts
new file mode 100644
index 00000000..392df0b9
--- /dev/null
+++ b/npm/ux/report.ts
@@ -0,0 +1,591 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { SingleEstimateResult } from "./data.js";
+
+type ReportData = {
+  groups: ReportGroup[];
+  assumptions: string[];
+};
+
+type ReportGroup = {
+  title: string;
+  alwaysVisible: boolean;
+  entries: ReportEntry[];
+};
+
+type ReportEntry = {
+  path: string;
+  label: string;
+  description: string;
+  explanation: string;
+};
+
+// THIS CODE HAS BEEN AUTOMATICALLY GENERATED WITH generate_report_code.py from output_data.md
+export function CreateReport(result: SingleEstimateResult): ReportData {
+  const groups = [] as ReportGroup[];
+  let entries = [] as ReportEntry[];
+  const numberFormat = new Intl.NumberFormat();
+  const numberFormatF64 = new Intl.NumberFormat(undefined, {
+    maximumFractionDigits: 2,
+    minimumFractionDigits: 2,
+  });
+
+  entries = [];
+  entries.push({
+    path: "physicalCountsFormatted/runtime",
+    label: "Runtime",
+    description: `Total runtime`,
+    explanation: `This is a runtime estimate for the execution time of the algorithm. In general, the execution time corresponds to the duration of one logical cycle (${numberFormat.format(
+      result.logicalQubit.logicalCycleTime,
+    )} nanosecs) multiplied by the ${numberFormat.format(
+      result.physicalCountsFormatted.algorithmicLogicalDepth,
+    )} logical cycles to run the algorithm. If however the duration of a single T factory (here: ${numberFormat.format(
+      result.tfactory == null ? 0 : result.tfactory.runtime,
+    )} nanosecs) is larger than the algorithm runtime, we extend the number of logical cycles artificially in order to exceed the runtime of a single T factory.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/rqops",
+    label: "rQOPS",
+    description: `Reliable quantum operations per second`,
+    explanation: `The value is computed as the number of logical qubits after layout (${numberFormat.format(
+      result.physicalCounts.breakdown.algorithmicLogicalQubits,
+    )}) (with a logical error rate of ${
+      result.physicalCountsFormatted.requiredLogicalQubitErrorRate
+    }) multiplied by the clock frequency (${numberFormatF64.format(
+      result.physicalCounts.breakdown.clockFrequency,
+    )}), which is the number of logical cycles per second.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/physicalQubits",
+    label: "Physical qubits",
+    description: `Number of physical qubits`,
+    explanation: `This value represents the total number of physical qubits, which is the sum of ${numberFormat.format(
+      result.physicalCounts.breakdown.physicalQubitsForAlgorithm,
+    )} physical qubits to implement the algorithm logic, and ${numberFormat.format(
+      result.physicalCounts.breakdown.physicalQubitsForTfactories,
+    )} physical qubits to execute the T factories that are responsible to produce the T states that are consumed by the algorithm.`,
+  });
+  groups.push({
+    title: "Physical resource estimates",
+    alwaysVisible: true,
+    entries: entries,
+  });
+
+  entries = [];
+  entries.push({
+    path: "physicalCountsFormatted/algorithmicLogicalQubits",
+    label: "Logical algorithmic qubits",
+    description: `Number of logical qubits for the algorithm after layout`,
+    explanation: `Laying out the logical qubits in the presence of nearest-neighbor constraints requires additional logical qubits. In particular, to layout the $Q_{\\rm alg} = ${numberFormat.format(
+      result.logicalCounts.numQubits,
+    )}$ logical qubits in the input algorithm, we require in total $2 \\cdot Q_{\\rm alg} + \\lceil \\sqrt{8 \\cdot Q_{\\rm alg}}\\rceil + 1 = ${numberFormat.format(
+      result.physicalCounts.breakdown.algorithmicLogicalQubits,
+    )}$ logical qubits.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/algorithmicLogicalDepth",
+    label: "Algorithmic depth",
+    description: `Number of logical cycles for the algorithm`,
+    explanation: `To execute the algorithm using _Parallel Synthesis Sequential Pauli Computation_ (PSSPC), operations are scheduled in terms of multi-qubit Pauli measurements, for which assume an execution time of one logical cycle. Based on the input algorithm, we require one multi-qubit measurement for the ${numberFormat.format(
+      result.logicalCounts.measurementCount,
+    )} single-qubit measurements, the ${numberFormat.format(
+      result.logicalCounts.rotationCount,
+    )} arbitrary single-qubit rotations, and the ${numberFormat.format(
+      result.logicalCounts.tCount,
+    )} T gates, three multi-qubit measurements for each of the ${numberFormat.format(
+      result.logicalCounts.cczCount,
+    )} CCZ and ${numberFormat.format(
+      result.logicalCounts.ccixCount,
+    )} CCiX gates in the input program, as well as ${
+      result.physicalCountsFormatted.numTsPerRotation
+    } multi-qubit measurements for each of the ${numberFormat.format(
+      result.logicalCounts.rotationDepth,
+    )} non-Clifford layers in which there is at least one single-qubit rotation with an arbitrary angle rotation.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalDepth",
+    label: "Logical depth",
+    description: `Number of logical cycles performed`,
+    explanation: `This number is usually equal to the logical depth of the algorithm, which is ${numberFormat.format(
+      result.physicalCountsFormatted.algorithmicLogicalDepth,
+    )}. However, in the case in which a single T factory is slower than the execution time of the algorithm, we adjust the logical cycle depth to exceed the T factory's execution time.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/clockFrequency",
+    label: "Clock frequency",
+    description: `Number of logical cycles per second`,
+    explanation: `This is the number of logical cycles that can be performed within one second. The logical cycle time is ${result.physicalCountsFormatted.logicalCycleTime}.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/numTstates",
+    label: "Number of T states",
+    description: `Number of T states consumed by the algorithm`,
+    explanation: `To execute the algorithm, we require one T state for each of the ${numberFormat.format(
+      result.logicalCounts.tCount,
+    )} T gates, four T states for each of the ${numberFormat.format(
+      result.logicalCounts.cczCount,
+    )} CCZ and ${numberFormat.format(
+      result.logicalCounts.ccixCount,
+    )} CCiX gates, as well as ${
+      result.physicalCountsFormatted.numTsPerRotation
+    } for each of the ${numberFormat.format(
+      result.logicalCounts.rotationCount,
+    )} single-qubit rotation gates with arbitrary angle rotation.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/numTfactories",
+    label: "Number of T factories",
+    description: `Number of T factories capable of producing the demanded ${numberFormat.format(
+      result.physicalCounts.breakdown.numTstates,
+    )} T states during the algorithm's runtime`,
+    explanation: `The total number of T factories ${numberFormat.format(
+      result.physicalCounts.breakdown.numTfactories,
+    )} that are executed in parallel is computed as $\\left\\lceil\\dfrac{\\text{T states}\\cdot\\text{T factory duration}}{\\text{T states per T factory}\\cdot\\text{algorithm runtime}}\\right\\rceil = \\left\\lceil\\dfrac{${numberFormat.format(
+      result.physicalCounts.breakdown.numTstates,
+    )} \\cdot ${numberFormat.format(
+      result.tfactory == null ? 0 : result.tfactory.runtime,
+    )}\\;\\text{ns}}{${numberFormat.format(
+      result.tfactory == null ? 0 : result.tfactory.numTstates,
+    )} \\cdot ${numberFormat.format(
+      result.physicalCounts.runtime,
+    )}\\;\\text{ns}}\\right\\rceil$`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/numTfactoryRuns",
+    label: "Number of T factory invocations",
+    description: `Number of times all T factories are invoked`,
+    explanation: `In order to prepare the ${numberFormat.format(
+      result.physicalCounts.breakdown.numTstates,
+    )} T states, the ${numberFormat.format(
+      result.physicalCounts.breakdown.numTfactories,
+    )} copies of the T factory are repeatedly invoked ${numberFormat.format(
+      result.physicalCounts.breakdown.numTfactoryRuns,
+    )} times.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/physicalQubitsForAlgorithm",
+    label: "Physical algorithmic qubits",
+    description: `Number of physical qubits for the algorithm after layout`,
+    explanation: `The ${numberFormat.format(
+      result.physicalCounts.breakdown.physicalQubitsForAlgorithm,
+    )} are the product of the ${numberFormat.format(
+      result.physicalCounts.breakdown.algorithmicLogicalQubits,
+    )} logical qubits after layout and the ${numberFormat.format(
+      result.logicalQubit.physicalQubits,
+    )} physical qubits that encode a single logical qubit.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/physicalQubitsForTfactories",
+    label: "Physical T factory qubits",
+    description: `Number of physical qubits for the T factories`,
+    explanation: `Each T factory requires ${numberFormat.format(
+      result.tfactory == null ? 0 : result.tfactory.physicalQubits,
+    )} physical qubits and we run ${numberFormat.format(
+      result.physicalCounts.breakdown.numTfactories,
+    )} in parallel, therefore we need $${numberFormat.format(
+      result.physicalCounts.breakdown.physicalQubitsForTfactories,
+    )} = ${numberFormat.format(
+      result.tfactory == null ? 0 : result.tfactory.physicalQubits,
+    )} \\cdot ${numberFormat.format(
+      result.physicalCounts.breakdown.numTfactories,
+    )}$ qubits.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/requiredLogicalQubitErrorRate",
+    label: "Required logical qubit error rate",
+    description: `The minimum logical qubit error rate required to run the algorithm within the error budget`,
+    explanation: `The minimum logical qubit error rate is obtained by dividing the logical error probability ${
+      result.physicalCountsFormatted.errorBudgetLogical
+    } by the product of ${numberFormat.format(
+      result.physicalCounts.breakdown.algorithmicLogicalQubits,
+    )} logical qubits and the total cycle count ${numberFormat.format(
+      result.physicalCounts.breakdown.logicalDepth,
+    )}.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/requiredLogicalTstateErrorRate",
+    label: "Required logical T state error rate",
+    description: `The minimum T state error rate required for distilled T states`,
+    explanation: `The minimum T state error rate is obtained by dividing the T distillation error probability ${
+      result.physicalCountsFormatted.errorBudgetTstates
+    } by the total number of T states ${numberFormat.format(
+      result.physicalCounts.breakdown.numTstates,
+    )}.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/numTsPerRotation",
+    label: "Number of T states per rotation",
+    description: `Number of T states to implement a rotation with an arbitrary angle`,
+    explanation: `The number of T states to implement a rotation with an arbitrary angle is $\\lceil 0.53 \\log_2(${numberFormat.format(
+      result.logicalCounts.rotationCount,
+    )} / ${
+      result.errorBudget.rotations
+    }) + 5.3\\rceil$ [[arXiv:2203.10064](https://arxiv.org/abs/2203.10064)]. For simplicity, we use this formula for all single-qubit arbitrary angle rotations, and do not distinguish between best, worst, and average cases.`,
+  });
+  groups.push({
+    title: "Resource estimates breakdown",
+    alwaysVisible: false,
+    entries: entries,
+  });
+
+  entries = [];
+  entries.push({
+    path: "jobParams/qecScheme/name",
+    label: "QEC scheme",
+    description: `Name of QEC scheme`,
+    explanation: `You can load pre-defined QEC schemes by using the name \`surface_code\` or \`floquet_code\`. The latter only works with Majorana qubits.`,
+  });
+  entries.push({
+    path: "logicalQubit/codeDistance",
+    label: "Code distance",
+    description: `Required code distance for error correction`,
+    explanation: `The code distance is the smallest odd integer greater or equal to $\\dfrac{2\\log(${result.jobParams.qecScheme.crossingPrefactor} / ${result.physicalCounts.breakdown.requiredLogicalQubitErrorRate})}{\\log(${result.jobParams.qecScheme.errorCorrectionThreshold}/${result.physicalCounts.breakdown.cliffordErrorRate})} - 1$`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/physicalQubitsPerLogicalQubit",
+    label: "Physical qubits",
+    description: `Number of physical qubits per logical qubit`,
+    explanation: `The number of physical qubits per logical qubit are evaluated using the formula ${result.jobParams.qecScheme.physicalQubitsPerLogicalQubit} that can be user-specified.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCycleTime",
+    label: "Logical cycle time",
+    description: `Duration of a logical cycle in nanoseconds`,
+    explanation: `The runtime of one logical cycle in nanoseconds is evaluated using the formula ${result.jobParams.qecScheme.logicalCycleTime} that can be user-specified.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalErrorRate",
+    label: "Logical qubit error rate",
+    description: `Logical qubit error rate`,
+    explanation: `The logical qubit error rate is computed as $${result.jobParams.qecScheme.crossingPrefactor} \\cdot \\left(\\dfrac{${result.physicalCounts.breakdown.cliffordErrorRate}}{${result.jobParams.qecScheme.errorCorrectionThreshold}}\\right)^\\frac{${result.logicalQubit.codeDistance} + 1}{2}$`,
+  });
+  entries.push({
+    path: "jobParams/qecScheme/crossingPrefactor",
+    label: "Crossing prefactor",
+    description: `Crossing prefactor used in QEC scheme`,
+    explanation: `The crossing prefactor is usually extracted numerically from simulations when fitting an exponential curve to model the relationship between logical and physical error rate.`,
+  });
+  entries.push({
+    path: "jobParams/qecScheme/errorCorrectionThreshold",
+    label: "Error correction threshold",
+    description: `Error correction threshold used in QEC scheme`,
+    explanation: `The error correction threshold is the physical error rate below which the error rate of the logical qubit is less than the error rate of the physical qubit that constitute it. This value is usually extracted numerically from simulations of the logical error rate.`,
+  });
+  entries.push({
+    path: "jobParams/qecScheme/logicalCycleTime",
+    label: "Logical cycle time formula",
+    description: `QEC scheme formula used to compute logical cycle time`,
+    explanation: `This is the formula that is used to compute the logical cycle time ${numberFormat.format(
+      result.logicalQubit.logicalCycleTime,
+    )} ns.`,
+  });
+  entries.push({
+    path: "jobParams/qecScheme/physicalQubitsPerLogicalQubit",
+    label: "Physical qubits formula",
+    description: `QEC scheme formula used to compute number of physical qubits per logical qubit`,
+    explanation: `This is the formula that is used to compute the number of physical qubits per logical qubits ${numberFormat.format(
+      result.logicalQubit.physicalQubits,
+    )}.`,
+  });
+  groups.push({
+    title: "Logical qubit parameters",
+    alwaysVisible: false,
+    entries: entries,
+  });
+
+  if (result.tfactory != null) {
+    entries = [];
+    entries.push({
+      path: "physicalCountsFormatted/tfactoryPhysicalQubits",
+      label: "Physical qubits",
+      description: `Number of physical qubits for a single T factory`,
+      explanation: `This corresponds to the maximum number of physical qubits over all rounds of T distillation units in a T factory. A round of distillation contains of multiple copies of distillation units to achieve the required success probability of producing a T state with the expected logical T state error rate.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/tfactoryRuntime",
+      label: "Runtime",
+      description: `Runtime of a single T factory`,
+      explanation: `The runtime of a single T factory is the accumulated runtime of executing each round in a T factory.`,
+    });
+    entries.push({
+      path: "tfactory/numTstates",
+      label: "Number of output T states per run",
+      description: `Number of output T states produced in a single run of T factory`,
+      explanation: `The T factory takes as input ${numberFormat.format(
+        result.tfactory == null ? 0 : result.tfactory.numInputTstates,
+      )} noisy physical T states with an error rate of ${
+        result.jobParams.qubitParams.tGateErrorRate
+      } and produces ${numberFormat.format(
+        result.tfactory == null ? 0 : result.tfactory.numTstates,
+      )} T states with an error rate of ${
+        result.physicalCountsFormatted.tstateLogicalErrorRate
+      }.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/numInputTstates",
+      label: "Number of input T states per run",
+      description: `Number of physical input T states consumed in a single run of a T factory`,
+      explanation: `This value includes the physical input T states of all copies of the distillation unit in the first round.`,
+    });
+    entries.push({
+      path: "tfactory/numRounds",
+      label: "Distillation rounds",
+      description: `The number of distillation rounds`,
+      explanation: `This is the number of distillation rounds. In each round one or multiple copies of some distillation unit is executed.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/numUnitsPerRound",
+      label: "Distillation units per round",
+      description: `The number of units in each round of distillation`,
+      explanation: `This is the number of copies for the distillation units per round.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/unitNamePerRound",
+      label: "Distillation units",
+      description: `The types of distillation units`,
+      explanation: `These are the types of distillation units that are executed in each round. The units can be either physical or logical, depending on what type of qubit they are operating. Space-efficient units require fewer qubits for the cost of longer runtime compared to Reed-Muller preparation units.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/codeDistancePerRound",
+      label: "Distillation code distances",
+      description: `The code distance in each round of distillation`,
+      explanation: `This is the code distance used for the units in each round. If the code distance is 1, then the distillation unit operates on physical qubits instead of error-corrected logical qubits.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/physicalQubitsPerRound",
+      label: "Number of physical qubits per round",
+      description: `The number of physical qubits used in each round of distillation`,
+      explanation: `The maximum number of physical qubits over all rounds is the number of physical qubits for the T factory, since qubits are reused by different rounds.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/tfactoryRuntimePerRound",
+      label: "Runtime per round",
+      description: `The runtime of each distillation round`,
+      explanation: `The runtime of the T factory is the sum of the runtimes in all rounds.`,
+    });
+    entries.push({
+      path: "physicalCountsFormatted/tstateLogicalErrorRate",
+      label: "Logical T state error rate",
+      description: `Logical T state error rate`,
+      explanation: `This is the logical T state error rate achieved by the T factory which is equal or smaller than the required error rate ${result.physicalCountsFormatted.requiredLogicalTstateErrorRate}.`,
+    });
+    groups.push({
+      title: "T factory parameters",
+      alwaysVisible: false,
+      entries: entries,
+    });
+  }
+  entries = [];
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsNumQubits",
+    label: "Logical qubits (pre-layout)",
+    description: `Number of logical qubits in the input quantum program`,
+    explanation: `We determine ${numberFormat.format(
+      result.physicalCounts.breakdown.algorithmicLogicalQubits,
+    )} algorithmic logical qubits from this number by assuming to align them in a 2D grid. Auxiliary qubits are added to allow for sufficient space to execute multi-qubit Pauli measurements on all or a subset of the logical qubits.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsTCount",
+    label: "T gates",
+    description: `Number of T gates in the input quantum program`,
+    explanation: `This includes all T gates and adjoint T gates, but not T gates used to implement rotation gates with arbitrary angle, CCZ gates, or CCiX gates.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsRotationCount",
+    label: "Rotation gates",
+    description: `Number of rotation gates in the input quantum program`,
+    explanation: `This is the number of all rotation gates. If an angle corresponds to a Pauli, Clifford, or T gate, it is not accounted for in this number.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsRotationDepth",
+    label: "Rotation depth",
+    description: `Depth of rotation gates in the input quantum program`,
+    explanation: `This is the number of all non-Clifford layers that include at least one single-qubit rotation gate with an arbitrary angle.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsCczCount",
+    label: "CCZ gates",
+    description: `Number of CCZ-gates in the input quantum program`,
+    explanation: `This is the number of CCZ gates.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsCcixCount",
+    label: "CCiX gates",
+    description: `Number of CCiX-gates in the input quantum program`,
+    explanation: `This is the number of CCiX gates, which applies $-iX$ controlled on two control qubits [[1212.5069](https://arxiv.org/abs/1212.5069)].`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/logicalCountsMeasurementCount",
+    label: "Measurement operations",
+    description: `Number of single qubit measurements in the input quantum program`,
+    explanation: `This is the number of single qubit measurements in Pauli basis that are used in the input program. Note that all measurements are counted, however, the measurement result is is determined randomly (with a fixed seed) to be 0 or 1 with a probability of 50%.`,
+  });
+  groups.push({
+    title: "Pre-layout logical resources",
+    alwaysVisible: false,
+    entries: entries,
+  });
+
+  entries = [];
+  entries.push({
+    path: "physicalCountsFormatted/errorBudget",
+    label: "Total error budget",
+    description: `Total error budget for the algorithm`,
+    explanation: `The total error budget sets the overall allowed error for the algorithm, i.e., the number of times it is allowed to fail. Its value must be between 0 and 1 and the default value is 0.001, which corresponds to 0.1%, and means that the algorithm is allowed to fail once in 1000 executions. This parameter is highly application specific. For example, if one is running Shor's algorithm for factoring integers, a large value for the error budget may be tolerated as one can check that the output are indeed the prime factors of the input. On the other hand, a much smaller error budget may be needed for an algorithm solving a problem with a solution which cannot be efficiently verified. This budget $\\epsilon = \\epsilon_{\\log} + \\epsilon_{\\rm dis} + \\epsilon_{\\rm syn}$ is uniformly distributed and applies to errors $\\epsilon_{\\log}$ to implement logical qubits, an error budget $\\epsilon_{\\rm dis}$ to produce T states through distillation, and an error budget $\\epsilon_{\\rm syn}$ to synthesize rotation gates with arbitrary angles. Note that for distillation and rotation synthesis, the respective error budgets $\\epsilon_{\\rm dis}$ and $\\epsilon_{\\rm syn}$ are uniformly distributed among all T states and all rotation gates, respectively. If there are no rotation gates in the input algorithm, the error budget is uniformly distributed to logical errors and T state errors.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/errorBudgetLogical",
+    label: "Logical error probability",
+    description: `Probability of at least one logical error`,
+    explanation: `This is one third of the total error budget ${result.physicalCountsFormatted.errorBudget} if the input algorithm contains rotation with gates with arbitrary angles, or one half of it, otherwise.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/errorBudgetTstates",
+    label: "T distillation error probability",
+    description: `Probability of at least one faulty T distillation`,
+    explanation: `This is one third of the total error budget ${result.physicalCountsFormatted.errorBudget} if the input algorithm contains rotation with gates with arbitrary angles, or one half of it, otherwise.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/errorBudgetRotations",
+    label: "Rotation synthesis error probability",
+    description: `Probability of at least one failed rotation synthesis`,
+    explanation: `This is one third of the total error budget ${result.physicalCountsFormatted.errorBudget}.`,
+  });
+  groups.push({
+    title: "Assumed error budget",
+    alwaysVisible: false,
+    entries: entries,
+  });
+
+  entries = [];
+  entries.push({
+    path: "jobParams/qubitParams/name",
+    label: "Qubit name",
+    description: `Some descriptive name for the qubit model`,
+    explanation: `You can load pre-defined qubit parameters by using the names \`qubit_gate_ns_e3\`, \`qubit_gate_ns_e4\`, \`qubit_gate_us_e3\`, \`qubit_gate_us_e4\`, \`qubit_maj_ns_e4\`, or \`qubit_maj_ns_e6\`. The names of these pre-defined qubit parameters indicate the instruction set (gate-based or Majorana), the operation speed (ns or Âµs regime), as well as the fidelity (e.g., e3 for $10^{-3}$ gate error rates).`,
+  });
+  entries.push({
+    path: "jobParams/qubitParams/instructionSet",
+    label: "Instruction set",
+    description: `Underlying qubit technology (gate-based or Majorana)`,
+    explanation: `When modeling the physical qubit abstractions, we distinguish between two different physical instruction sets that are used to operate the qubits. The physical instruction set can be either _gate-based_ or _Majorana_. A gate-based instruction set provides single-qubit measurement, single-qubit gates (incl. T gates), and two-qubit gates. A Majorana instruction set provides a physical T gate, single-qubit measurement and two-qubit joint measurement operations.`,
+  });
+  entries.push({
+    path: "jobParams/qubitParams/oneQubitMeasurementTime",
+    label: "Single-qubit measurement time",
+    description: `Operation time for single-qubit measurement (t_meas) in ns`,
+    explanation: `This is the operation time in nanoseconds to perform a single-qubit measurement in the Pauli basis.`,
+  });
+  if (result.jobParams.qubitParams.instructionSet == "Majorana") {
+    entries.push({
+      path: "jobParams/qubitParams/twoQubitJointMeasurementTime",
+      label: "Two-qubit measurement time",
+      description: `Operation time for two-qubit measurement in ns`,
+      explanation: `This is the operation time in nanoseconds to perform a non-destructive two-qubit joint Pauli measurement.`,
+    });
+  }
+  if (result.jobParams.qubitParams.instructionSet == "GateBased") {
+    entries.push({
+      path: "jobParams/qubitParams/oneQubitGateTime",
+      label: "Single-qubit gate time",
+      description: `Operation time for single-qubit gate (t_gate) in ns`,
+      explanation: `This is the operation time in nanoseconds to perform a single-qubit Clifford operation, e.g., Hadamard or Phase gates.`,
+    });
+  }
+  if (result.jobParams.qubitParams.instructionSet == "GateBased") {
+    entries.push({
+      path: "jobParams/qubitParams/twoQubitGateTime",
+      label: "Two-qubit gate time",
+      description: `Operation time for two-qubit gate in ns`,
+      explanation: `This is the operation time in nanoseconds to perform a two-qubit Clifford operation, e.g., a CNOT or CZ gate.`,
+    });
+  }
+  entries.push({
+    path: "jobParams/qubitParams/tGateTime",
+    label: "T gate time",
+    description: `Operation time for a T gate`,
+    explanation: `This is the operation time in nanoseconds to execute a T gate.`,
+  });
+  entries.push({
+    path: "jobParams/qubitParams/oneQubitMeasurementErrorRate",
+    label: "Single-qubit measurement error rate",
+    description: `Error rate for single-qubit measurement`,
+    explanation: `This is the probability in which a single-qubit measurement in the Pauli basis may fail.`,
+  });
+  if (result.jobParams.qubitParams.instructionSet == "Majorana") {
+    entries.push({
+      path: "jobParams/qubitParams/twoQubitJointMeasurementErrorRate",
+      label: "Two-qubit measurement error rate",
+      description: `Error rate for two-qubit measurement`,
+      explanation: `This is the probability in which a non-destructive two-qubit joint Pauli measurement may fail.`,
+    });
+  }
+  if (result.jobParams.qubitParams.instructionSet == "GateBased") {
+    entries.push({
+      path: "jobParams/qubitParams/oneQubitGateErrorRate",
+      label: "Single-qubit error rate",
+      description: `Error rate for single-qubit Clifford gate (p)`,
+      explanation: `This is the probability in which a single-qubit Clifford operation, e.g., Hadamard or Phase gates, may fail.`,
+    });
+  }
+  if (result.jobParams.qubitParams.instructionSet == "GateBased") {
+    entries.push({
+      path: "jobParams/qubitParams/twoQubitGateErrorRate",
+      label: "Two-qubit error rate",
+      description: `Error rate for two-qubit Clifford gate`,
+      explanation: `This is the probability in which a two-qubit Clifford operation, e.g., CNOT or CZ gates, may fail.`,
+    });
+  }
+  entries.push({
+    path: "jobParams/qubitParams/tGateErrorRate",
+    label: "T gate error rate",
+    description: `Error rate to prepare single-qubit T state or apply a T gate (p_T)`,
+    explanation: `This is the probability in which executing a single T gate may fail.`,
+  });
+  groups.push({
+    title: "Physical qubit parameters",
+    alwaysVisible: false,
+    entries: entries,
+  });
+
+  entries = [];
+  entries.push({
+    path: "physicalCountsFormatted/logicalDepthFactor",
+    label: "Logical depth factor",
+    description: `Factor, the initial number of logical cycles is multiplied by`,
+    explanation: `This is the factor takes into account a potential overhead to the initial number of logical cycles.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/maxTFactories",
+    label: "Maximum number of T factories",
+    description: `The maximum number of T factories can be utilized during the algorithm's runtime`,
+    explanation: `This is the maximum number of T factories used for producing the demanded T states, which can be created and executed by the algorithm in parallel.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/maxDuration",
+    label: "Maximum runtime duration",
+    description: `The maximum runtime duration allowed for the algorithm runtime`,
+    explanation: `This is the maximum time allowed to the algorithm. If specified, the estimator targets to minimize the number of physical qubits consumed by the algorithm for runtimes under the maximum allowed.`,
+  });
+  entries.push({
+    path: "physicalCountsFormatted/maxPhysicalQubits",
+    label: "Maximum number of physical qubits",
+    description: `The maximum number of physical qubits allowed for utilization to the algorithm`,
+    explanation: `This is the maximum number of physical qubits available to the algorithm. If specified, the estimator targets to minimize the runtime of the algorithm with number of physical qubits consumed not exceeding this maximum.`,
+  });
+  groups.push({ title: "Constraints", alwaysVisible: false, entries: entries });
+
+  const assumptions = [
+    "_More details on the following lists of assumptions can be found in the paper [Accessing requirements for scaling quantum computers and their applications](https://aka.ms/AQ/RE/Paper)._",
+    "**Uniform independent physical noise.** We assume that the noise on physical qubits and physical qubit operations is the standard circuit noise model. In particular we assume error events at different space-time locations are independent and that error rates are uniform across the system in time and space.",
+    "**Efficient classical computation.** We assume that classical overhead (compilation, control, feedback, readout, decoding, etc.) does not dominate the overall cost of implementing the full quantum algorithm.",
+    "**Extraction circuits for planar quantum ISA.** We assume that stabilizer extraction circuits with similar depth and error correction performance to those for standard surface and Hastings-Haah code patches can be constructed to implement all operations of the planar quantum ISA (instruction set architecture).",
+    "**Uniform independent logical noise.** We assume that the error rate of a logical operation is approximately equal to its space-time volume (the number of tiles multiplied by the number of logical time steps) multiplied by the error rate of a logical qubit in a standard one-tile patch in one logical time step.",
+    "**Negligible Clifford costs for synthesis.** We assume that the space overhead for synthesis and space and time overhead for transport of magic states within magic state factories and to synthesis qubits are all negligible.",
+    "**Smooth magic state consumption rate.** We assume that the rate of T state consumption throughout the compiled algorithm is almost constant, or can be made almost constant without significantly increasing the number of logical time steps for the algorithm.",
+  ];
+
+  return { groups: groups, assumptions: assumptions };
+}
+// END OF AUTOMATICALLY GENERATED CODE
diff --git a/npm/ux/resultsTable.tsx b/npm/ux/resultsTable.tsx
index ca7ec0ec..df93d01d 100644
--- a/npm/ux/resultsTable.tsx
+++ b/npm/ux/resultsTable.tsx
@@ -2,14 +2,17 @@
 // Licensed under the MIT License.
 
 import { useRef, useState } from "preact/hooks";
-import { type ReData } from "./reTable.js";
 
-type CellValue = string | number | { value: string; sortBy: number };
+export type CellValue = string | number | { value: string; sortBy: number };
+export type Row = {
+  color: string;
+  cells: CellValue[];
+};
 
 // Note: column 0 is expected to be unique amongst all rows
 export function ResultsTable(props: {
   columnNames: string[];
-  data: ReData[];
+  rows: Row[];
   initialColumns: number[];
   ensureSelected: boolean;
   onRowSelected(rowId: string): void;
@@ -24,20 +27,18 @@ export function ResultsTable(props: {
   const [showColumnMenu, setShowColumnMenu] = useState(false);
   const [showRowMenu, setShowRowMenu] = useState("");
 
-  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",
+  const newest = getSortedRows(props.rows).find(
+    (row) => (row.cells[row.cells.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();
+    const rowId = newest.cells[0].toString();
     setSelectedRow(rowId);
     props.onRowSelected(rowId);
-  } else if (!selectedRow && props.ensureSelected && rows.length > 0) {
-    const rowId = rows[0][0].toString();
+  } else if (!selectedRow && props.ensureSelected && props.rows.length > 0) {
+    const rowId = props.rows[0].cells[0].toString();
     setSelectedRow(rowId);
     props.onRowSelected(rowId);
   }
@@ -156,7 +157,7 @@ export function ResultsTable(props: {
     }
   }
 
-  function getSortedRows(rows: CellValue[][]) {
+  function getSortedRows(rows: Row[]) {
     if (!sortColumn) return rows;
 
     const colIdx = sortColumn.columnId;
@@ -164,8 +165,8 @@ export function ResultsTable(props: {
 
     const sortedRows = [...rows];
     sortedRows.sort((a, b) => {
-      const aVal = a[colIdx];
-      const bVal = b[colIdx];
+      const aVal = a.cells[colIdx];
+      const bVal = b.cells[colIdx];
       if (typeof aVal === "string" && typeof bVal === "string") {
         return ascending ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
       } else if (typeof aVal === "number" && typeof bVal === "number") {
@@ -344,8 +345,8 @@ export function ResultsTable(props: {
         </tr>
       </thead>
       <tbody>
-        {getSortedRows(rows).map((row) => {
-          const rowId = row[0].toString();
+        {getSortedRows(props.rows).map((row) => {
+          const rowId = row.cells[0].toString();
           return (
             <tr
               onClick={() => rowClicked(rowId)}
@@ -364,7 +365,7 @@ export function ResultsTable(props: {
                   <svg width="16" height="16" style="position: relative;">
                     <path
                       stroke-width="1.5"
-                      stroke="gray"
+                      stroke={row.color}
                       stroke-linecap="round"
                       d="M4,5 h8 M4,8 h8 M4,11 h8"
                     />
@@ -386,7 +387,9 @@ export function ResultsTable(props: {
               </td>
               {showColumns.map((idx) => {
                 return (
-                  <td data-colid={idx.toString()}>{getCellStr(row[idx])}</td>
+                  <td data-colid={idx.toString()}>
+                    {getCellStr(row.cells[idx])}
+                  </td>
                 );
               })}
             </tr>
@@ -396,25 +399,3 @@ 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/npm/ux/scatterChart.tsx b/npm/ux/scatterChart.tsx
new file mode 100644
index 00000000..f7b4636a
--- /dev/null
+++ b/npm/ux/scatterChart.tsx
@@ -0,0 +1,351 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { CreateIntegerTicks, CreateTimeTicks, Tick } from "../src/ux/ticks.js";
+
+export type ScatterSeries = {
+  color: string;
+  items: PlotItem[];
+};
+
+export type PlotItem = {
+  x: number;
+  y: number;
+  label: string;
+};
+
+export type Axis = {
+  isTime: boolean;
+  label: string;
+};
+
+type Range = {
+  min: number;
+  max: number;
+};
+
+export function HideTooltip() {
+  const tooltip = document.getElementById("tooltip");
+  if (tooltip) {
+    tooltip.setAttribute("visibility", "hidden");
+  }
+}
+
+function hideTooltipIfNotClicked() {
+  const tooltip = document.getElementById("tooltip");
+  if (tooltip) {
+    if (tooltip.getAttribute("clicked") === "false") {
+      tooltip.setAttribute("visibility", "hidden");
+    }
+  }
+}
+
+export function ScatterChart(props: {
+  data: ScatterSeries[];
+  xAxis: Axis;
+  yAxis: Axis;
+  onPointSelected(seriesIndex: number, pointIndex: number): void;
+}) {
+  const data = props.data;
+
+  function findMinMaxSingle(
+    series: ScatterSeries,
+  ): [number, number, number, number] {
+    const xs = series.items.map((item) => item.x);
+    const ys = series.items.map((item) => item.y);
+    const minX = Math.min(...xs);
+    const maxX = Math.max(...xs);
+    const minY = Math.min(...ys);
+    const maxY = Math.max(...ys);
+    return [minX, maxX, minY, maxY];
+  }
+
+  function findMinMaxAll(
+    series: ScatterSeries[],
+  ): [number, number, number, number] {
+    const minMax = series.map(findMinMaxSingle);
+    const minX = Math.min(...minMax.map((x) => x[0]));
+    const maxX = Math.max(...minMax.map((x) => x[1]));
+    const minY = Math.min(...minMax.map((x) => x[2]));
+    const maxY = Math.max(...minMax.map((x) => x[3]));
+    return [minX, maxX, minY, maxY];
+  }
+
+  const [minX, maxX, minY, maxY] = findMinMaxAll(data);
+
+  const rangeCoefficient = 2;
+  const rangeX: Range = {
+    min: minX / rangeCoefficient,
+    max: maxX * rangeCoefficient,
+  };
+  const rangeY: Range = {
+    min: minY / rangeCoefficient,
+    max: maxY * rangeCoefficient,
+  };
+
+  function createAxisTicks(range: Range, isTime: boolean): Tick[] {
+    if (isTime) {
+      return CreateTimeTicks(range.min, range.max);
+    } else {
+      return CreateIntegerTicks(range.min, range.max);
+    }
+  }
+
+  const xTicks = createAxisTicks(rangeX, props.xAxis.isTime);
+  const yTicks = createAxisTicks(rangeY, props.yAxis.isTime);
+
+  function coordinateToSvgLogarithmic(
+    value: number,
+    range: Range,
+    size: number,
+  ): number {
+    return (
+      ((Math.log(value) - Math.log(range.min)) /
+        (Math.log(range.max) - Math.log(range.min))) *
+      size
+    );
+  }
+
+  const yAxisTitleWidth = 20;
+  const yAxisTickCaptionMaxWidth = 100;
+  const axisTickLength = 5;
+  const axisLineWidth = 1;
+  const xMargin =
+    yAxisTitleWidth + yAxisTickCaptionMaxWidth + axisTickLength + axisLineWidth;
+
+  const axisTitleHeight = 20;
+  const xAxisTickCaptionMaxHeight = 16;
+  const yMargin =
+    axisTitleHeight +
+    xAxisTickCaptionMaxHeight +
+    axisTickLength +
+    axisLineWidth;
+
+  const svgWidth = 844;
+  const svgViewBoxWidthPadding = 50;
+  const svgHeight = 480;
+  const svgViewBoxHeightPadding = 10;
+  const svgXMin = -xMargin;
+
+  const plotAreaWidth = svgWidth - xMargin;
+  const plotAreaHeight = svgHeight - yMargin;
+
+  const viewBox = `${svgXMin - svgViewBoxWidthPadding} ${
+    -plotAreaHeight - svgViewBoxHeightPadding
+  } ${svgWidth + svgViewBoxWidthPadding} ${
+    svgHeight + svgViewBoxHeightPadding
+  }`;
+
+  const tooltipTextLeftPadding = 5;
+  const tooltipRectanglePaddingHeight = 10;
+  const tooltipTextPaddingHeight = 25;
+
+  const yAxisTextPaddingFromTicks = 5;
+  const yAxisTextYPadding = 6;
+
+  function drawTooltip(
+    text: string,
+    x: number,
+    y: number,
+    seriesIndex: number,
+    pointIndex: number,
+    clicked: boolean = false,
+  ) {
+    const tooltip = document.getElementById("tooltip");
+    const tooltipRect = document.getElementById("tooltipRect");
+    const tooltipText = document.getElementById(
+      "tooltipText",
+    ) as unknown as SVGTextElement;
+
+    if (tooltipText) {
+      tooltipText.setAttribute("x", (x + tooltipTextLeftPadding).toString());
+      tooltipText.setAttribute("y", (y + tooltipTextPaddingHeight).toString());
+      tooltipText.textContent = text;
+    }
+    if (tooltipRect && tooltipText) {
+      const box = tooltipText.getBBox();
+      const textWidth = box.width;
+      tooltipRect.setAttribute(
+        "width",
+        (textWidth + 2 * tooltipTextLeftPadding).toString(),
+      );
+      tooltipRect.setAttribute("x", x.toString());
+      tooltipRect.setAttribute(
+        "y",
+        (y + tooltipRectanglePaddingHeight).toString(),
+      );
+    }
+    if (tooltip) {
+      tooltip.setAttribute("visibility", "visible");
+      tooltip.setAttribute("clicked", clicked.toString());
+    }
+    props.onPointSelected(seriesIndex, pointIndex);
+  }
+
+  return (
+    <div style="display: flex; flex-wrap: wrap; margin-top: 8px;">
+      <div class="chart-container">
+        <svg
+          width={svgWidth}
+          height={svgHeight}
+          viewBox={viewBox}
+          id="scatterChart"
+        >
+          <line
+            id="xAxis"
+            x1="0"
+            y1="0"
+            x2={plotAreaWidth}
+            y2="0"
+            stroke="var(--border-color)"
+          />
+
+          {xTicks.map((tick) => {
+            const x = coordinateToSvgLogarithmic(
+              tick.value,
+              rangeX,
+              plotAreaWidth,
+            );
+            return (
+              <g>
+                <line
+                  x1={x}
+                  y1="1"
+                  x2={x}
+                  y2={axisTickLength}
+                  stroke="var(--border-color)"
+                />
+                <text
+                  x={x}
+                  y={axisTickLength + xAxisTickCaptionMaxHeight}
+                  text-anchor="middle"
+                  fill="var(--main-color)"
+                >
+                  {tick.label}
+                </text>
+              </g>
+            );
+          })}
+
+          <line
+            id="yAxis"
+            x1="0"
+            y1="0"
+            x2="0"
+            y2={-svgHeight}
+            stroke="var(--border-color)"
+          />
+
+          {yTicks.map((tick) => {
+            const y = -coordinateToSvgLogarithmic(
+              tick.value,
+              rangeY,
+              plotAreaHeight,
+            );
+            return (
+              <g>
+                <line
+                  x1="0"
+                  y1={y}
+                  x2={-axisTickLength}
+                  y2={y}
+                  stroke="var(--border-color)"
+                />
+                <text
+                  x={-axisTickLength - yAxisTextPaddingFromTicks}
+                  y={y + yAxisTextYPadding}
+                  text-anchor="end"
+                  fill="var(--main-color)"
+                >
+                  {tick.label}
+                </text>
+              </g>
+            );
+          })}
+
+          <text
+            x={plotAreaWidth / 2}
+            y={yMargin}
+            class="qs-scatterChart-x-axisTitle"
+          >
+            {props.xAxis.label} (logarithmic)
+          </text>
+
+          <text
+            x={xMargin - axisTitleHeight}
+            y={plotAreaHeight / 2}
+            class="qs-scatterChart-y-axisTitle"
+          >
+            {props.yAxis.label} (logarithmic)
+          </text>
+
+          <text
+            class="qs-scatterChart-watermark"
+            x={xMargin - axisTitleHeight}
+            y={-svgHeight + yMargin}
+          >
+            Created with Azure Quantum Resource Estimator
+          </text>
+
+          {data.map((data, seriesIndex) => {
+            return data.items.map((item, pointIndex) => {
+              const x = coordinateToSvgLogarithmic(
+                item.x,
+                rangeX,
+                plotAreaWidth,
+              );
+
+              const y = -coordinateToSvgLogarithmic(
+                item.y,
+                rangeY,
+                plotAreaHeight,
+              );
+              return (
+                <circle
+                  id={`point-${seriesIndex}-${pointIndex}`}
+                  cx={x}
+                  cy={y}
+                  class="qs-scatterChart-point"
+                  stroke={data.color}
+                  onMouseOver={() =>
+                    drawTooltip(
+                      item.label,
+                      x,
+                      y,
+                      seriesIndex,
+                      pointIndex,
+                      false,
+                    )
+                  }
+                  onClick={() =>
+                    drawTooltip(item.label, x, y, seriesIndex, pointIndex, true)
+                  }
+                  onMouseOut={() => hideTooltipIfNotClicked()}
+                />
+              );
+            });
+          })}
+          <g id="tooltip" visibility="hidden">
+            <rect
+              id="tooltipRect"
+              x="100"
+              y="-100"
+              width="200"
+              height="22"
+              fill="white"
+              stroke="black"
+              stroke-width="1"
+            />
+            <text
+              id="tooltipText"
+              x="105"
+              y="115"
+              text-anchor="left"
+              fill="black"
+            ></text>
+          </g>
+        </svg>
+      </div>
+    </div>
+  );
+}
diff --git a/npm/ux/spaceChart.tsx b/npm/ux/spaceChart.tsx
index dc954743..4441a5f5 100644
--- a/npm/ux/spaceChart.tsx
+++ b/npm/ux/spaceChart.tsx
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-import { ReData } from "./reTable.js";
+import { SingleEstimateResult } from "./data.js";
 
 function getPieSegment(
   x: number,
@@ -26,7 +26,7 @@ function getPieSegment(
   return d;
 }
 
-export function SpaceChart(props: { estimatesData: ReData }) {
+export function SpaceChart(props: { estimatesData: SingleEstimateResult }) {
   const breakdown = props.estimatesData.physicalCounts.breakdown;
 
   // The values to be shown on the pie chart
diff --git a/package-lock.json b/package-lock.json
index 38757671..34d63d74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,7 @@
         "punycode": "^2.3.1",
         "typescript": "^5.2.2",
         "url": "^0.11.3",
+        "user-agent-data-types": "^0.4.2",
         "vscode-uri": "^3.0.8"
       },
       "engines": {
@@ -4632,6 +4633,12 @@
       "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
       "dev": true
     },
+    "node_modules/user-agent-data-types": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/user-agent-data-types/-/user-agent-data-types-0.4.2.tgz",
+      "integrity": "sha512-jXep3kO/dGNmDOkbDa8ccp4QArgxR4I76m3QVcJ1aOF0B9toc+YtSXtX5gLdDTZXyWlpQYQrABr6L1L2GZOghw==",
+      "dev": true
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 1a546d88..0c1eb99a 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
     "punycode": "^2.3.1",
     "typescript": "^5.2.2",
     "url": "^0.11.3",
+    "user-agent-data-types": "^0.4.2",
     "vscode-uri": "^3.0.8"
   }
 }
diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi
index 92e8b4b5..d96b72e3 100644
--- a/pip/qsharp/_native.pyi
+++ b/pip/qsharp/_native.pyi
@@ -57,17 +57,15 @@ class Interpreter:
         """
         ...
     def run(
-        self, entry_expr: str, shots: int, output_fn: Callable[[Output], None]
+        self, entry_expr: str, output_fn: Callable[[Output], None]
     ) -> Any:
         """
-        Runs the given Q# expressin for the given number of shots.
-        Each shot uses an independent instance of the simulator.
+        Runs the given Q# expression with an independent instance of the simulator.
 
         :param entry_expr: The entry expression.
-        :param shots: The number of shots to run.
         :param output_fn: A callback function that will be called with each output.
 
-        :returns values: A list of results or runtime errors.
+        :returns values: A result or runtime errors.
 
         :raises QSharpError: If there is an error interpreting the input.
         """
diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py
index 4573ffd0..9b279ca5 100644
--- a/pip/qsharp/_qsharp.py
+++ b/pip/qsharp/_qsharp.py
@@ -163,9 +163,9 @@ def run(
     for _ in range(shots):
         results.append({"result": None, "events": []})
         run_results = get_interpreter().run(
-            entry_expr, 1, on_save_events if save_events else print_output
+            entry_expr, on_save_events if save_events else print_output
         )
-        results[-1]["result"] = run_results[0]
+        results[-1]["result"] = run_results
         if on_result:
             on_result(results[-1])
 
diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs
index 1f3c8fde..1e0d4c73 100644
--- a/pip/src/interpreter.rs
+++ b/pip/src/interpreter.rs
@@ -167,19 +167,14 @@ impl Interpreter {
         &mut self,
         py: Python,
         entry_expr: &str,
-        shots: u32,
         callback: Option<PyObject>,
-    ) -> PyResult<Py<PyList>> {
+    ) -> PyResult<PyObject> {
         let mut receiver = OptionalCallbackReceiver { callback, py };
-        match self.interpreter.run(&mut receiver, entry_expr, shots) {
-            Ok(results) => Ok(PyList::new(
-                py,
-                results.into_iter().map(|res| match res {
-                    Ok(v) => ValueWrapper(v).into_py(py),
-                    Err(errors) => QSharpError::new_err(format_errors(errors)).into_py(py),
-                }),
-            )
-            .into_py(py)),
+        match self.interpreter.run(&mut receiver, entry_expr) {
+            Ok(result) => match result {
+                Ok(v) => Ok(ValueWrapper(v).into_py(py)),
+                Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
+            },
             Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
         }
     }
diff --git a/prereqs.py b/prereqs.py
index 0eee7fc0..c7f101e6 100755
--- a/prereqs.py
+++ b/prereqs.py
@@ -13,14 +13,14 @@ import tempfile
 import functools
 
 python_ver = (3, 11)  # Python support for Windows on ARM64 requires v3.11 or later
-rust_ver = (1, 73)  # Ensure Rust version 1.73 or later is installed
+rust_ver = (1, 75)  # Ensure Rust version 1.75 or later is installed
 node_ver = (
     18,
     17,
 )
 wasmpack_ver = (0, 12, 1)  # Latest tested wasm-pack version
-rust_fmt_ver = (1, 6, 0)  # Current version when Rust 1.73 shipped
-clippy_ver = (0, 1, 69)
+rust_fmt_ver = (1, 7, 0)  # Current version when Rust 1.75 shipped
+clippy_ver = (0, 1, 75)
 
 # Disable buffered output so that the log statements and subprocess output get interleaved in proper order
 print = functools.partial(print, flush=True)
diff --git a/resource_estimator/src/counts.rs b/resource_estimator/src/counts.rs
index c956b1a3..0557b7a3 100644
--- a/resource_estimator/src/counts.rs
+++ b/resource_estimator/src/counts.rs
@@ -522,10 +522,6 @@ impl Backend for LogicalCounter {
         true
     }
 
-    fn reinit(&mut self) {
-        *self = Self::default();
-    }
-
     fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
         match name {
             "BeginEstimateCaching" => {
diff --git a/resource_estimator/src/lib.rs b/resource_estimator/src/lib.rs
index d82b8cf0..a18199c6 100644
--- a/resource_estimator/src/lib.rs
+++ b/resource_estimator/src/lib.rs
@@ -53,11 +53,8 @@ pub fn estimate_expr(
     let mut stdout = std::io::sink();
     let mut out = GenericReceiver::new(&mut stdout);
     interpreter
-        .run_with_sim(&mut counter, &mut out, expr, 1)
+        .run_with_sim(&mut counter, &mut out, expr)
         .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/samples/estimation/estimation-frontier-widgets.ipynb b/samples/estimation/estimation-frontier-widgets.ipynb
new file mode 100644
index 00000000..f2eafe41
--- /dev/null
+++ b/samples/estimation/estimation-frontier-widgets.ipynb
@@ -0,0 +1,83 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import qsharp\n",
+    "from qsharp_widgets import SpaceChart, EstimateDetails, EstimatesOverview\n",
+    "with open(\"Dynamics.qs\", \"r\") as f:\n",
+    "    contents = f.read()\n",
+    "qsharp.eval(contents)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "result = qsharp.estimate(\n",
+    "    \"QuantumDynamics.Main()\",\n",
+    "    [\n",
+    "        {\n",
+    "        \"qubitParams\": { \"name\": \"qubit_maj_ns_e4\" },\n",
+    "        \"qecScheme\": { \"name\": \"surface_code\" },\n",
+    "        \"estimateType\": \"frontier\",\n",
+    "        },\n",
+    "        {\n",
+    "        \"qubitParams\": { \"name\": \"qubit_maj_ns_e6\" },\n",
+    "        \"qecScheme\": { \"name\": \"floquet_code\" },\n",
+    "        \"estimateType\": \"frontier\",\n",
+    "        },\n",
+    "    ]\n",
+    ")\n",
+    "\n",
+    "EstimatesOverview(result, colors=[\"#1f77b4\", \"#ff7f0e\"], runNames=[\"e4 Surface Code\", \"e6 Floquet Code\"])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Qubit usage diagram for the first (estimate index=0) run and the third (point index=2) shortest runtime\n",
+    "SpaceChart(result[0], 2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Estimate details usage for the second (estimate index=0) run and the fourth (point index=3) shortest runtime\n",
+    "EstimateDetails(result[1], 4)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.12.1"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/vscode/src/azure/networkRequests.ts b/vscode/src/azure/networkRequests.ts
index d5e419fb..5977e316 100644
--- a/vscode/src/azure/networkRequests.ts
+++ b/vscode/src/azure/networkRequests.ts
@@ -2,7 +2,12 @@
 // Licensed under the MIT License.
 
 import { log } from "qsharp-lang";
-import { EventType, UserFlowStatus, sendTelemetryEvent } from "../telemetry";
+import {
+  EventType,
+  UserFlowStatus,
+  getUserAgent,
+  sendTelemetryEvent,
+} from "../telemetry";
 import { getRandomGuid } from "../utils";
 
 const publicMgmtEndpoint = "https://management.azure.com";
@@ -19,6 +24,7 @@ export async function azureRequest(
   const headers: [string, string][] = [
     ["Authorization", `Bearer ${token}`],
     ["Content-Type", "application/json"],
+    ["User-Agent", getUserAgent()],
   ];
 
   try {
@@ -75,6 +81,7 @@ export async function storageRequest(
   const headers: [string, string][] = [
     ["x-ms-version", "2023-01-03"],
     ["x-ms-date", new Date().toUTCString()],
+    ["User-Agent", getUserAgent()],
   ];
   if (token) headers.push(["Authorization", `Bearer ${token}`]);
 
diff --git a/vscode/src/telemetry.ts b/vscode/src/telemetry.ts
index 6b522654..4f41bf82 100644
--- a/vscode/src/telemetry.ts
+++ b/vscode/src/telemetry.ts
@@ -1,3 +1,8 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/// <reference types="user-agent-data-types" />
+
 import * as vscode from "vscode";
 import TelemetryReporter from "@vscode/extension-telemetry";
 import { log } from "qsharp-lang";
@@ -223,6 +228,7 @@ export enum DebugEvent {
 }
 
 let reporter: TelemetryReporter | undefined;
+let userAgentString: string | undefined;
 
 export function initTelemetry(context: vscode.ExtensionContext) {
   const packageJson = context.extension?.packageJSON;
@@ -230,6 +236,9 @@ export function initTelemetry(context: vscode.ExtensionContext) {
     return;
   }
   reporter = new TelemetryReporter(packageJson.aiKey);
+  const version = context.extension?.packageJSON?.version;
+  const browserAndRelease = getBrowserRelease();
+  userAgentString = `VSCode/${version} ${browserAndRelease}`;
 
   sendTelemetryEvent(EventType.InitializePlugin, {}, {});
 }
@@ -250,3 +259,17 @@ export function sendTelemetryEvent<E extends keyof EventTypes>(
     )}`,
   );
 }
+
+function getBrowserRelease(): string {
+  if (navigator.userAgentData?.brands) {
+    const browser =
+      navigator.userAgentData.brands[navigator.userAgentData.brands.length - 1];
+    return `${browser.brand}/${browser.version}`;
+  } else {
+    return "";
+  }
+}
+
+export function getUserAgent(): string {
+  return userAgentString || navigator.userAgent;
+}
diff --git a/vscode/src/webview/rePage.tsx b/vscode/src/webview/rePage.tsx
index c5b9c508..eeb65878 100644
--- a/vscode/src/webview/rePage.tsx
+++ b/vscode/src/webview/rePage.tsx
@@ -2,25 +2,13 @@
 // Licensed under the MIT License.
 
 import { useState } from "preact/hooks";
-import { ResultsTable, SpaceChart, ReTable, type ReData } from "qsharp-lang/ux";
-
-const columnNames = [
-  "Run name",
-  "Qubit type",
-  "QEC scheme",
-  "Error budget",
-  "Logical qubits",
-  "Logical depth",
-  "Code distance",
-  "T states",
-  "T factories",
-  "T factory fraction",
-  "Runtime",
-  "rQOPS",
-  "Physical qubits",
-];
-
-const initialColumns = [0, 1, 2, 3, 4, 10, 11, 12];
+import {
+  SpaceChart,
+  ReTable,
+  type ReData,
+  EstimatesOverview,
+} from "qsharp-lang/ux";
+import { SingleEstimateResult } from "qsharp-lang/ux/data";
 
 export function RePage(props: {
   estimatesData: ReData[];
@@ -28,25 +16,7 @@ export function RePage(props: {
   renderer: (input: string) => string;
   onRowDeleted: (rowId: string) => void;
 }) {
-  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 {
-      setEstimate(
-        props.estimatesData.find((data) => data.jobParams.runName === rowId) ||
-          null,
-      );
-    }
-  }
-
-  function onRowDeleted(rowId: string) {
-    props.onRowDeleted(rowId);
-  }
+  const [estimate, setEstimate] = useState<SingleEstimateResult | null>(null);
 
   return (
     <>
@@ -96,19 +66,14 @@ export function RePage(props: {
         ) : null}
         <h1>Azure Quantum Resource Estimator</h1>
       </div>
-      <details open>
-        <summary style="font-size: 1.5em; font-weight: bold; margin: 24px 8px;">
-          Results
-        </summary>
-        <ResultsTable
-          columnNames={columnNames}
-          data={props.estimatesData}
-          initialColumns={initialColumns}
-          ensureSelected={true}
-          onRowSelected={onRowSelected}
-          onRowDeleted={onRowDeleted}
-        />
-      </details>
+      <EstimatesOverview
+        estimatesData={props.estimatesData}
+        isSimplifiedView={false}
+        onRowDeleted={props.onRowDeleted}
+        setEstimate={setEstimate}
+        runNames={null}
+        colors={null}
+      ></EstimatesOverview>
       {!estimate ? null : (
         <>
           <details open>
diff --git a/vscode/src/webviewPanel.ts b/vscode/src/webviewPanel.ts
index f07427bf..3e46f205 100644
--- a/vscode/src/webviewPanel.ts
+++ b/vscode/src/webviewPanel.ts
@@ -167,6 +167,7 @@ export function registerWebViewCommands(context: ExtensionContext) {
       const params = qubitType.map((item) => ({
         ...item.params,
         errorBudget: parseFloat(errorBudget),
+        estimateType: "frontier",
       }));
 
       log.info("RE params", params);
diff --git a/widgets/js/index.tsx b/widgets/js/index.tsx
index b979daf9..a7c3d90b 100644
--- a/widgets/js/index.tsx
+++ b/widgets/js/index.tsx
@@ -2,7 +2,13 @@
 // Licensed under the MIT License.
 
 import { render as prender } from "preact";
-import { ReTable, SpaceChart, Histogram } from "qsharp-lang/ux";
+import {
+  ReTable,
+  SpaceChart,
+  Histogram,
+  CreateSingleEstimateResult,
+  EstimatesOverview,
+} from "qsharp-lang/ux";
 import markdownIt from "markdown-it";
 
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -20,6 +26,7 @@ export function mdRenderer(input: string) {
 
 // Param types for AnyWidget render functions
 import type { AnyModel } from "@anywidget/types";
+
 type RenderArgs = {
   model: AnyModel;
   el: HTMLElement;
@@ -32,6 +39,9 @@ export function render({ model, el }: RenderArgs) {
     case "SpaceChart":
       renderChart({ model, el });
       break;
+    case "EstimatesOverview":
+      renderEstimatesOverview({ model, el });
+      break;
     case "EstimateDetails":
       renderTable({ model, el });
       break;
@@ -46,24 +56,66 @@ export function render({ model, el }: RenderArgs) {
 function renderTable({ model, el }: RenderArgs) {
   const onChange = () => {
     const estimates = model.get("estimates");
+    const index = model.get("index");
+    const singleEstimateResult = CreateSingleEstimateResult(estimates, index);
     prender(
-      <ReTable estimatesData={estimates} mdRenderer={mdRenderer}></ReTable>,
+      <ReTable
+        estimatesData={singleEstimateResult}
+        mdRenderer={mdRenderer}
+      ></ReTable>,
       el,
     );
   };
 
   onChange();
   model.on("change:estimates", onChange);
+  model.on("change:index", onChange);
 }
 
 function renderChart({ model, el }: RenderArgs) {
   const onChange = () => {
     const estimates = model.get("estimates");
-    prender(<SpaceChart estimatesData={estimates}></SpaceChart>, el);
+    const index = model.get("index");
+    const singleEstimateResult = CreateSingleEstimateResult(estimates, index);
+    prender(<SpaceChart estimatesData={singleEstimateResult}></SpaceChart>, el);
+  };
+
+  onChange();
+  model.on("change:estimates", onChange);
+  model.on("change:index", onChange);
+}
+
+function renderEstimatesOverview({ model, el }: RenderArgs) {
+  const onChange = () => {
+    const results = model.get("estimates");
+    const colors = model.get("colors");
+    const runNames = model.get("runNames");
+
+    const estimates = [];
+    if (results[0] == null) {
+      estimates.push(results);
+    } else {
+      for (const estimate of Object.values(results)) {
+        estimates.push(estimate);
+      }
+    }
+
+    prender(
+      <EstimatesOverview
+        estimatesData={estimates}
+        runNames={runNames}
+        colors={colors}
+        isSimplifiedView={true}
+        onRowDeleted={() => undefined}
+        setEstimate={() => undefined}
+      ></EstimatesOverview>,
+      el,
+    );
   };
 
   onChange();
   model.on("change:estimates", onChange);
+  model.on("change:colors", onChange);
 }
 
 function renderHistogram({ model, el }: RenderArgs) {
diff --git a/widgets/src/qsharp_widgets/__init__.py b/widgets/src/qsharp_widgets/__init__.py
index 5bb38c10..09fceb23 100644
--- a/widgets/src/qsharp_widgets/__init__.py
+++ b/widgets/src/qsharp_widgets/__init__.py
@@ -20,10 +20,46 @@ class SpaceChart(anywidget.AnyWidget):
 
     comp = traitlets.Unicode("SpaceChart").tag(sync=True)
     estimates = traitlets.Dict().tag(sync=True)
+    index = traitlets.Integer().tag(sync=True)
 
-    def __init__(self, estimates):
+    def __init__(self, estimates, index=None):
+        """
+        This function generates a chart for the qubit utilization of the estimates.
+
+        Parameters:
+        - estimates: data for the chart.
+        - index (optional): the index of the estimate to be displayed. In case of a single point estimate, the parameter is ignored. In case of the frontier estimate, indexes correspond to points on frontier from the shortest runtime to the longest one. If not provided, the shortest runtime estimate is displayed.
+        """
         super().__init__()
         self.estimates = estimates
+        self.index = 0 if index is None else index
+
+
+class EstimatesOverview(anywidget.AnyWidget):
+    _esm = pathlib.Path(__file__).parent / "static" / "index.js"
+    _css = pathlib.Path(__file__).parent / "static" / "index.css"
+
+    comp = traitlets.Unicode("EstimatesOverview").tag(sync=True)
+    estimates = traitlets.Dict().tag(sync=True)
+    colors = traitlets.List().tag(sync=True)
+    runNames = traitlets.List().tag(sync=True)
+
+    def __init__(self, estimates, colors=None, runNames=None):
+        """
+        This function generates a summary results table with a qubit-time diagram.
+
+        Parameters:
+        - estimates: data for the table and the diagram.
+        - colors (optional): the list of colors which could be provided in the hex form or by name. If the length of the list does not match the number of the estimates, the colors parameter will be ignored and replaced with defaults.
+        - runNames (optional): the list of the run names. If the length of the list does not match the number of the estimates, the runNames parameter will be ignored and replaced with defaults.
+
+        Returns:
+        None
+        """
+        super().__init__()
+        self.estimates = estimates
+        self.colors = [] if colors is None else colors
+        self.runNames = [] if runNames is None else runNames
 
 
 class EstimateDetails(anywidget.AnyWidget):
@@ -32,10 +68,19 @@ class EstimateDetails(anywidget.AnyWidget):
 
     comp = traitlets.Unicode("EstimateDetails").tag(sync=True)
     estimates = traitlets.Dict().tag(sync=True)
+    index = traitlets.Integer().tag(sync=True)
+
+    def __init__(self, estimates, index=None):
+        """
+        This function generates a report for the qubit utilization of the estimates.
 
-    def __init__(self, estimates):
+        Parameters:
+        - estimates: data for the report.
+        - index (optional): the index of the estimate to be displayed. In case of a single point estimate, the parameter is ignored. In case of the frontier estimate, indexes correspond to points on frontier from the shortest runtime to the longest one. If not provided, the shortest runtime estimate is displayed.
+        """
         super().__init__()
         self.estimates = estimates
+        self.index = 0 if index is None else index
 
 
 class Histogram(anywidget.AnyWidget):


Test patch:
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index a885c800..2da9196b 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -24,11 +24,10 @@ mod given_interpreter {
     fn run(
         interpreter: &mut Interpreter,
         expr: &str,
-        shots: u32,
-    ) -> (Result<Vec<InterpretResult>, Vec<Error>>, String) {
+    ) -> (Result<InterpretResult, Vec<Error>>, String) {
         let mut cursor = Cursor::new(Vec::<u8>::new());
         let mut receiver = CursorReceiver::new(&mut cursor);
-        (interpreter.run(&mut receiver, expr, shots), receiver.dump())
+        (interpreter.run(&mut receiver, expr), receiver.dump())
     }
 
     fn entry(
@@ -960,47 +959,45 @@ mod given_interpreter {
         #[test]
         fn run_with_shots() {
             let mut interpreter = get_interpreter();
-            let (result, output) = line(&mut interpreter, "operation Foo() : Int { 1 }");
+            let (result, output) = line(&mut interpreter, "operation Foo(qs : Qubit[]) : Unit { Microsoft.Quantum.Diagnostics.DumpMachine(); }");
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "Foo()", 5);
-            assert_eq!(output, String::new());
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                let val = r.expect("individual run should succeed");
-                assert_eq!(val, Value::Int(1));
+            for _ in 0..4 {
+                let (results, output) = run(&mut interpreter, "{use qs = Qubit[2]; Foo(qs)}");
+                is_unit_with_output(
+                    &results.expect("compilation should succeed"),
+                    &output,
+                    "STATE:\n|00⟩: 1+0i",
+                );
             }
         }
 
         #[test]
         fn run_parse_error() {
             let mut interpreter = get_interpreter();
-            let (results, _) = run(&mut interpreter, "Foo)", 5);
+            let (results, _) = run(&mut interpreter, "Foo)");
             results.expect_err("run() should fail");
         }
 
         #[test]
         fn run_compile_error() {
             let mut interpreter = get_interpreter();
-            let (results, _) = run(&mut interpreter, "Foo()", 5);
+            let (results, _) = run(&mut interpreter, "Foo()");
             results.expect_err("run() should fail");
         }
 
         #[test]
-        fn run_multiple_statements() {
+        fn run_multiple_statements_with_return_value() {
             let mut interpreter = get_interpreter();
             let (result, output) = line(&mut interpreter, "operation Foo() : Int { 1 }");
             is_only_value(&result, &output, &Value::unit());
             let (result, output) = line(&mut interpreter, "operation Bar() : Int { 2 }");
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "{ Foo(); Bar() }", 5);
-            assert_eq!(output, String::new());
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                let val = r.expect("individual run should succeed");
-                assert_eq!(val, Value::Int(2));
-            }
+            let (result, output) = run(&mut interpreter, "{ Foo(); Bar() }");
+            is_only_value(
+                &result.expect("compilation should succeed"),
+                &output,
+                &Value::Int(2),
+            );
         }
 
         #[test]
@@ -1011,12 +1008,16 @@ mod given_interpreter {
                 r#"operation Foo() : Int { fail "failed" }"#,
             );
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "Foo()", 5);
-            assert_eq!(output, String::new());
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                r.expect_err("individual run should fail");
+            for _ in 0..1 {
+                let (result, output) = run(&mut interpreter, "Foo()");
+                is_only_error(
+                    &result.expect("compilation should succeed"),
+                    &output,
+                    &expect![[r#"
+                        runtime error: program failed: failed
+                          explicit fail [line_0] [fail "failed"]
+                    "#]],
+                );
             }
         }
 
@@ -1028,19 +1029,13 @@ mod given_interpreter {
                 r#"operation Foo() : Unit { Message("hello!") }"#,
             );
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "Foo()", 5);
-            expect![[r#"
-                hello!
-                hello!
-                hello!
-                hello!
-                hello!"#]]
-            .assert_eq(&output);
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                let val = r.expect("individual run should succeed");
-                assert_eq!(val, Value::unit());
+            for _ in 0..4 {
+                let (result, output) = run(&mut interpreter, "Foo()");
+                is_unit_with_output(
+                    &result.expect("compilation should succeed"),
+                    &output,
+                    "hello!",
+                );
             }
         }
     }
diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs
index 2b3c7f1e..4c05191d 100644
--- a/compiler/qsc_eval/src/intrinsic/tests.rs
+++ b/compiler/qsc_eval/src/intrinsic/tests.rs
@@ -134,10 +134,6 @@ impl Backend for CustomSim {
         self.sim.qubit_is_zero(q)
     }
 
-    fn reinit(&mut self) {
-        self.sim.reinit();
-    }
-
     fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
         match name {
             "Add1" => Some(Ok(Value::Int(arg.unwrap_int() + 1))),
diff --git a/compiler/qsc_frontend/src/lower/tests.rs b/compiler/qsc_frontend/src/lower/tests.rs
index c9b403b6..47ae433e 100644
--- a/compiler/qsc_frontend/src/lower/tests.rs
+++ b/compiler/qsc_frontend/src/lower/tests.rs
@@ -134,7 +134,7 @@ fn test_target_profile_attr_wrong_args() {
         &expect![[r#"
             [
                 InvalidAttrArgs(
-                    "Full or Base",
+                    "Unrestricted or Base",
                     Span {
                         lo: 29,
                         hi: 34,
diff --git a/library/tests/Cargo.toml b/library/Cargo.toml
similarity index 78%
rename from library/tests/Cargo.toml
rename to library/Cargo.toml
index a2ecf62d..a635254a 100644
--- a/library/tests/Cargo.toml
+++ b/library/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "stdlib_tests"
+name = "library"
 
 version.workspace = true
 authors.workspace = true
@@ -8,12 +8,10 @@ repository.workspace = true
 edition.workspace = true
 license.workspace = true
 
-[dependencies]
-num-bigint = { workspace = true }
-qsc = { path = "../../compiler/qsc" }
-
 [dev-dependencies]
 indoc = { workspace = true }
+num-bigint = { workspace = true }
+qsc = { path = "../compiler/qsc" }
 
 [lib]
 doctest = false
diff --git a/library/src/tests.rs b/library/src/tests.rs
new file mode 100644
index 00000000..afc9bc88
--- /dev/null
+++ b/library/src/tests.rs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::needless_raw_string_hashes)]
+
+mod arithmetic;
+mod arrays;
+mod canon;
+mod convert;
+mod diagnostics;
+mod math;
+mod measurement;
+mod table_lookup;
+
+use indoc::indoc;
+use qsc::{
+    interpret::{stateful, GenericReceiver, Value},
+    target::Profile,
+    PackageType, SourceMap,
+};
+
+/// # Panics
+///
+/// Will panic if compilation fails or the result is not the same as expected.
+/// NOTE: Floating point numbers in tuples are compared taking precision into
+/// account so that results of calculations can also be compared.
+pub fn test_expression(expr: &str, expected: &Value) {
+    test_expression_with_lib(expr, "", expected);
+}
+
+pub fn test_expression_with_lib(expr: &str, lib: &str, expected: &Value) {
+    test_expression_with_lib_and_profile(expr, lib, Profile::Unrestricted, expected);
+}
+
+pub fn test_expression_with_lib_and_profile(
+    expr: &str,
+    lib: &str,
+    profile: Profile,
+    expected: &Value,
+) {
+    let mut stdout = vec![];
+    let mut out = GenericReceiver::new(&mut stdout);
+
+    let sources = SourceMap::new([("test".into(), lib.into())], Some(expr.into()));
+
+    let mut interpreter =
+        stateful::Interpreter::new(true, sources, PackageType::Exe, profile.into())
+            .expect("test should compile");
+    let result = interpreter
+        .eval_entry(&mut out)
+        .expect("test should run successfully");
+
+    match (&expected, result) {
+        (&Value::Tuple(tup1), Value::Tuple(tup2)) if tup1.len() == tup2.len() => {
+            // If both values are tuples of the same length, we crack them open and compare elements
+            for (value1, value2) in tup1.iter().zip(tup2.iter()) {
+                if let (Value::Double(double1), Value::Double(double2)) = (value1, value2) {
+                    // If both elements are doubles, we use approximate comparison
+                    assert_doubles_almost_equal(*double1, *double2);
+                } else {
+                    assert_eq!(value1, value2);
+                }
+            }
+        }
+        (&Value::Double(double1), Value::Double(double2)) => {
+            assert_doubles_almost_equal(*double1, double2);
+        }
+        (&expected, result) => assert_eq!(expected, &result),
+    }
+}
+
+/// # Panics
+///
+/// Will panic if f64 values are significantly different.
+fn assert_doubles_almost_equal(val1: f64, val2: f64) {
+    let val1_abs = val1.abs();
+    let val2_abs = val2.abs();
+    if val1_abs < f64::MIN_POSITIVE && val2_abs < f64::MIN_POSITIVE {
+        // Note, that f64::MIN_POSITIVE is not the smallest representable positive number.
+        return;
+    }
+    assert!(
+        ((val1 - val2).abs() / (val1_abs + val2_abs)) < 1e-15,
+        "Significant difference between expected and actual values: val1={val1}, val2={val2}."
+    );
+}
+
+//
+// Core namespace
+//
+#[test]
+fn check_repeated() {
+    test_expression("Repeated(Zero, 0)", &Value::Array(vec![].into()));
+    test_expression(
+        "Repeated(One, 1)",
+        &Value::Array(vec![Value::RESULT_ONE].into()),
+    );
+    test_expression(
+        "Repeated(1, 2)",
+        &Value::Array(vec![Value::Int(1), Value::Int(1)].into()),
+    );
+    test_expression(
+        "Repeated(true, 3)",
+        &Value::Array(vec![Value::Bool(true), Value::Bool(true), Value::Bool(true)].into()),
+    );
+}
+
+#[test]
+fn check_exp_with_cnot() {
+    // This decomposition only holds if the magnitude of the angle used in Exp is correct and if the
+    // sign convention between Rx, Rz, and Exp is consistent.
+    test_expression(
+        indoc! {r#"{
+            open Microsoft.Quantum.Diagnostics;
+            open Microsoft.Quantum.Math;
+
+            use (aux, control, target) = (Qubit(), Qubit(), Qubit());
+            within {
+                H(aux);
+                CNOT(aux, control);
+                CNOT(aux, target);
+            }
+            apply {
+                let theta  = PI() / 4.0;
+                Rx(-2.0 * theta, target);
+                Rz(-2.0 * theta, control);
+                Adjoint Exp([PauliZ, PauliX], theta, [control, target]);
+
+                Adjoint CNOT(control, target);
+            }
+
+            CheckAllZero([aux, control, target])
+        }"#},
+        &Value::Bool(true),
+    );
+}
+
+#[test]
+fn check_exp_with_swap() {
+    // This decomposition only holds if the magnitude of the angle used in Exp is correct.
+    test_expression(
+        indoc! {r#"{
+            open Microsoft.Quantum.Diagnostics;
+            open Microsoft.Quantum.Math;
+
+            use (aux, qs) = (Qubit(), Qubit[2]);
+            within {
+                H(aux);
+                CNOT(aux, qs[0]);
+                CNOT(aux, qs[1]);
+            }
+            apply {
+                let theta  = PI() / 4.0;
+                Exp([PauliX, PauliX], theta, qs);
+                Exp([PauliY, PauliY], theta, qs);
+                Exp([PauliZ, PauliZ], theta, qs);
+
+                Adjoint SWAP(qs[0], qs[1]);
+            }
+
+            CheckAllZero([aux] + qs)
+        }"#},
+        &Value::Bool(true),
+    );
+}
+
+#[test]
+fn check_base_profile_measure_resets_aux_qubits() {
+    test_expression_with_lib_and_profile(
+        indoc! {"{
+            use q = Qubit();
+            X(q);
+            let result = M(q);
+            Reset(q);
+            result
+        }"},
+        "",
+        Profile::Base,
+        &Value::RESULT_ONE,
+    );
+}
diff --git a/library/tests/src/test_arithmetic.rs b/library/src/tests/arithmetic.rs
similarity index 99%
rename from library/tests/src/test_arithmetic.rs
rename to library/src/tests/arithmetic.rs
index 2eef74fe..f25209b7 100644
--- a/library/tests/src/test_arithmetic.rs
+++ b/library/src/tests/arithmetic.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::{test_expression, test_expression_with_lib};
+use super::{test_expression, test_expression_with_lib};
 use qsc::interpret::Value;
 
 // Tests for Microsoft.Quantum.Unstable.Arithmetic namespace
diff --git a/library/tests/src/test_arrays.rs b/library/src/tests/arrays.rs
similarity index 99%
rename from library/tests/src/test_arrays.rs
rename to library/src/tests/arrays.rs
index 8e44c423..9628f939 100644
--- a/library/tests/src/test_arrays.rs
+++ b/library/src/tests/arrays.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::test_expression;
+use super::test_expression;
 use num_bigint::BigInt;
 use qsc::interpret::Value;
 
diff --git a/library/tests/src/test_canon.rs b/library/src/tests/canon.rs
similarity index 99%
rename from library/tests/src/test_canon.rs
rename to library/src/tests/canon.rs
index f70b9123..4d0c158f 100644
--- a/library/tests/src/test_canon.rs
+++ b/library/src/tests/canon.rs
@@ -3,7 +3,7 @@
 
 #![allow(clippy::needless_raw_string_hashes)]
 
-use crate::{test_expression, test_expression_with_lib};
+use super::{test_expression, test_expression_with_lib};
 use indoc::indoc;
 use qsc::interpret::Value;
 
diff --git a/library/tests/src/test_convert.rs b/library/src/tests/convert.rs
similarity index 99%
rename from library/tests/src/test_convert.rs
rename to library/src/tests/convert.rs
index 428663de..a194e78f 100644
--- a/library/tests/src/test_convert.rs
+++ b/library/src/tests/convert.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::test_expression;
+use super::test_expression;
 use qsc::interpret::Value;
 
 // Tests for Microsoft.Quantum.Convert namespace
diff --git a/library/tests/src/test_diagnostics.rs b/library/src/tests/diagnostics.rs
similarity index 97%
rename from library/tests/src/test_diagnostics.rs
rename to library/src/tests/diagnostics.rs
index 240ace1a..7aed5a90 100644
--- a/library/tests/src/test_diagnostics.rs
+++ b/library/src/tests/diagnostics.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::test_expression;
+use super::test_expression;
 use qsc::interpret::Value;
 
 #[test]
diff --git a/library/tests/src/test_math.rs b/library/src/tests/math.rs
similarity index 99%
rename from library/tests/src/test_math.rs
rename to library/src/tests/math.rs
index f9651352..61becf42 100644
--- a/library/tests/src/test_math.rs
+++ b/library/src/tests/math.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::test_expression;
+use super::test_expression;
 use core::f64::consts::E;
 use num_bigint::BigInt;
 use qsc::interpret::Value;
diff --git a/library/tests/src/test_measurement.rs b/library/src/tests/measurement.rs
similarity index 99%
rename from library/tests/src/test_measurement.rs
rename to library/src/tests/measurement.rs
index 8923e207..8b36185f 100644
--- a/library/tests/src/test_measurement.rs
+++ b/library/src/tests/measurement.rs
@@ -3,7 +3,7 @@
 
 #![allow(clippy::needless_raw_string_hashes)]
 
-use crate::test_expression;
+use super::test_expression;
 use indoc::indoc;
 use qsc::interpret::Value;
 
diff --git a/library/tests/src/resources/add_le.qs b/library/src/tests/resources/add_le.qs
similarity index 100%
rename from library/tests/src/resources/add_le.qs
rename to library/src/tests/resources/add_le.qs
diff --git a/library/tests/src/resources/compare.qs b/library/src/tests/resources/compare.qs
similarity index 100%
rename from library/tests/src/resources/compare.qs
rename to library/src/tests/resources/compare.qs
diff --git a/library/tests/src/resources/inc_by_le.qs b/library/src/tests/resources/inc_by_le.qs
similarity index 100%
rename from library/tests/src/resources/inc_by_le.qs
rename to library/src/tests/resources/inc_by_le.qs
diff --git a/library/tests/src/resources/qft_le.qs b/library/src/tests/resources/qft_le.qs
similarity index 100%
rename from library/tests/src/resources/qft_le.qs
rename to library/src/tests/resources/qft_le.qs
diff --git a/library/tests/src/resources/select.qs b/library/src/tests/resources/select.qs
similarity index 100%
rename from library/tests/src/resources/select.qs
rename to library/src/tests/resources/select.qs
diff --git a/library/tests/src/test_table_lookup.rs b/library/src/tests/table_lookup.rs
similarity index 96%
rename from library/tests/src/test_table_lookup.rs
rename to library/src/tests/table_lookup.rs
index dfad1fbf..9f8563b0 100644
--- a/library/tests/src/test_table_lookup.rs
+++ b/library/src/tests/table_lookup.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-use crate::test_expression_with_lib;
+use super::test_expression_with_lib;
 use qsc::interpret::Value;
 
 // Tests for Microsoft.Quantum.TableLookup namespace
diff --git a/library/tests/src/lib.rs b/library/tests/src/lib.rs
deleted file mode 100644
index bf785e30..00000000
--- a/library/tests/src/lib.rs
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#![warn(clippy::mod_module_files, clippy::pedantic, clippy::unwrap_used)]
-#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
-
-#[cfg(test)]
-mod test_arithmetic;
-#[cfg(test)]
-mod test_arrays;
-#[cfg(test)]
-mod test_canon;
-#[cfg(test)]
-mod test_convert;
-#[cfg(test)]
-mod test_diagnostics;
-#[cfg(test)]
-mod test_math;
-#[cfg(test)]
-mod test_measurement;
-#[cfg(test)]
-mod test_table_lookup;
-#[cfg(test)]
-mod tests;
-
-use qsc::{
-    interpret::{stateful, GenericReceiver, Value},
-    target::Profile,
-    PackageType, SourceMap,
-};
-
-/// # Panics
-///
-/// Will panic if compilation fails or the result is not the same as expected.
-/// NOTE: Floating point numbers in tuples are compared taking precision into
-/// account so that results of calculations can also be compared.
-pub fn test_expression(expr: &str, expected: &Value) {
-    test_expression_with_lib(expr, "", expected);
-}
-
-pub fn test_expression_with_lib(expr: &str, lib: &str, expected: &Value) {
-    test_expression_with_lib_and_profile(expr, lib, Profile::Unrestricted, expected);
-}
-
-pub fn test_expression_with_lib_and_profile(
-    expr: &str,
-    lib: &str,
-    profile: Profile,
-    expected: &Value,
-) {
-    let mut stdout = vec![];
-    let mut out = GenericReceiver::new(&mut stdout);
-
-    let sources = SourceMap::new([("test".into(), lib.into())], Some(expr.into()));
-
-    let mut interpreter =
-        stateful::Interpreter::new(true, sources, PackageType::Exe, profile.into())
-            .expect("test should compile");
-    let result = interpreter
-        .eval_entry(&mut out)
-        .expect("test should run successfully");
-
-    match (&expected, result) {
-        (&Value::Tuple(tup1), Value::Tuple(tup2)) if tup1.len() == tup2.len() => {
-            // If both values are tuples of the same length, we crack them open and compare elements
-            for (value1, value2) in tup1.iter().zip(tup2.iter()) {
-                if let (Value::Double(double1), Value::Double(double2)) = (value1, value2) {
-                    // If both elements are doubles, we use approximate comparison
-                    assert_doubles_almost_equal(*double1, *double2);
-                } else {
-                    assert_eq!(value1, value2);
-                }
-            }
-        }
-        (&Value::Double(double1), Value::Double(double2)) => {
-            assert_doubles_almost_equal(*double1, double2);
-        }
-        (&expected, result) => assert_eq!(expected, &result),
-    }
-}
-
-/// # Panics
-///
-/// Will panic if f64 values are significantly different.
-fn assert_doubles_almost_equal(val1: f64, val2: f64) {
-    let val1_abs = val1.abs();
-    let val2_abs = val2.abs();
-    if val1_abs < f64::MIN_POSITIVE && val2_abs < f64::MIN_POSITIVE {
-        // Note, that f64::MIN_POSITIVE is not the smallest representable positive number.
-        return;
-    }
-    assert!(
-        ((val1 - val2).abs() / (val1_abs + val2_abs)) < 1e-15,
-        "Significant difference between expected and actual values: val1={val1}, val2={val2}."
-    );
-}
diff --git a/library/tests/src/tests.rs b/library/tests/src/tests.rs
deleted file mode 100644
index 177c6856..00000000
--- a/library/tests/src/tests.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#![allow(clippy::needless_raw_string_hashes)]
-
-use crate::{test_expression, test_expression_with_lib_and_profile};
-use indoc::indoc;
-use qsc::{interpret::Value, target::Profile};
-
-//
-// Core namespace
-//
-#[test]
-fn check_repeated() {
-    test_expression("Repeated(Zero, 0)", &Value::Array(vec![].into()));
-    test_expression(
-        "Repeated(One, 1)",
-        &Value::Array(vec![Value::RESULT_ONE].into()),
-    );
-    test_expression(
-        "Repeated(1, 2)",
-        &Value::Array(vec![Value::Int(1), Value::Int(1)].into()),
-    );
-    test_expression(
-        "Repeated(true, 3)",
-        &Value::Array(vec![Value::Bool(true), Value::Bool(true), Value::Bool(true)].into()),
-    );
-}
-
-#[test]
-fn check_exp_with_cnot() {
-    // This decomposition only holds if the magnitude of the angle used in Exp is correct and if the
-    // sign convention between Rx, Rz, and Exp is consistent.
-    test_expression(
-        indoc! {r#"{
-            open Microsoft.Quantum.Diagnostics;
-            open Microsoft.Quantum.Math;
-
-            use (aux, control, target) = (Qubit(), Qubit(), Qubit());
-            within {
-                H(aux);
-                CNOT(aux, control);
-                CNOT(aux, target);
-            }
-            apply {
-                let theta  = PI() / 4.0;
-                Rx(-2.0 * theta, target);
-                Rz(-2.0 * theta, control);
-                Adjoint Exp([PauliZ, PauliX], theta, [control, target]);
-
-                Adjoint CNOT(control, target);
-            }
-
-            CheckAllZero([aux, control, target])
-        }"#},
-        &Value::Bool(true),
-    );
-}
-
-#[test]
-fn check_exp_with_swap() {
-    // This decomposition only holds if the magnitude of the angle used in Exp is correct.
-    test_expression(
-        indoc! {r#"{
-            open Microsoft.Quantum.Diagnostics;
-            open Microsoft.Quantum.Math;
-
-            use (aux, qs) = (Qubit(), Qubit[2]);
-            within {
-                H(aux);
-                CNOT(aux, qs[0]);
-                CNOT(aux, qs[1]);
-            }
-            apply {
-                let theta  = PI() / 4.0;
-                Exp([PauliX, PauliX], theta, qs);
-                Exp([PauliY, PauliY], theta, qs);
-                Exp([PauliZ, PauliZ], theta, qs);
-
-                Adjoint SWAP(qs[0], qs[1]);
-            }
-
-            CheckAllZero([aux] + qs)
-        }"#},
-        &Value::Bool(true),
-    );
-}
-
-#[test]
-fn check_base_profile_measure_resets_aux_qubits() {
-    test_expression_with_lib_and_profile(
-        indoc! {"{
-            use q = Qubit();
-            X(q);
-            let result = M(q);
-            Reset(q);
-            result
-        }"},
-        "",
-        Profile::Base,
-        &Value::RESULT_ONE,
-    );
-}
diff --git a/npm/test/basics.js b/npm/test/basics.js
index 714587e8..0245dbba 100644
--- a/npm/test/basics.js
+++ b/npm/test/basics.js
@@ -16,6 +16,7 @@ import {
 import { QscEventTarget } from "../dist/compiler/events.js";
 import { getAllKatas, getExerciseSources, getKata } from "../dist/katas.js";
 import samples from "../dist/samples.generated.js";
+import { CreateIntegerTicks, CreateTimeTicks } from "../dist/ux/ticks.js";
 
 /** @type {import("../dist/log.js").TelemetryEvent[]} */
 const telemetryEvents = [];
@@ -879,3 +880,269 @@ test("debug service compiling multiple sources - web worker", async () => {
     debugService.terminate();
   }
 });
+
+test("CreateIntegerTicks: invalid inputs", () => {
+  runAndAssertIntegerTicks(2, 1, []);
+  runAndAssertIntegerTicks(0, 2, []);
+  runAndAssertIntegerTicks(-5, 100, []);
+});
+
+test("CreateIntegerTicks: below 100", () => {
+  runAndAssertIntegerTicks(1, 1, [1]);
+  runAndAssertIntegerTicks(3, 3, [3]);
+  runAndAssertIntegerTicks(4, 6, [4, 5, 6]);
+  runAndAssertIntegerTicks(1, 100, [1, 10, 100]);
+  runAndAssertIntegerTicks(1, 10, [1, 10]);
+  runAndAssertIntegerTicks(1, 9, [1]);
+  runAndAssertIntegerTicks(2, 10, [10]);
+  runAndAssertIntegerTicks(2, 9, [2, 3, 4, 5, 6, 7, 8, 9]);
+});
+
+test("CreateIntegerTicks: more than 100", () => {
+  runAndAssertIntegerTicks(20, 59, [20, 30, 40, 50]);
+  runAndAssertIntegerTicks(231, 365, [300]);
+  runAndAssertIntegerTicks(331, 365, [340, 350, 360]);
+  runAndAssertIntegerTicks(567, 569, [567, 568, 569]);
+});
+
+test("CreateIntegerTicks: expected qubit numbers", () => {
+  runAndAssertIntegerTicks(400, 8000000, [1000, 10000, 100000, 1000000]);
+  runAndAssertIntegerTicks(12345, 67890, [20000, 30000, 40000, 50000, 60000]);
+  runAndAssertIntegerTicks(23456, 27890, [24000, 25000, 26000, 27000]);
+});
+
+test("CreateTimeTicks: invalid inputs", () => {
+  runAndAssertTimeTicks(2, 1, []);
+  runAndAssertTimeTicks(0, 2, []);
+  runAndAssertTimeTicks(-5, 100, []);
+});
+
+const second = 1e9;
+const minute = 60 * second;
+const hour = 60 * minute;
+const day = 24 * hour;
+const week = 7 * day;
+const month = 30 * day;
+const year = 365 * day;
+const decade = 10 * year;
+const century = 10 * decade;
+
+test("CreateTimeTicks: nanoseconds below 100", () => {
+  runAndAssertTimeTicks(1, 1, ["1 nanosecond"]);
+  runAndAssertTimeTicks(3, 3, ["3 nanoseconds"]);
+  runAndAssertTimeTicks(4, 6, [
+    "4 nanoseconds",
+    "5 nanoseconds",
+    "6 nanoseconds",
+  ]);
+  runAndAssertTimeTicks(1, 100, ["1 nanosecond"]);
+  runAndAssertTimeTicks(1, 10, ["1 nanosecond"]);
+  runAndAssertTimeTicks(1, 9, ["1 nanosecond"]);
+  runAndAssertTimeTicks(2, 10, ["10 nanoseconds"]);
+  runAndAssertTimeTicks(2, 9, [
+    "2 nanoseconds",
+    "3 nanoseconds",
+    "4 nanoseconds",
+    "5 nanoseconds",
+    "6 nanoseconds",
+    "7 nanoseconds",
+    "8 nanoseconds",
+    "9 nanoseconds",
+  ]);
+});
+
+test("CreateTimeTicks: microseconds", () => {
+  runAndAssertTimeTicks(800, 1000, ["1 microsecond"]);
+  runAndAssertTimeTicks(800, 2000, ["1 microsecond"]);
+  runAndAssertTimeTicks(800, 11000, ["1 microsecond"]);
+  runAndAssertTimeTicks(800, 21000, ["1 microsecond"]);
+  runAndAssertTimeTicks(800, 111000, ["1 microsecond"]);
+  runAndAssertTimeTicks(1001, 21000, ["10 microseconds"]);
+  runAndAssertTimeTicks(10001, 21000, ["20 microseconds"]);
+  runAndAssertTimeTicks(10001, 30000, ["20 microseconds", "30 microseconds"]);
+});
+
+test("CreateTimeTicks: milliseconds", () => {
+  runAndAssertTimeTicks(800, 999999, ["1 microsecond"]);
+  runAndAssertTimeTicks(800, 1000000, ["1 microsecond", "1 millisecond"]);
+  runAndAssertTimeTicks(800000, 2000000, ["1 millisecond"]);
+  runAndAssertTimeTicks(800000, 11000000, ["1 millisecond"]);
+  runAndAssertTimeTicks(800000, 21000000, ["1 millisecond"]);
+  runAndAssertTimeTicks(800000, 111000000, ["1 millisecond"]);
+  runAndAssertTimeTicks(1000001, 111000000, ["100 milliseconds"]);
+});
+
+test("CreateTimeTicks: seconds", () => {
+  runAndAssertTimeTicks(800000, second - 1, ["1 millisecond"]);
+  runAndAssertTimeTicks(800000, second, ["1 millisecond", "1 second"]);
+  runAndAssertTimeTicks(800000000, 2 * second, ["1 second"]);
+  runAndAssertTimeTicks(800000000, 11 * second, ["1 second"]);
+  runAndAssertTimeTicks(800000000, 21 * second, ["1 second"]);
+  runAndAssertTimeTicks(800000000, 111 * second, ["1 second", "1 minute"]);
+  runAndAssertTimeTicks(second + 1, 111 * second, ["1 minute"]);
+});
+
+test("CreateTimeTicks: minutes", () => {
+  runAndAssertTimeTicks(second - 1, minute, ["1 second", "1 minute"]);
+  runAndAssertTimeTicks(minute - second, 2 * minute, ["1 minute"]);
+  runAndAssertTimeTicks(minute, 11 * minute, ["1 minute"]);
+  runAndAssertTimeTicks(minute + 1, 21 * minute, ["10 minutes"]);
+  runAndAssertTimeTicks(second, 111 * minute, [
+    "1 second",
+    "1 minute",
+    "1 hour",
+  ]);
+  runAndAssertTimeTicks(minute + 1, 111 * minute, ["1 hour"]);
+});
+
+test("CreateTimeTicks: hours", () => {
+  runAndAssertTimeTicks(minute - 1, hour, ["1 minute", "1 hour"]);
+  runAndAssertTimeTicks(hour - minute, 2 * hour, ["1 hour"]);
+  runAndAssertTimeTicks(hour, 11 * hour, ["1 hour"]);
+  runAndAssertTimeTicks(hour + 1, 21 * hour, ["10 hours"]);
+  runAndAssertTimeTicks(minute, 111 * hour, ["1 minute", "1 hour", "1 day"]);
+  runAndAssertTimeTicks(hour + 1, 111 * hour, ["1 day"]);
+});
+
+test("CreateTimeTicks: days", () => {
+  runAndAssertTimeTicks(hour - 1, day, ["1 hour", "1 day"]);
+  runAndAssertTimeTicks(day - hour, 2 * day, ["1 day"]);
+  runAndAssertTimeTicks(day, 11 * day, ["1 day", "1 week"]);
+  runAndAssertTimeTicks(day + 1, 21 * day, ["1 week"]);
+  runAndAssertTimeTicks(hour, 111 * day, [
+    "1 hour",
+    "1 day",
+    "1 week",
+    "1 month",
+  ]);
+  runAndAssertTimeTicks(day + 1, 111 * day, ["1 week", "1 month"]);
+});
+
+test("CreateTimeTicks: weeks", () => {
+  runAndAssertTimeTicks(day, week, ["1 day", "1 week"]);
+  runAndAssertTimeTicks(day + 1, week, ["1 week"]);
+  runAndAssertTimeTicks(day * 8, day * 27, ["2 weeks", "3 weeks"]);
+  runAndAssertTimeTicks(week - day, 2 * week, ["1 week"]);
+  runAndAssertTimeTicks(week, 11 * week, ["1 week", "1 month"]);
+  runAndAssertTimeTicks(week + 1, 35 * week, ["1 month"]);
+  runAndAssertTimeTicks(day, 111 * week, [
+    "1 day",
+    "1 week",
+    "1 month",
+    "1 year",
+  ]);
+  runAndAssertTimeTicks(week + 1, 111 * week, ["1 month", "1 year"]);
+});
+
+test("CreateTimeTicks: months", () => {
+  runAndAssertTimeTicks(week - 1, month, ["1 week", "1 month"]);
+  runAndAssertTimeTicks(month - 1, 2 * month, ["1 month"]);
+  runAndAssertTimeTicks(month, 11 * month, ["1 month"]);
+  runAndAssertTimeTicks(month, 12 * month, ["1 month"]);
+  runAndAssertTimeTicks(month, 12 * month + 5 * day, ["1 month", "1 year"]);
+  runAndAssertTimeTicks(month + 1, 12 * month, ["10 months"]);
+  // due to precision issues month + 1 == month
+  runAndAssertTimeTicks(month + hour, 10 * month - hour, [
+    "2 months",
+    "3 months",
+    "4 months",
+    "5 months",
+    "6 months",
+    "7 months",
+    "8 months",
+    "9 months",
+  ]);
+  runAndAssertTimeTicks(week, 111 * month, ["1 week", "1 month", "1 year"]);
+  runAndAssertTimeTicks(month + 1, 111 * month, ["1 year"]);
+});
+
+test("CreateTimeTicks: years", () => {
+  runAndAssertTimeTicks(month - 1, year, ["1 month", "1 year"]);
+  runAndAssertTimeTicks(year - month, 2 * year, ["1 year"]);
+  // due to precision issues year + 1 == year and decade - 1 == decade
+  runAndAssertTimeTicks(year + day, decade - day, [
+    "2 years",
+    "3 years",
+    "4 years",
+    "5 years",
+    "6 years",
+    "7 years",
+    "8 years",
+    "9 years",
+  ]);
+
+  runAndAssertTimeTicks(month, 111 * year, [
+    "1 month",
+    "1 year",
+    "1 decade",
+    "1 century",
+  ]);
+});
+
+test("CreateTimeTicks: decades", () => {
+  // due to precision issues year + 1 == year
+  runAndAssertTimeTicks(year + day, 21 * year, ["1 decade"]);
+  runAndAssertTimeTicks(year, decade, ["1 year", "1 decade"]);
+  runAndAssertTimeTicks(decade - year, 2 * decade, ["1 decade"]);
+  runAndAssertTimeTicks(year, 111 * decade, [
+    "1 year",
+    "1 decade",
+    "1 century",
+  ]);
+  // due to precision issues decade + 1 == decade
+  runAndAssertTimeTicks(decade + month, 111 * decade, ["1 century"]);
+});
+
+test("CreateTimeTicks: centuries", () => {
+  runAndAssertTimeTicks(decade - 1, century, ["1 decade", "1 century"]);
+  runAndAssertTimeTicks(century - decade, 2 * century, ["1 century"]);
+  runAndAssertTimeTicks(century, 11 * century, ["1 century"]);
+  runAndAssertTimeTicks(century + 1, 21 * century, ["1 century"]);
+  runAndAssertTimeTicks(decade, 111 * century, ["1 decade", "1 century"]);
+  runAndAssertTimeTicks(century + 1, 111 * century, ["1 century"]);
+});
+
+test("CreateTimeTicks: above centuries", () => {
+  runAndAssertTimeTicks(century + 30 * year, 3 * century, [
+    "2 centuries",
+    "3 centuries",
+  ]);
+  runAndAssertTimeTicks(century + 30 * year, century + 55 * year, [
+    "13 decades",
+    "14 decades",
+    "15 decades",
+  ]);
+  runAndAssertTimeTicks(2 * century + 32 * year, 2 * century + 36 * year, [
+    "232 years",
+    "233 years",
+    "234 years",
+    "235 years",
+    "236 years",
+  ]);
+});
+
+function getValues(ticks) {
+  return ticks.map((tick) => tick.value);
+}
+
+function getLabels(ticks) {
+  return ticks.map((tick) => tick.label);
+}
+
+function runAndAssertIntegerTicks(min, max, expected) {
+  const message = `min: ${min}, max: ${max}`;
+  assert.deepStrictEqual(
+    getValues(CreateIntegerTicks(min, max)),
+    expected,
+    message,
+  );
+}
+
+function runAndAssertTimeTicks(min, max, expected) {
+  const message = `min: ${min}, max: ${max}`;
+  assert.deepStrictEqual(
+    getLabels(CreateTimeTicks(min, max)),
+    expected,
+    message,
+  );
+}
diff --git a/pip/tests/test_interpreter.py b/pip/tests/test_interpreter.py
index e3ef98ae..87709a2a 100644
--- a/pip/tests/test_interpreter.py
+++ b/pip/tests/test_interpreter.py
@@ -199,7 +199,9 @@ def test_run_with_shots() -> None:
     e.interpret('operation Foo() : Unit { Message("Hello, world!"); }', callback)
     assert called == 0
 
-    value = e.run("Foo()", 5, callback)
+    value = []
+    for _ in range(5):
+        value.append(e.run("Foo()", callback))
     assert called == 5
 
     assert value == [None, None, None, None, None]
diff --git a/pip/tests/test_re.py b/pip/tests/test_re.py
index 748aa6d7..ace1e685 100644
--- a/pip/tests/test_re.py
+++ b/pip/tests/test_re.py
@@ -107,8 +107,14 @@ def test_qsharp_estimation_with_multiple_params() -> None:
     params.items[2].error_budget = 0.333
     assert params.as_dict() == {
         "items": [
-            {"qubitParams": {"name": "qubit_gate_us_e3"}, "errorBudget": 0.333},
-            {"qubitParams": {"name": "qubit_gate_us_e4"}, "errorBudget": 0.333},
+            {
+                "qubitParams": {"name": "qubit_gate_us_e3"},
+                "errorBudget": 0.333,
+            },
+            {
+                "qubitParams": {"name": "qubit_gate_us_e4"},
+                "errorBudget": 0.333,
+            },
             {
                 "qubitParams": {"name": "qubit_maj_ns_e6"},
                 "qecScheme": {"name": "floquet_code"},