Repository: microsoft/qsharp
Pull request #1769 (2024-07-22 22:48:16)

Fix accumulating perf impact of `qsharp.run` with may shots in Python
The existing imlementation of `qsharp.run` would pass the entry expression for every shot executed, causing it to be recompiling and building up large internal state due to incrementing node ids and the like. This updates the patterns to have the interpreter track the last execution graph it compiled such that a new API in Rust can re-`eval` that last expression without incurring any additional compilation.

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

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 333.1±2.64µs | 332.1±2.15µs | -0.30% |
| Array literal evaluation | **171.4±1.38µs** | 189.0±1.28µs | **+10.27%** |
| Array update evaluation | **405.5±1.78µs** | 410.3±4.11µs | **+1.18%** |
| Core + Standard library compilation | 23.8±0.97ms | **22.9±0.72ms** | **-3.78%** |
| Deutsch-Jozsa evaluation | **4.9±0.06ms** | 5.0±0.14ms | **+2.04%** |
| Large file parity evaluation | 34.8±0.33ms | 34.7±0.37ms | -0.29% |
| Large input file compilation | 14.1±0.52ms | **13.4±0.54ms** | **-4.96%** |
| Large input file compilation (interpreter) | 54.6±2.22ms | 53.0±1.98ms | -2.93% |
| Large nested iteration | **32.0±0.15ms** | 32.8±0.33ms | **+2.50%** |
| Perform Runtime Capabilities Analysis (RCA) on Deutsch-Jozsa sample | 1621.1±150.76µs | 1588.4±94.00µs | -2.02% |
| Perform Runtime Capabilities Analysis (RCA) on large file sample | 8.1±0.14ms | 8.0±0.14ms | -1.23% |
| Perform Runtime Capabilities Analysis (RCA) on teleport sample | 1442.3±49.03µs | 1468.8±131.68µs | +1.84% |
| Perform Runtime Capabilities Analysis (RCA) on the core and std libraries | 28.1±0.33ms | 28.3±0.30ms | +0.71% |
| Teleport evaluation | 91.3±3.69µs | 93.1±4.29µs | +1.97% |

  </details>
  

Patch:
diff --git a/build.py b/build.py
index e8f1ee25..ae188c9c 100755
--- a/build.py
+++ b/build.py
@@ -515,3 +515,17 @@ if build_pip and build_widgets and args.integration_tests:
             raise Exception(f"Error running {notebook}")
 
     step_end()
+
+    step_start("Running qsharp testing samples")
+    project_directories = [
+        dir for dir in os.walk(samples_src) if "qsharp.json" in dir[2]
+    ]
+
+    test_projects_directories = [ dir
+        for dir, _, _ in project_directories
+        if dir.find("testing") != -1
+    ]
+
+    for test_project_dir in test_projects_directories:
+        run_python_tests(test_project_dir, python_bin)
+    step_end()
diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs
index f6e72383..ba81890e 100644
--- a/compiler/qsc/src/interpret.rs
+++ b/compiler/qsc/src/interpret.rs
@@ -11,8 +11,6 @@ mod package_tests;
 #[cfg(test)]
 mod tests;
 
-use std::rc::Rc;
-
 pub use qsc_eval::{
     debug::Frame,
     output::{self, GenericReceiver},
@@ -52,7 +50,7 @@ use qsc_eval::{
     output::Receiver,
     val, Env, State, VariableInfo,
 };
-use qsc_fir::fir::{self, ExecGraphNode, Global, PackageStoreLookup};
+use qsc_fir::fir::{self, ExecGraph, Global, PackageStoreLookup};
 use qsc_fir::{
     fir::{Block, BlockId, Expr, ExprId, Package, PackageId, Pat, PatId, Stmt, StmtId},
     visit::{self, Visitor},
@@ -119,6 +117,8 @@ pub struct Interpreter {
     fir_store: fir::PackageStore,
     /// FIR lowerer
     lowerer: qsc_lowerer::Lowerer,
+    /// The execution graph for the last expression evaluated.
+    expr_graph: Option<ExecGraph>,
     /// The ID of the current package.
     /// This ID is valid both for the FIR store and the `PackageStore`.
     package: PackageId,
@@ -241,6 +241,7 @@ impl Interpreter {
             capabilities,
             fir_store,
             lowerer: qsc_lowerer::Lowerer::new().with_debug(dbg),
+            expr_graph: None,
             env: Env::default(),
             sim: sim_circuit_backend(),
             quantum_seed: None,
@@ -275,6 +276,7 @@ impl Interpreter {
             capabilities,
             fir_store,
             lowerer: qsc_lowerer::Lowerer::new(),
+            expr_graph: None,
             env: Env::default(),
             sim: sim_circuit_backend(),
             quantum_seed: None,
@@ -300,6 +302,7 @@ impl Interpreter {
         receiver: &mut impl Receiver,
     ) -> std::result::Result<Value, Vec<Error>> {
         let graph = self.get_entry_exec_graph()?;
+        self.expr_graph = Some(graph.clone());
         eval(
             self.source_package,
             self.classical_seed,
@@ -320,6 +323,7 @@ impl Interpreter {
         receiver: &mut impl Receiver,
     ) -> std::result::Result<Value, Vec<Error>> {
         let graph = self.get_entry_exec_graph()?;
+        self.expr_graph = Some(graph.clone());
         if self.quantum_seed.is_some() {
             sim.set_seed(self.quantum_seed);
         }
@@ -335,7 +339,7 @@ impl Interpreter {
         )
     }
 
-    fn get_entry_exec_graph(&self) -> std::result::Result<Rc<[ExecGraphNode]>, Vec<Error>> {
+    fn get_entry_exec_graph(&self) -> std::result::Result<ExecGraph, Vec<Error>> {
         let unit = self.fir_store.get(self.source_package);
         if unit.entry.is_some() {
             return Ok(unit.entry_exec_graph.clone());
@@ -393,6 +397,7 @@ impl Interpreter {
         increment: Increment,
     ) -> InterpretResult {
         let (graph, _) = self.lower(&increment)?;
+        self.expr_graph = Some(graph.clone());
 
         // Updating the compiler state with the new AST/HIR nodes
         // is not necessary for the interpreter to function, as all
@@ -405,7 +410,7 @@ impl Interpreter {
         eval(
             self.package,
             self.classical_seed,
-            graph.into(),
+            graph,
             self.compiler.package_store(),
             &self.fir_store,
             &mut self.env,
@@ -419,7 +424,7 @@ impl Interpreter {
     pub fn run(
         &mut self,
         receiver: &mut impl Receiver,
-        expr: &str,
+        expr: Option<&str>,
     ) -> std::result::Result<InterpretResult, Vec<Error>> {
         self.run_with_sim(&mut SparseSim::new(), receiver, expr)
     }
@@ -452,7 +457,7 @@ impl Interpreter {
         };
         let package = self.fir_store.get(self.package);
         let entry = ProgramEntry {
-            exec_graph: graph.into(),
+            exec_graph: graph,
             expr: (
                 self.package,
                 package
@@ -538,9 +543,15 @@ impl Interpreter {
         &mut self,
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
-        expr: &str,
+        expr: Option<&str>,
     ) -> std::result::Result<InterpretResult, Vec<Error>> {
-        let (graph, _) = self.compile_entry_expr(expr)?;
+        let graph = if let Some(expr) = expr {
+            let (graph, _) = self.compile_entry_expr(expr)?;
+            self.expr_graph = Some(graph.clone());
+            graph
+        } else {
+            self.expr_graph.clone().ok_or(vec![Error::NoEntryPoint])?
+        };
 
         if self.quantum_seed.is_some() {
             sim.set_seed(self.quantum_seed);
@@ -549,7 +560,7 @@ impl Interpreter {
         Ok(eval(
             self.package,
             self.classical_seed,
-            graph.into(),
+            graph,
             self.compiler.package_store(),
             &self.fir_store,
             &mut Env::default(),
@@ -568,11 +579,12 @@ impl Interpreter {
 
         let (package_id, graph) = if let Some(entry_expr) = entry_expr {
             // entry expression is provided
-            (self.package, self.compile_entry_expr(&entry_expr)?.0.into())
+            (self.package, self.compile_entry_expr(&entry_expr)?.0)
         } else {
             // no entry expression, use the entrypoint in the package
             (self.source_package, self.get_entry_exec_graph()?)
         };
+        self.expr_graph = Some(graph.clone());
 
         if self.quantum_seed.is_some() {
             sim.set_seed(self.quantum_seed);
@@ -593,8 +605,7 @@ impl Interpreter {
     fn compile_entry_expr(
         &mut self,
         expr: &str,
-    ) -> std::result::Result<(Vec<ExecGraphNode>, Option<PackageStoreComputeProperties>), Vec<Error>>
-    {
+    ) -> std::result::Result<(ExecGraph, Option<PackageStoreComputeProperties>), Vec<Error>> {
         let increment = self
             .compiler
             .compile_entry_expr(expr)
@@ -624,14 +635,13 @@ impl Interpreter {
     fn lower(
         &mut self,
         unit_addition: &qsc_frontend::incremental::Increment,
-    ) -> core::result::Result<(Vec<ExecGraphNode>, Option<PackageStoreComputeProperties>), Vec<Error>>
-    {
+    ) -> core::result::Result<(ExecGraph, Option<PackageStoreComputeProperties>), Vec<Error>> {
         if self.capabilities != TargetCapabilityFlags::all() {
             return self.run_fir_passes(unit_addition);
         }
 
         self.lower_and_update_package(unit_addition);
-        Ok((self.lowerer.take_exec_graph(), None))
+        Ok((self.lowerer.take_exec_graph().into(), None))
     }
 
     fn lower_and_update_package(&mut self, unit: &qsc_frontend::incremental::Increment) {
@@ -647,8 +657,7 @@ impl Interpreter {
     fn run_fir_passes(
         &mut self,
         unit: &qsc_frontend::incremental::Increment,
-    ) -> std::result::Result<(Vec<ExecGraphNode>, Option<PackageStoreComputeProperties>), Vec<Error>>
-    {
+    ) -> std::result::Result<(ExecGraph, Option<PackageStoreComputeProperties>), Vec<Error>> {
         self.lower_and_update_package(unit);
 
         let cap_results =
@@ -673,7 +682,7 @@ impl Interpreter {
         })?;
 
         let graph = self.lowerer.take_exec_graph();
-        Ok((graph, Some(compute_properties)))
+        Ok((graph.into(), Some(compute_properties)))
     }
 
     fn next_line_label(&mut self) -> String {
@@ -897,7 +906,7 @@ impl Debugger {
 fn eval(
     package: PackageId,
     classical_seed: Option<u64>,
-    exec_graph: Rc<[ExecGraphNode]>,
+    exec_graph: ExecGraph,
     package_store: &PackageStore,
     fir_store: &fir::PackageStore,
     env: &mut Env,
diff --git a/compiler/qsc_eval/src/lib.rs b/compiler/qsc_eval/src/lib.rs
index 79aa2e19..1ddedd90 100644
--- a/compiler/qsc_eval/src/lib.rs
+++ b/compiler/qsc_eval/src/lib.rs
@@ -36,9 +36,9 @@ use num_bigint::BigInt;
 use output::Receiver;
 use qsc_data_structures::{functors::FunctorApp, index_map::IndexMap, span::Span};
 use qsc_fir::fir::{
-    self, BinOp, CallableImpl, ExecGraphNode, Expr, ExprId, ExprKind, Field, FieldAssign, Global,
-    Lit, LocalItemId, LocalVarId, PackageId, PackageStoreLookup, PatId, PatKind, PrimField, Res,
-    StmtId, StoreItemId, StringComponent, UnOp,
+    self, BinOp, CallableImpl, ExecGraph, ExecGraphNode, Expr, ExprId, ExprKind, Field,
+    FieldAssign, Global, Lit, LocalItemId, LocalVarId, PackageId, PackageStoreLookup, PatId,
+    PatKind, PrimField, Res, StmtId, StoreItemId, StringComponent, UnOp,
 };
 use qsc_fir::ty::Ty;
 use qsc_lowerer::map_fir_package_to_hir;
@@ -199,10 +199,7 @@ impl Display for Spec {
 /// Utility function to identify a subset of a control flow graph corresponding to a given
 /// range.
 #[must_use]
-pub fn exec_graph_section(
-    graph: &Rc<[ExecGraphNode]>,
-    range: ops::Range<usize>,
-) -> Rc<[ExecGraphNode]> {
+pub fn exec_graph_section(graph: &ExecGraph, range: ops::Range<usize>) -> ExecGraph {
     let start: u32 = range
         .start
         .try_into()
@@ -227,7 +224,7 @@ pub fn exec_graph_section(
 pub fn eval(
     package: PackageId,
     seed: Option<u64>,
-    exec_graph: Rc<[ExecGraphNode]>,
+    exec_graph: ExecGraph,
     globals: &impl PackageStoreLookup,
     env: &mut Env,
     sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
@@ -439,7 +436,7 @@ struct Scope {
 }
 
 pub struct State {
-    exec_graph_stack: Vec<Rc<[ExecGraphNode]>>,
+    exec_graph_stack: Vec<ExecGraph>,
     idx: u32,
     idx_stack: Vec<u32>,
     val_register: Option<Value>,
@@ -453,11 +450,7 @@ pub struct State {
 
 impl State {
     #[must_use]
-    pub fn new(
-        package: PackageId,
-        exec_graph: Rc<[ExecGraphNode]>,
-        classical_seed: Option<u64>,
-    ) -> Self {
+    pub fn new(package: PackageId, exec_graph: ExecGraph, classical_seed: Option<u64>) -> Self {
         let rng = match classical_seed {
             Some(seed) => RefCell::new(StdRng::seed_from_u64(seed)),
             None => RefCell::new(StdRng::from_entropy()),
@@ -476,12 +469,7 @@ impl State {
         }
     }
 
-    fn push_frame(
-        &mut self,
-        exec_graph: Rc<[ExecGraphNode]>,
-        id: StoreItemId,
-        functor: FunctorApp,
-    ) {
+    fn push_frame(&mut self, exec_graph: ExecGraph, id: StoreItemId, functor: FunctorApp) {
         self.call_stack.push_frame(Frame {
             span: self.current_span,
             id,
diff --git a/compiler/qsc_fir/src/fir.rs b/compiler/qsc_fir/src/fir.rs
index 39c4b8ea..d07e327f 100644
--- a/compiler/qsc_fir/src/fir.rs
+++ b/compiler/qsc_fir/src/fir.rs
@@ -562,7 +562,7 @@ pub struct Package {
     /// The entry expression for an executable package.
     pub entry: Option<ExprId>,
     /// The control flow graph for the entry expression in the package.
-    pub entry_exec_graph: Rc<[ExecGraphNode]>,
+    pub entry_exec_graph: ExecGraph,
     /// The blocks in the package.
     pub blocks: IndexMap<BlockId, Block>,
     /// The expressions in the package.
@@ -881,7 +881,7 @@ pub struct SpecDecl {
     /// The input of the specialization.
     pub input: Option<PatId>,
     /// The flattened control flow graph for the execution of the specialization.
-    pub exec_graph: Rc<[ExecGraphNode]>,
+    pub exec_graph: ExecGraph,
 }
 
 impl Display for SpecDecl {
@@ -894,6 +894,9 @@ impl Display for SpecDecl {
     }
 }
 
+/// An execution graph represented by a reference counted vector of nodes.
+pub type ExecGraph = Rc<[ExecGraphNode]>;
+
 #[derive(Copy, Clone, Debug, PartialEq)]
 /// A node within the control flow graph.
 pub enum ExecGraphNode {
diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs
index c0981367..ee8f2349 100644
--- a/compiler/qsc_partial_eval/src/lib.rs
+++ b/compiler/qsc_partial_eval/src/lib.rs
@@ -30,10 +30,10 @@ use qsc_eval::{
 };
 use qsc_fir::{
     fir::{
-        self, BinOp, Block, BlockId, CallableDecl, CallableImpl, ExecGraphNode, Expr, ExprId,
-        ExprKind, Global, Ident, LocalVarId, Mutability, PackageId, PackageStore,
-        PackageStoreLookup, Pat, PatId, PatKind, Res, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind,
-        StoreBlockId, StoreExprId, StoreItemId, StorePatId, StoreStmtId, UnOp,
+        self, BinOp, Block, BlockId, CallableDecl, CallableImpl, ExecGraph, Expr, ExprId, ExprKind,
+        Global, Ident, LocalVarId, Mutability, PackageId, PackageStore, PackageStoreLookup, Pat,
+        PatId, PatKind, Res, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StoreBlockId, StoreExprId,
+        StoreItemId, StorePatId, StoreStmtId, UnOp,
     },
     ty::{Prim, Ty},
 };
@@ -127,7 +127,7 @@ impl Error {
 /// An entry to the program to be partially evaluated.
 pub struct ProgramEntry {
     /// The execution graph that corresponds to the entry expression.
-    pub exec_graph: Rc<[ExecGraphNode]>,
+    pub exec_graph: ExecGraph,
     /// The entry expression unique identifier within a package store.
     pub expr: fir::StoreExprId,
 }
@@ -2088,7 +2088,7 @@ impl<'a> PartialEvaluator<'a> {
         self.get_program_block_mut(self.eval_context.get_current_block_id())
     }
 
-    fn get_current_scope_exec_graph(&self) -> &Rc<[ExecGraphNode]> {
+    fn get_current_scope_exec_graph(&self) -> &ExecGraph {
         if let Some(spec_decl) = self.get_current_scope_spec_decl() {
             &spec_decl.exec_graph
         } else {
diff --git a/katas/content/random_numbers/index.md b/katas/content/random_numbers/index.md
index 365a5998..034bf512 100644
--- a/katas/content/random_numbers/index.md
+++ b/katas/content/random_numbers/index.md
@@ -16,14 +16,16 @@ The quantum algorithm for random number generation is one of the simplest applic
 
 **What you should know to start working on this kata:**
 
-- The concept of qubit and measurement
-- Single-qubit gates
+- Basic knowledge of qubits and measurements
+- Basic knowledge of single-qubit gates
+
+If you need a refresher on these topics, you can check out the previous katas.
 
 @[section]({"id": "random_numbers__introduction", "title": "Introduction"})
 
 Recall from the Qubit kata that a qubit state $\ket{\psi}$ is defined via the basis states $\ket{0}$ and $\ket{1}$ as $\ket{\psi} = \begin{bmatrix} \alpha \\ \beta \end{bmatrix} = \alpha\ket{0} + \beta\ket{1}$, where $|\alpha|^2 + |\beta|^2 = 1$.
 
-We call $\alpha$ and $\beta$ the probability amplitudes of states $\ket{0}$ and $\ket{1}$, respectively. When $\ket{\psi}$ is measured in the $\{\ket{0}, \ket{1}\}$ basis (the computational basis), the probabilities of the outcomes are defined based on the state amplitudes: there is a $|\alpha|^2$ probability that the measurement result will be $0$, and a $|\beta|^2$ probability that the measurement result will be $1$.
+$\alpha$ and $\beta$ are the probability amplitudes of states $\ket{0}$ and $\ket{1}$, respectively. When $\ket{\psi}$ is measured in the $\{\ket{0}, \ket{1}\}$ basis (the computational basis), the probabilities of the outcomes are defined based on the state amplitudes: there is a $|\alpha|^2$ probability that the measurement result will be $0$, and a $|\beta|^2$ probability that the measurement result will be $1$.
 
 > For example, a qubit in state $\begin{bmatrix} \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} \end{bmatrix}$ will yield measurement results $0$ or $1$ with equal probability, while a qubit in state $\begin{bmatrix} \frac{1}{2} \\ \frac{\sqrt3}{2} \end{bmatrix}$ will yield measurement result $0$ only 25% of the time, and $1$ 75% of the time.
 
diff --git a/katas/content/random_numbers/random_bit/solution.md b/katas/content/random_numbers/random_bit/solution.md
index 3a3258a1..6b322dc9 100644
--- a/katas/content/random_numbers/random_bit/solution.md
+++ b/katas/content/random_numbers/random_bit/solution.md
@@ -1,6 +1,6 @@
-The state of single qubit can be represented as a two-dimensional column vector $\begin{bmatrix} \alpha \\ \beta \end{bmatrix}$, where $\alpha$ and $\beta$ are complex numbers that satisfy $|\alpha|^2 + |\beta|^2 = 1$. When we measure the qubit, we get either 0 with probability $|\alpha|^2$ or 1 with probability $|\beta|^2$. Essentially we can control probability of measurement outcome by setting the right amplitudes of basis states. 
+The state of single qubit can be represented as a two-dimensional column vector $\begin{bmatrix} \alpha \\ \beta \end{bmatrix}$, where $\alpha$ and $\beta$ are complex numbers that satisfy $|\alpha|^2 + |\beta|^2 = 1$. When you measure the qubit, you get either 0 with probability $|\alpha|^2$ or 1 with probability $|\beta|^2$. Essentially, you can control probability of measurement outcome by setting the right amplitudes of basis states. 
 
-When we allocate the qubit in Q#, amplitudes $\alpha$ and $\beta$ are 1 and 0, respectively. Now our goal is set equal amplitudes for $\alpha$ and $\beta$ for absolute randomness. We can achieve that by simply applying Hadamard gate to the initial state $\ket{0}$:
+When you allocate the qubit in Q#, amplitudes $\alpha$ and $\beta$ are 1 and 0, respectively. Now your goal is to set equal amplitudes for $\alpha$ and $\beta$ for absolute randomness. You can achieve that by simply applying Hadamard gate to the initial state $\ket{0}$:
 
 $$
 H\ket{0} =
@@ -15,7 +15,7 @@ $$
 
 Now, both 0 and 1 measurement outcomes occur with equal probability of $|\frac{1}{\sqrt{2}}|^2 = \frac{1}{2}$.
 
-> Since the outcome probability is the square of the absolute value of amplitude, we will get the same distribution of outcomes by measuring the $\ket{-}$ state, which we can prepare by applying the Hadamard gate to the basis state $\ket{1}$. Try it out to achieve the stretch goal!
+> Since the outcome probability is the square of the absolute value of amplitude, you'll get the same distribution of outcomes by measuring the $\ket{-}$ state, which you can prepare by applying the Hadamard gate to the basis state $\ket{1}$. Try it out to achieve the stretch goal!
 
 @[solution]({
     "id": "random_numbers__random_bit_solution",
diff --git a/katas/content/random_numbers/random_n_bits/solution.md b/katas/content/random_numbers/random_n_bits/solution.md
index 34cdae85..c956cc2b 100644
--- a/katas/content/random_numbers/random_n_bits/solution.md
+++ b/katas/content/random_numbers/random_n_bits/solution.md
@@ -1,6 +1,6 @@
 Let's reuse the `RandomBit` operation from the "Generate A Single Random Bit" exercise again.
-We'll generate $N$ random bits by calling `RandomBit` operation $N$ times, and treat the result as a binary notation of the integer we're looking for.
-Since the maximum value of the number written with $N$ bits is $2^N - 1$, we don't need to do any extra checks to ensure that the result is within the given range.
+You'll generate $N$ random bits by calling `RandomBit` operation $N$ times, and treat the result as a binary notation of the integer you're looking for.
+Since the maximum value of the number written with $N$ bits is $2^N - 1$, you don't need to do any extra checks to ensure that the result is within the given range.
 
 @[solution]({
     "id": "random_numbers__random_n_bits_solution",
diff --git a/katas/content/random_numbers/random_number/index.md b/katas/content/random_numbers/random_number/index.md
index 56e6fd24..9da603bd 100644
--- a/katas/content/random_numbers/random_number/index.md
+++ b/katas/content/random_numbers/random_number/index.md
@@ -1,4 +1,4 @@
-In the "Generate A Number Of Arbitrary Size" exercise, we generated numbers in the range $[0, 2^N-1]$ $(1 \leq N \leq 10)$. Now let's create an operation that will return a random number in the range $[min, max]$. 
+In the "Generate A Number Of Arbitrary Size" exercise, you generated numbers in the range $[0, 2^N-1]$ $(1 \leq N \leq 10)$. Now let's create an operation that will return a random number in the range $[min, max]$. 
 
 **Input:** 
 Two integers $min$ and $max$ ($0 \leq min \leq max \leq 2^{10}-1$).
diff --git a/katas/content/random_numbers/random_number/solution.md b/katas/content/random_numbers/random_number/solution.md
index ca44b453..5f112e45 100644
--- a/katas/content/random_numbers/random_number/solution.md
+++ b/katas/content/random_numbers/random_number/solution.md
@@ -1,6 +1,6 @@
-We can reuse the `RandomBit` and `RandomNBits` operations from earlier exercises.
+You can reuse the `RandomBit` and `RandomNBits` operations from earlier exercises.
 
-We'll generate an $N$-bit random number by calling the `RandomNBits` operation, where $N$ is the bitsize of $max - min$. We can repeat this process until the result is less than or equal than $max - min$, and return that number plus $min$.
+You'll generate an $N$-bit random number by calling the `RandomNBits` operation, where $N$ is the bitsize of $max - min$. You can repeat this process until the result is less than or equal than $max - min$, and return that number plus $min$.
 
 @[solution]({
     "id": "random_numbers__random_number_solution",
diff --git a/katas/content/random_numbers/random_two_bits/solution.md b/katas/content/random_numbers/random_two_bits/solution.md
index 81171cec..d485b14c 100644
--- a/katas/content/random_numbers/random_two_bits/solution.md
+++ b/katas/content/random_numbers/random_two_bits/solution.md
@@ -1,5 +1,5 @@
 Let's reuse the `RandomBit` operation from the "Generate A Single Random Bit" exercise.
-We can generate two random bits by calling the `RandomBit` operation twice, multiply the most significant bit by 2 and add the least significant bit to generate a random two-bit number.
+You can generate two random bits by calling the `RandomBit` operation twice, multiply the most significant bit by 2 and add the least significant bit to generate a random two-bit number.
 
 @[solution]({
     "id": "random_numbers__random_two_bits_solution",
diff --git a/katas/content/random_numbers/weighted_random_bit/index.md b/katas/content/random_numbers/weighted_random_bit/index.md
index 1be6af4e..319613e2 100644
--- a/katas/content/random_numbers/weighted_random_bit/index.md
+++ b/katas/content/random_numbers/weighted_random_bit/index.md
@@ -1,6 +1,6 @@
 In each of the above exercises, all generated numbers were equally likely. Now let's create an operation that will return a random bit with different probabilities of outcomes. 
 
-> Remember that by setting the amplitudes of basis states $\alpha$ and $\beta$, we can control the probability of getting measurement outcomes $0$ and $1$ when the qubit is measured.
+> Remember that by setting the amplitudes of basis states $\alpha$ and $\beta$, you can control the probability of getting measurement outcomes $0$ and $1$ when the qubit is measured.
 
 **Input:** 
 A floating-point number $x$, $0 \le x \le 1$. 
diff --git a/katas/content/random_numbers/weighted_random_bit/solution.md b/katas/content/random_numbers/weighted_random_bit/solution.md
index daf04a04..2720c824 100644
--- a/katas/content/random_numbers/weighted_random_bit/solution.md
+++ b/katas/content/random_numbers/weighted_random_bit/solution.md
@@ -7,24 +7,24 @@ $$
 
 Here, $\theta$ is the angle between the state vector and the $Z$-axis, and $\phi$ is the longitude angle with respect to the $X$-axis on the Bloch sphere.
 
-Our goal is to generate $0$ or $1$ with the probability of generating a $0$ equal to $x$ and the probability of generating a $1$ equal to $1 - x$. This means that the qubit state should look like
+Your goal is to generate $0$ or $1$ with the probability of generating a $0$ equal to $x$ and the probability of generating a $1$ equal to $1 - x$. This means that the qubit state should look like this:
 
 $$
 \ket{\psi} =
     \sqrt x \ket{0 } + \sqrt{1 - x} \ket{1}
 $$
 
-Comparing the amplitudes of the state $\ket{0 }$ in the two equations, we get
+Comparing the amplitudes of the state $\ket{0 }$ in the two equations, you get
 
 $$
 \sqrt x = \cos \frac{\theta}{2} \Rightarrow \theta = 2 \arccos\sqrt x
 $$
 
-Since $\theta$ is the angle between the state vector and the $Z$-axis, we need to apply the $Ry$ gate with the calculated $\theta$ to the starting state $\ket{0 }$ to get the desired qubit state.
+Since $\theta$ is the angle between the state vector and the $Z$-axis, you need to apply the $Ry$ gate with the calculated $\theta$ to the starting state $\ket{0 }$ to get the desired qubit state.
 
-The $Ry$ gate applies a given rotation about the $Y$-axis (i.e., in the $ZX$-plane). Hence, $\phi$ (longitude angle with respect to $X$-axis) is always equal to $0^{\circ}$, which means that the relative phase $e^{i\phi}$ doesn't have any impact on the resulting qubit state.
+The $Ry$ gate applies a given rotation about the $Y$-axis, that is, in the $ZX$-plane. Hence, $\phi$ (longitude angle with respect to $X$-axis) is always equal to $0^{\circ}$, which means that the relative phase $e^{i\phi}$ doesn't have any impact on the resulting qubit state.
 
-> We can also calculate ${\theta}$ by comparing the amplitudes of the state $\ket{1 }$ in the two equations, which is $2 \arcsin\sqrt{1.0 - x}$. The results will be equivalent.
+> You can also calculate ${\theta}$ by comparing the amplitudes of the state $\ket{1 }$ in the two equations, which is $2 \arcsin\sqrt{1.0 - x}$. The results will be equivalent.
 
 @[solution]({
     "id": "random_numbers__weighted_random_bit_solution",
diff --git a/katas/content/single_qubit_gates/amplitude_change/solution.md b/katas/content/single_qubit_gates/amplitude_change/solution.md
index 561d1c2a..589ea6b8 100644
--- a/katas/content/single_qubit_gates/amplitude_change/solution.md
+++ b/katas/content/single_qubit_gates/amplitude_change/solution.md
@@ -1,4 +1,4 @@
-We can recognize that we need to use one of the rotation gates Rx, Ry, and Rz (named because they "rotate" the qubit state in the three dimensional space visualized as the Bloch sphere about the x, y, and z axes, respectively), since they involve angle parameters. Of these three gates, only Ry rotates the basis states $\ket{0}$ and $\ket{1}$ to have real amplitudes (the other two gates introduce complex coefficients).
+You can recognize that you need to use one of the rotation gates Rx, Ry, and Rz (named because they "rotate" the qubit state in the three dimensional space visualized as the Bloch sphere about the x, y, and z axes, respectively), since they involve angle parameters. Of these three gates, only Ry rotates the basis states $\ket{0}$ and $\ket{1}$ to have real amplitudes (the other two gates introduce complex coefficients).
 
 As a reminder,
 
@@ -20,8 +20,8 @@ R_y(\theta)\ket{0} =
 $$
 
 Recall that when applying a gate, you can tell what its matrix does to the basis states by looking at its columns: the first column of the matrix is the state into which it will transform the $\ket{0}$ state, and the second column is the state into which it will transform the $\ket{1}$ state.
-One of the examples used by the testing harness has $\beta = 0.6, \gamma = 0.8$ and $\alpha = \frac{\pi}{3} = 1.0472$.
-Since $\cos \frac{\pi}{3} = 0.5$ and $\sin \frac{\pi}{3} = 0.8660$, working to 4 decimal places, we can use $\frac{\theta}{2} = \alpha$ to compute:
+One of the examples used by the testing harness has $\beta = 0.6, \gamma = 0.8$ and $\alpha = \frac{\pi}{3} ≈ 1.0472$.
+Since $\cos \frac{\pi}{3} = 0.5$ and $\sin \frac{\pi}{3} = 0.8660$, working to 4 decimal places, you can use $\frac{\theta}{2} = \alpha$ to compute:
 
 $$
 R_{y}(\theta) \ket{\psi}=
@@ -33,7 +33,7 @@ R_{y}(\theta) \ket{\psi}=
 \begin{bmatrix}-0.3928\\ 0.9196\\ \end{bmatrix}
 $$
 
-Notice that we used $\frac{\theta}{2} = \alpha$; this means that in the Q# code we need to pass the angle $\theta = 2\alpha$.
+Notice that $\frac{\theta}{2} = \alpha$; this means that in the Q# code you need to pass the angle $\theta = 2\alpha$.
 
 @[solution]({
 "id": "single_qubit_gates__amplitude_change_solution",
diff --git a/katas/content/single_qubit_gates/basis_change/solution.md b/katas/content/single_qubit_gates/basis_change/solution.md
index 3f53e0b1..6de57645 100644
--- a/katas/content/single_qubit_gates/basis_change/solution.md
+++ b/katas/content/single_qubit_gates/basis_change/solution.md
@@ -1,4 +1,4 @@
-We can recognize that the Hadamard gate changes states $\ket{0}$ and $\ket{1}$ to $\ket{+}$ and $\ket{-}$, respectively, and vice versa.
+You can recognize that the Hadamard gate changes states $\ket{0}$ and $\ket{1}$ to $\ket{+}$ and $\ket{-}$, respectively, and vice versa.
 
 As a reminder, the Hadamard gate is defined by the following matrix:
 
@@ -6,7 +6,7 @@ $$
 \frac{1}{\sqrt{2}}\begin{bmatrix}1 & 1 \\1 & -1\end{bmatrix}
 $$
 
-For example, we can work out $H\ket{1}$ as follows:
+For example, you can work out $H\ket{1}$ as follows:
 
 $$
 H\ket{1}=
@@ -17,7 +17,7 @@ H\ket{1}=
 \frac{1}{\sqrt{2}} \big(\ket{0} - \ket{1}\big) = \ket{-}
 $$
 
-Similarly, we can consider the effect of the Hadamard gate on the superposition state $\ket{\psi} = 0.6\ket{0} + 0.8\ket{1}$ (rounding the numbers to 4 decimal places):
+Similarly, you can consider the effect of the Hadamard gate on the superposition state $\ket{\psi} = 0.6\ket{0} + 0.8\ket{1}$ (rounding the numbers to 4 decimal places):
 
 $$
 H|\psi⟩ =
diff --git a/katas/content/single_qubit_gates/complex_phase/solution.md b/katas/content/single_qubit_gates/complex_phase/solution.md
index 3455033b..d18f534d 100644
--- a/katas/content/single_qubit_gates/complex_phase/solution.md
+++ b/katas/content/single_qubit_gates/complex_phase/solution.md
@@ -1,11 +1,11 @@
-We know that:
+You know that:
 
 $$
 R1(\alpha)=
  \begin{bmatrix}1 & 0 \\ 0 & \color{red}{e^{i\alpha}}\end{bmatrix}
 $$
 
-So we have:
+So you have:
 
 $$
 R1(\beta \ket{0} + \gamma \ket{1}) =
@@ -18,8 +18,8 @@ $$
 
 > Suppose now that $\alpha = \frac{\pi}{2}$.
 > Then $e^{i\alpha}= \cos\frac{\pi}{2} + i\sin\frac{\pi}{2}$.
-> And, since $\cos\frac{\pi}{2}= 0$ and $\sin\frac{\pi}{2} = 1$, then we have that $\cos\frac{\pi}{2} + i \sin\frac{\pi}{2} = i$, and  
-> $R1(\frac{\pi}{2}) = S$, which we used in the second solution to the task "Relative Phase i".
+> And, since $\cos\frac{\pi}{2}= 0$ and $\sin\frac{\pi}{2} = 1$, then you have that $\cos\frac{\pi}{2} + i \sin\frac{\pi}{2} = i$, and  
+> $R1(\frac{\pi}{2}) = S$, which you used in the second solution to the task "Relative Phase i".
 
 @[solution]({
     "id": "single_qubit_gates__complex_phase_solution",
diff --git a/katas/content/single_qubit_gates/global_phase_i/solution.md b/katas/content/single_qubit_gates/global_phase_i/solution.md
index bfbd3d0f..e8a448e2 100644
--- a/katas/content/single_qubit_gates/global_phase_i/solution.md
+++ b/katas/content/single_qubit_gates/global_phase_i/solution.md
@@ -1,12 +1,12 @@
-We need to apply a gate which applies a global phase of $i$, i.e. $\ket{\psi} \rightarrow i\ket{\psi}$.
+You need to apply a gate which applies a global phase of $i$, that is,  $\ket{\psi} \rightarrow i\ket{\psi}$.
 The matrix representation of such a gate is $\begin{bmatrix} i & 0 \\ 0 & i \end{bmatrix} = i\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} = iI$.
-Since we are restricted to the Pauli gates, we use the property that a product of any two distinct Pauli gates equals the third gate with a $+i$ or a $-i$ global phase, therefore the product of all three Pauli gates is $XYZ = iI$.
+Since you're restricted to the Pauli gates, you can use the property that a product of any two distinct Pauli gates equals the third gate with a $+i$ or a $-i$ global phase, therefore the product of all three Pauli gates is $XYZ = iI$.
 $$
 \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}\begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix}\begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} = 
 \begin{bmatrix} i & 0 \\ 0 & i \end{bmatrix}
 $$
 
-> Remember the rightmost gates in mathematical notation are applied first in Q# code. Hence we first apply the $Z$ gate, followed by the $Y$ gate, and finally the $X$ gate.
+> Remember the rightmost gates in mathematical notation are applied first in Q# code. Hence you first apply the $Z$ gate, followed by the $Y$ gate, and finally the $X$ gate.
 
 @[solution]({
     "id": "single_qubit_gates__global_phase_i_solution",
diff --git a/katas/content/single_qubit_gates/global_phase_minusone/index.md b/katas/content/single_qubit_gates/global_phase_minusone/index.md
index 9d4d763f..0ed2b5b8 100644
--- a/katas/content/single_qubit_gates/global_phase_minusone/index.md
+++ b/katas/content/single_qubit_gates/global_phase_minusone/index.md
@@ -2,6 +2,6 @@
 
 **Goal**: Change the state of the qubit to $- \beta \ket{0} - \gamma \ket{1}$.
 
-> This change on its own is not observable - there is no experiment you can do on a standalone qubit to figure out whether it acquired the global phase or not.
+> This change on its own is not observable - there's no experiment you can do on a standalone qubit to figure out whether it acquired the global phase or not.
 > However, you can use a controlled version of this operation to observe the global phase it introduces.
 > This is used in later katas as part of more complicated tasks.
diff --git a/katas/content/single_qubit_gates/global_phase_minusone/solution.md b/katas/content/single_qubit_gates/global_phase_minusone/solution.md
index 5b15cb43..93cffd61 100644
--- a/katas/content/single_qubit_gates/global_phase_minusone/solution.md
+++ b/katas/content/single_qubit_gates/global_phase_minusone/solution.md
@@ -1,10 +1,10 @@
-A global phase is a phase factor that multiplies the entire quantum state. It is not observable when measuring the qubit's state, as the probabilities remain unchanged. However, it is significant when considering quantum state transformations.
+A global phase is a phase factor that multiplies the entire quantum state. It isn't observable when measuring the qubit's state, as the probabilities remain unchanged. However, it's significant when considering quantum state transformations.
 
 Our task is to implement an operation that transforms the given qubit state from $\ket{\psi} = \beta \ket{0} + \gamma \ket{1}$ to $- \beta \ket{0} - \gamma \ket{1}$.
 
-To do that, we utilize a sequence of gates. The Pauli Z gate followed by the Pauli X gate can be used to achieve this effect when applied in succession twice.
+To do that, you use a sequence of gates. The Pauli Z gate followed by the Pauli X gate can be used to achieve this effect when applied in succession twice.
 
-1. **Apply the Pauli Z gate**: The Z gate multiplies the $\ket{1}$ state by $-1$ but does not change the $\ket{0}$ state, converting our state $\beta \ket{0} + \gamma \ket{1}$ to $\beta \ket{0} - \gamma \ket{1}$.
+1. **Apply the Pauli Z gate**: The Z gate multiplies the $\ket{1}$ state by $-1$ but doen't change the $\ket{0}$ state, converting our state $\beta \ket{0} + \gamma \ket{1}$ to $\beta \ket{0} - \gamma \ket{1}$.
 
    The matrix representation of the Z gate is:
 
@@ -13,7 +13,7 @@ To do that, we utilize a sequence of gates. The Pauli Z gate followed by the Pau
    \begin{bmatrix}1 & 0 \\ 0 & -1 \end{bmatrix}
    $$
 
-2. **Apply the Pauli X gate**: The X gate flips the $\ket{0}$ and $\ket{1}$ basis states, converting our $\beta \ket{0} - \gamma \ket{1}$ state to $\beta \ket{1} - \gamma \ket{0}$.
+2. **Apply the Pauli X gate**: The X gate flips the $\ket{0}$ and $\ket{1}$ basis states, converting $\beta \ket{0} - \gamma \ket{1}$ state to $\beta \ket{1} - \gamma \ket{0}$.
 
    The matrix representation of the X gate is:
 
@@ -22,7 +22,7 @@ To do that, we utilize a sequence of gates. The Pauli Z gate followed by the Pau
    \begin{bmatrix}0 & 1 \\ 1 & 0\end{bmatrix}
    $$
 
-3. **Repeat the Z and X gates**: Applying the Z gate again will multiply the $\ket{1}$ state (that used to be $\ket{0}$), converting our state $\beta \ket{1} - \gamma \ket{0}$ to $- \beta \ket{1} - \gamma \ket{0}$. Finally, the second X gate will restore the original basis states, but now with both amplitudes having acquired an additional phase of $-1$. This means our state has been multiplied by $-1$, achieving the required global phase change.
+3. **Repeat the Z and X gates**: Applying the Z gate again will multiply the $\ket{1}$ state (that used to be $\ket{0}$), converting the state $\beta \ket{1} - \gamma \ket{0}$ to $- \beta \ket{1} - \gamma \ket{0}$. Finally, the second X gate will restore the original basis states, but now with both amplitudes having acquired an additional phase of $-1$. This means the state has been multiplied by $-1$, achieving the required global phase change.
 
 @[solution]({
 "id": "single_qubit_gates__global_phase_minusone_solution",
diff --git a/katas/content/single_qubit_gates/index.md b/katas/content/single_qubit_gates/index.md
index e2796660..04d3acee 100644
--- a/katas/content/single_qubit_gates/index.md
+++ b/katas/content/single_qubit_gates/index.md
@@ -15,15 +15,17 @@ This kata introduces you to single-qubit gates. Quantum gates are the quantum co
 
 **What you should know to start working on this kata:**
 
-- Basic linear algebra
+- Basic knowledge of linear algebra
 - The concept of qubit
 
+If you need a refresher on these topics, you can check out the previous katas.
+
 @[section]({
     "id": "single_qubit_gates__basics",
     "title": "The Basics"
 })
 
-There are certain properties common to all quantum gates. This section will introduce those properties, using the $X$ gate as an example.
+There are certain properties common to all quantum gates. This section introduces those properties, using the $X$ gate as an example.
 
 ## Matrix Representation
 
@@ -74,7 +76,7 @@ Matrix multiplication is associative, so this is equivalent to multiplying the $
 
 >Note that matrix multiplication isn’t commutative, thus $(BA) \neq (AB)$.
 
-All quantum gates are reversible - there is another gate which will undo any given gate's transformation, returning the qubit to its original state.
+All quantum gates are reversible, that is, there exists another gate which will undo any given gate's transformation, returning the qubit to its original state.
 This means that when dealing with quantum gates, information about qubit states is never lost, as opposed to classical logic gates, some of which destroy information.
 Quantum gates are represented by unitary matrices, so the inverse of a gate is its adjoint; these terms are also used interchangeably in quantum computing.
 
@@ -84,7 +86,7 @@ There is a simple way to find out what a gate does to the two computational basi
 
 $$A = \begin{bmatrix} \epsilon & \zeta \\ \eta & \mu \end{bmatrix}$$
 
-Watch what happens when we apply it to these states:
+Watch what happens when applying $A$ to these states:
 
 $$
 A\ket{0} =
@@ -102,7 +104,7 @@ A\ket{1} =
 \begin{bmatrix} \zeta \\ \mu \end{bmatrix} = \zeta\ket{0} + \mu\ket{1}
 $$
 
-Notice that applying the gate to the $\ket{0}$ state transforms it into the state written as the first column of the gate's matrix. Likewise, applying the gate to the $\ket{1}$ state transforms it into the state written as the second column. This holds true for any quantum gate, including, of course, the $X$ gate:
+Notice that applying the $A$ gate to the $\ket{0}$ state transforms it into the state written as the first column of the gate's matrix. Likewise, applying the $A$ gate to the $\ket{1}$ state transforms it into the state written as the second column. This holds true for any quantum gate, including, of course, the $X$ gate:
 
 $$X = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}$$
 
@@ -126,7 +128,7 @@ That is, applying a gate to a qubit in superposition is equivalent to applying t
     "title": "Ket-Bra Representation"
 })
 
-There is another way to represent quantum gates, this time using Dirac notation. However, the kets we've been using aren't enough to represent arbitrary matrices. We need to introduce another piece of notation: the **bra** (this is why Dirac notation is sometimes called **bra-ket notation**).
+There is another way to represent quantum gates, this time using Dirac notation. However, kets aren't enough to represent arbitrary matrices. An additional notation is required: the **bra** (this is why Dirac notation is sometimes called **bra-ket notation**).
 
 Recall that kets represent column vectors; a bra is a ket's row vector counterpart. For any ket $\ket{\psi}$, the corresponding bra is its adjoint (conjugate transpose): $\bra{\psi} = \ket{\psi}^\dagger$.
 
@@ -161,7 +163,7 @@ Some examples:
 
 Kets and bras give us a neat way to express inner and outer products. The inner product of $\ket{\phi}$ and $\ket{\psi}$ is the matrix product of $\bra{\phi}$ and $\ket{\psi}$, denoted as $\braket{\phi|\psi}$, and their outer product is the matrix product of $\ket{\phi}$ and $\bra{\psi}$, denoted as $\ket{\phi}\bra{\psi}$. Notice that the norm of $\ket{\psi}$ is $\sqrt{\braket{\psi|\psi}}$.
 
-This brings us to representing matrices. Recall that the outer product of two vectors of the same size produces a square matrix. We can use a linear combination of several outer products of simple vectors (such as basis vectors) to express any square matrix. For example, the $X$ gate can be expressed as follows:
+This leads to the representation of matrices via outer products. Recall that the outer product of two vectors of the same size produces a square matrix. You can use a linear combination of several outer products of simple vectors (such as basis vectors) to express any square matrix. For example, the $X$ gate can be expressed as follows:
 
 $$X = \ket{0}\bra{1} + \ket{1}\bra{0}$$
 
@@ -177,7 +179,7 @@ This representation can be used to carry out calculations in Dirac notation with
 
 $$X\ket{0} = \big(\ket{0}\bra{1} + \ket{1}\bra{0}\big)\ket{0} = \ket{0}\braket{1|0} + \ket{1}\braket{0|0} = \ket{0}\big(\braket{1|0}\big) + \ket{1}\big(\braket{0|0}\big) = \ket{0}(0) + \ket{1}(1) = \ket{1}$$
 
-> That last step may seem a bit confusing. Recall that $\ket{0}$ and $\ket{1}$ form an **orthonormal basis**. That is, they are both normalized, and they are orthogonal to each other.
+> That last step may seem a bit confusing. Recall that $\ket{0}$ and $\ket{1}$ form an **orthonormal basis**. That is, they're both normalized, and they're orthogonal to each other.
 >
 > A vector is normalized if its norm is equal to $1$, which only happens if its inner product with itself is equal to $1$. This means that $\braket{0|0} = \braket{1|1} = 1$
 >
@@ -193,7 +195,7 @@ $$A = a_{00} \ket{0}\bra{0} + a_{01} \ket{0}\bra{1} + a_{10} \ket{1}\bra{0} + a_
     "title": "Ket-Bra Decomposition"
 })
 
-This section describes a more formal process of finding the ket-bra decompositions of quantum gates. This section is not necessary to start working with quantum gates, so feel free to skip it for now, and come back to it later.
+This section describes a more formal process of finding the ket-bra decompositions of quantum gates. This section isn't necessary to start working with quantum gates, so feel free to skip it for now, and come back to it later.
 
 You can use the properties of _eigenvalues_ and _eigenvectors_ to find the ket-bra decomposition of any gate. Given a gate $A$ and the orthogonal vectors $\ket{\phi}$ and $\ket{\psi}$, if:
 
@@ -221,7 +223,7 @@ $$= \ket{0}\bra{1} + \ket{1}\bra{0}$$
     "title": "Pauli Gates"
 })
 
-This section introduces some of the common single-qubit gates, including their matrix form, their ket-bra decomposition, and a brief "cheatsheet" listing their effect on some common qubit states.
+This section introduces some of the common single-qubit gates, including their matrix form, their ket-bra decomposition, and a brief "cheat sheet" listing their effect on some common qubit states.
 
 You can use a tool called <a href="https://algassert.com/quirk" target="_blank">Quirk</a> to visualize how these gates interact with various qubit states.
 
@@ -312,16 +314,16 @@ Here are several properties of the Pauli gates that are easy to verify and conve
 
 The following example contains code demonstrating how to apply gates in Q#. It sets up a series of quantum states, and then shows the result of applying the $X$ gate to each one.
 
-In the previous kata we discussed that qubit state in Q# cannot be directly assigned or accessed. The same logic is extended to quantum gates: applying a gate to a qubit modifies the internal state of that qubit, but doesn't return the resulting state of the qubit. This is why we never assign the output of these gates to any variables in this demo - they don't produce any output.
+The previous kata mentioned that qubit state in Q# cannot be directly assigned or accessed. The same logic is extended to quantum gates: applying a gate to a qubit modifies the internal state of that qubit but doesn't return the resulting state of the qubit. That's the reason why you never assign the output of these gates to any variables in this demo - they don't produce any output.
 
-The same principle applies to applying several gates in a row to a qubit. In the mathematical notation, applying an $X$ gate followed by a $Z$ gate to a state $\ket{\psi}$ is denoted as $Z(X(\ket{\psi}))$, because the result of applying a gate to a state is another state. In Q#, applying a gate doesn't return anything, so you can't use its output as an input to another gate - something like `Z(X(q))` will not produce the expected result. Instead, to apply several gates to the same qubit, you need to call them separately in the order in which they are applied:
+The same principle applies to applying several gates in a row to a qubit. In the mathematical notation, applying an $X$ gate followed by a $Z$ gate to a state $\ket{\psi}$ is denoted as $Z(X(\ket{\psi}))$, because the result of applying a gate to a state is another state. In Q#, applying a gate doesn't return anything, so you can't use its output as an input to another gate - something like `Z(X(q))` won't produce the expected result. Instead, to apply several gates to the same qubit, you need to call them separately in the order in which they're applied:
 
 ```qsharp
 X(q);
 Z(q);
 ```
 
-All the basic gates we will be covering in this kata are part of the Intrinsic namespace. We're also using the function DumpMachine to print the state of the quantum simulator.
+All the basic gates covered in this kata are part of the Intrinsic namespace. Additionally, the function `DumpMachine` from the Diagnostics namespace is used to print the state of the quantum simulator.
 
 @[example]({"id": "single_qubit_gates__pauli_gates_in_qsharp_demo", "codePath": "./examples/PauliGates.qs"})
 
@@ -367,7 +369,7 @@ All the basic gates we will be covering in this kata are part of the Intrinsic n
     "title": "Identity Gate"
 })
 
-The identity gate is mostly here for completeness, at least for now. It will come in handy when dealing with multi-qubit systems and multi-qubit gates. It is represented by the identity matrix, and does not affect the state of the qubit.
+The identity gate is mostly here for completeness, at least for now. It will come in handy when dealing with multi-qubit systems and multi-qubit gates. It's represented by the identity matrix, and doesn't affect the state of the qubit.
 
 <table>
 <tr>
@@ -435,7 +437,7 @@ $H\ket{-i} = e^{-i\pi/4}\ket{i} $ <br>
     "title": "Phase Shift Gates"
 })
 
-The next two gates are known as phase shift gates. They apply a phase to the $\ket{1}$ state, and leave the $\ket{0}$ state unchanged.
+The next two gates are known as **phase shift gates**. They apply a phase to the $\ket{1}$ state, and leave the $\ket{0}$ state unchanged.
 
 <table>
   <tr>
@@ -496,8 +498,8 @@ These gates are the $X$ rotation gate $R_x(\theta)$, $Y$ rotation gate $R_y(\the
 Note that for the first three gates the parameter $\theta$ is multiplied by $\frac{1}{2}$ within the gate's matrix.
 
 > These gates are known as rotation gates, because they represent rotations around various axes on the Bloch sphere. The Bloch sphere is a way of representing the qubit states visually, mapping them onto the surface of a sphere.
-> Unfortunately, this visualization isn't very useful beyond single-qubit states, which is why we have opted not to go into details in this kata.
-> If you are curious about it, you can learn more in <a href="https://en.wikipedia.org/wiki/Bloch_sphere" target="_blank">this Wikipedia article</a>.
+> Unfortunately, this visualization isn't very useful beyond single-qubit states, which is why this kata doesn't go into details.
+> If you're curious about it, you can learn more in <a href="https://en.wikipedia.org/wiki/Bloch_sphere" target="_blank">this Wikipedia article</a>.
 
 <table>
   <tr>
@@ -597,4 +599,4 @@ Congratulations!  In this kata you learned the matrix and the ket-bra representa
 - Any square matrix can be represented as a linear combination of the outer products of vectors. The outer product is the matrix product of $\ket{\phi}$ and $\bra{\psi}$, denoted as $\ket{\phi}\bra{\psi}$.
 - Pauli gates, identity and Hadamard gates, phase shift gates, and rotation gates are examples of single-qubit gates. All of them are available in Q#.
 
-Next, you will learn about multi-qubit systems in the “Multi-Qubit Systems” kata.
+Next, you'll learn about multi-qubit systems in the “Multi-Qubit Systems” kata.
diff --git a/katas/content/single_qubit_gates/phase_i/solution.md b/katas/content/single_qubit_gates/phase_i/solution.md
index 35a52382..8cccbb91 100644
--- a/katas/content/single_qubit_gates/phase_i/solution.md
+++ b/katas/content/single_qubit_gates/phase_i/solution.md
@@ -1,6 +1,6 @@
 #### Solution 1
 
-We can recognize that the $S$ gate performs this particular relative phase addition to the $\ket{1}$ basis state. As a reminder,
+You can recognize that the $S$ gate performs this particular relative phase addition to the $\ket{1}$ basis state. As a reminder,
 
 $$
 S =
diff --git a/katas/content/single_qubit_gates/prepare_arbitrary_state/index.md b/katas/content/single_qubit_gates/prepare_arbitrary_state/index.md
index 41d367c4..e0ef5ea1 100644
--- a/katas/content/single_qubit_gates/prepare_arbitrary_state/index.md
+++ b/katas/content/single_qubit_gates/prepare_arbitrary_state/index.md
@@ -7,4 +7,4 @@
 
 **Goal:** Transform the qubit into state $\alpha\ket{0} + e^{i\theta}\beta\ket{1}$.
 
-> Since only the relative amplitudes and relative phase have any physical meaning, this allows us to prepare any single-qubit quantum state we want to.
\ No newline at end of file
+> Since only the relative amplitudes and relative phase have any physical meaning, this allows you to prepare any single-qubit quantum state you want to.
diff --git a/katas/content/single_qubit_gates/prepare_arbitrary_state/solution.md b/katas/content/single_qubit_gates/prepare_arbitrary_state/solution.md
index 41c69eb2..f135c3fe 100644
--- a/katas/content/single_qubit_gates/prepare_arbitrary_state/solution.md
+++ b/katas/content/single_qubit_gates/prepare_arbitrary_state/solution.md
@@ -2,7 +2,7 @@ This exercise can be done in two steps.
 
 1. Convert the state from $\ket{0}$ to $\alpha\ket{0} + \beta\ket{1}$.
    This can be done in a similar manner to the "Preparing a Rotated State" exercise, we first prepare an $\alpha\ket{0} -i\beta\ket{1}$ state using the $R_x$ gate, and then removing the relative phase of $-i$ by applying the $S$ gate, which would turn $\alpha\ket{0} -i\beta\ket{1}$ to $\alpha\ket{0} + \beta\ket{1}$.
-   An alternative, simpler approach is to use the $R_y$ gate, which allows us to get the necessary state right away without introducing a relative phase:
+   An alternative, simpler approach is to use the $R_y$ gate, which allows you to get the necessary state right away without introducing a relative phase:
 $$
 R_y(2\arctan\frac{\beta}{\alpha}) = \begin{bmatrix} \alpha & -\beta \\ \beta & \alpha \end{bmatrix}
 $$
diff --git a/katas/content/single_qubit_gates/prepare_minus/solution.md b/katas/content/single_qubit_gates/prepare_minus/solution.md
index 35761b7f..fcce962d 100644
--- a/katas/content/single_qubit_gates/prepare_minus/solution.md
+++ b/katas/content/single_qubit_gates/prepare_minus/solution.md
@@ -1,5 +1,5 @@
-We know that applying the Hadamard gate $H$ on the computational basis states $\ket{0}$ and $\ket{1}$ results in Hadamard basis states $\ket{+}$ and $\ket{-}$, respectively.
-We are given a qubit in the state $\ket{0}$. We first apply the Pauli $X$ gate to turn it into $X\ket{0}=\ket{1}$, and then apply the $H$ gate, turning the qubit into the required $H\ket{1}=\ket{-}$ state.
+You know that applying the Hadamard gate $H$ on the computational basis states $\ket{0}$ and $\ket{1}$ results in Hadamard basis states $\ket{+}$ and $\ket{-}$, respectively.
+You're given a qubit in the state $\ket{0}$. You first apply the Pauli $X$ gate to turn it into $X\ket{0}=\ket{1}$, and then apply the $H$ gate, turning the qubit into the required $H\ket{1}=\ket{-}$ state.
 
 @[solution]({
     "id": "single_qubit_gates__prepare_minus_solution",
diff --git a/katas/content/single_qubit_gates/prepare_rotated_state/index.md b/katas/content/single_qubit_gates/prepare_rotated_state/index.md
index a6480989..d68844cf 100644
--- a/katas/content/single_qubit_gates/prepare_rotated_state/index.md
+++ b/katas/content/single_qubit_gates/prepare_rotated_state/index.md
@@ -5,6 +5,6 @@
 
 **Goal:** Use a rotation gate to transform the qubit into state $\alpha\ket{0} -i\beta\ket{1}$.
 
-> You will probably need functions from the `Microsoft.Quantum.Math` namespace, specifically <a href="https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.math/arctan2" target="_blank">ArcTan2</a>.
+> You'll probably need functions from the `Microsoft.Quantum.Math` namespace, specifically <a href="https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.math/arctan2" target="_blank">ArcTan2</a>.
 >
 > You can assign variables in Q# by using the `let` keyword: `let num = 3;` or `let result = Function(input);`
diff --git a/katas/content/single_qubit_gates/prepare_rotated_state/solution.md b/katas/content/single_qubit_gates/prepare_rotated_state/solution.md
index 4cfaa7b7..a04c31b5 100644
--- a/katas/content/single_qubit_gates/prepare_rotated_state/solution.md
+++ b/katas/content/single_qubit_gates/prepare_rotated_state/solution.md
@@ -1,9 +1,9 @@
-We use the rotation gate $R_x(\theta)$. This gate turns the state $\ket{0}$ into $R_x(\theta)\ket{0} = \cos\frac{\theta}{2}\ket{0} - i\sin\frac{\theta}{2}\ket{1}$.
-This is similar to the state we need. We just need to find an angle $\theta$ such that $\cos\frac{\theta}{2}=\alpha$ and $\sin\frac{\theta}{2}=\beta$. We can use these two equations to solve for $\theta$: $\theta = 2\arctan\frac{\beta}{\alpha}$. (*Note: It is given that $\alpha^2 + \beta^2=1$*).
+You use the rotation gate $R_x(\theta)$. This gate turns the state $\ket{0}$ into $R_x(\theta)\ket{0} = \cos\frac{\theta}{2}\ket{0} - i\sin\frac{\theta}{2}\ket{1}$.
+This is similar to the state you need. You just need to find an angle $\theta$ such that $\cos\frac{\theta}{2}=\alpha$ and $\sin\frac{\theta}{2}=\beta$. You can use these two equations to solve for $\theta$: $\theta = 2\arctan\frac{\beta}{\alpha}$. (*Note: It's given that $\alpha^2 + \beta^2=1$*).
 Hence the required gate is $R_x(2\arctan\frac{\beta}{\alpha})$, which in matrix form is $\begin{bmatrix} \alpha & -i\beta \\ -i\beta & \alpha \end{bmatrix}$.
 This gate turns $\ket{0} = \begin{bmatrix} 1 \\ 0\end{bmatrix}$ into $\begin{bmatrix} \alpha & -i\beta \\ -i\beta & \alpha \end{bmatrix} \begin{bmatrix} 1 \\ 0\end{bmatrix} = \begin{bmatrix} \alpha \\ -i\beta \end{bmatrix} = \alpha\ket{0} -i\beta\ket{1}$.
 
-> Trigonometric functions are available in Q# via the `Microsoft.Quantum.Math` namespace. In this case we will need <a href="https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.math/arctan2" target="_blank">ArcTan2</a>.
+> Trigonometric functions are available in Q# via the `Microsoft.Quantum.Math` namespace. In this case, you'll need <a href="https://learn.microsoft.com/qsharp/api/qsharp-lang/microsoft.quantum.math/arctan2" target="_blank">ArcTan2</a>.
 
 @[solution]({
     "id": "single_qubit_gates__prepare_rotated_state_solution",
diff --git a/katas/content/single_qubit_gates/sign_flip/index.md b/katas/content/single_qubit_gates/sign_flip/index.md
index c7cc9318..17ad6731 100644
--- a/katas/content/single_qubit_gates/sign_flip/index.md
+++ b/katas/content/single_qubit_gates/sign_flip/index.md
@@ -1,3 +1,3 @@
 **Input**: A qubit in state $\ket{\psi} = \alpha \ket{0} + \beta \ket{1}$.
 
-**Goal** : Change the qubit state to $\alpha \ket{0} - \beta \ket{1}$ (i.e., flip the sign of the $\ket{1}$ component of the superposition).
+**Goal** : Change the qubit state to $\alpha \ket{0} - \beta \ket{1}$, that is, flip the sign of the $\ket{1}$ component of the superposition.
diff --git a/katas/content/single_qubit_gates/sign_flip_on_zero/index.md b/katas/content/single_qubit_gates/sign_flip_on_zero/index.md
index 1777a019..83abf2b5 100644
--- a/katas/content/single_qubit_gates/sign_flip_on_zero/index.md
+++ b/katas/content/single_qubit_gates/sign_flip_on_zero/index.md
@@ -1,5 +1,5 @@
 **Input:** A qubit in an arbitrary state $\ket{\psi} = \alpha\ket{0} + \beta\ket{1}$.
 
-**Goal:** Use several Pauli gates to change the qubit state to $- \alpha\ket{0} + \beta\ket{1}$, i.e., apply the transformation represented by the following matrix:
+**Goal:** Use several Pauli gates to change the qubit state to $- \alpha\ket{0} + \beta\ket{1}$, that is, apply the transformation represented by the following matrix:
 
 $$\begin{bmatrix} -1 & 0 \\ 0 & 1 \end{bmatrix}$$
diff --git a/katas/content/single_qubit_gates/sign_flip_on_zero/solution.md b/katas/content/single_qubit_gates/sign_flip_on_zero/solution.md
index 25102579..34b50289 100644
--- a/katas/content/single_qubit_gates/sign_flip_on_zero/solution.md
+++ b/katas/content/single_qubit_gates/sign_flip_on_zero/solution.md
@@ -1,7 +1,7 @@
 The first thing to notice is that the gate $\begin{bmatrix} -1 & 0 \\ 0 & 1 \end{bmatrix}$ is quite similar to the Pauli $Z$ gate $\begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix}$.
-The only difference being that the negative phase is applied on the $\ket{0}$ instead of $\ket{1}$. Hence we can simulate this gate by switching $\ket{0}$ and $\ket{1}$ states, applying the Pauli $Z$ gate and switching them back. The Pauli $X$ gate is the perfect gate to flip the state of the qubit and to undo the action afterwards.
+The only difference being that the negative phase is applied on the $\ket{0}$ instead of $\ket{1}$. Hence you can simulate this gate by switching $\ket{0}$ and $\ket{1}$ states, applying the Pauli $Z$ gate and switching them back. The Pauli $X$ gate is the perfect gate to flip the state of the qubit and to undo the action afterwards.
 
-Hence we can express the $Z_0 = \begin{bmatrix} -1 & 0 \\ 0 & 1 \end{bmatrix}$ matrix as
+Hence you can express the $Z_0 = \begin{bmatrix} -1 & 0 \\ 0 & 1 \end{bmatrix}$ matrix as
 
 $$
 Z_0 =
diff --git a/katas/content/single_qubit_gates/state_flip/solution.md b/katas/content/single_qubit_gates/state_flip/solution.md
index be8a74d8..234ba2f7 100644
--- a/katas/content/single_qubit_gates/state_flip/solution.md
+++ b/katas/content/single_qubit_gates/state_flip/solution.md
@@ -1,4 +1,4 @@
-We can recognize that the Pauli $X$ gate will change the state $\ket{0}$ to $\ket{1}$ and vice versa, and $\alpha\ket{0} + \beta\ket{1}$ to $\alpha\ket{1} + \beta\ket{0}$.
+You can recognize that the Pauli $X$ gate will change the state $\ket{0}$ to $\ket{1}$ and vice versa, and $\alpha\ket{0} + \beta\ket{1}$ to $\alpha\ket{1} + \beta\ket{0}$.
 
 As a reminder, the Pauli $X$ gate is defined by the following matrix:
 
@@ -7,7 +7,7 @@ X =
 \begin{bmatrix} 0 &  1 \\ 1 &  0 \end{bmatrix}
 $$
 
-We can see how it affects, for example, the basis state $\ket{0}$:
+You can see how it affects, for example, the basis state $\ket{0}$:
 
 $$
 X\ket{0} =
@@ -18,7 +18,7 @@ X\ket{0} =
 \ket{1}
 $$
 
-Similarly, we can consider the effect of the $X$ gate on the superposition state $\ket{\psi} = 0.6\ket{0} + 0.8\ket{1}$:
+Similarly, you can consider the effect of the $X$ gate on the superposition state $\ket{\psi} = 0.6\ket{0} + 0.8\ket{1}$:
 
 $$
 X\ket{\psi} =
diff --git a/katas/content/single_qubit_gates/y_gate/index.md b/katas/content/single_qubit_gates/y_gate/index.md
index 710a9050..56a2c1f9 100644
--- a/katas/content/single_qubit_gates/y_gate/index.md
+++ b/katas/content/single_qubit_gates/y_gate/index.md
@@ -1,3 +1,3 @@
 **Input:** A qubit in an arbitrary state $\ket{\psi} = \alpha\ket{0} + \beta\ket{1}$.
 
-**Goal:** Apply the Y gate to the qubit, i.e., transform the given state into $i\alpha\ket{1} - i\beta\ket{0}$.
+**Goal:** Apply the Y gate to the qubit, that is, transform the given state into $i\alpha\ket{1} - i\beta\ket{0}$.
diff --git a/katas/content/single_qubit_gates/y_gate/solution.md b/katas/content/single_qubit_gates/y_gate/solution.md
index 4b7d05a2..e7bf1d66 100644
--- a/katas/content/single_qubit_gates/y_gate/solution.md
+++ b/katas/content/single_qubit_gates/y_gate/solution.md
@@ -1,4 +1,4 @@
-We have to do exactly what the task asks us to do: apply the Pauli gate $Y=\begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix}$.
+You have to do exactly what the task asks us to do: apply the Pauli gate $Y=\begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix}$.
 
 This has the effect of turning $\ket{\psi} = \alpha\ket{0} + \beta\ket{1}$ into $Y\ket{\psi} = i\alpha\ket{1} - i\beta\ket{0}$, which in matrix form looks as follows:
 $$
diff --git a/katas/content/solving_graph_coloring/Common.qs b/katas/content/solving_graph_coloring/Common.qs
new file mode 100644
index 00000000..8ba50911
--- /dev/null
+++ b/katas/content/solving_graph_coloring/Common.qs
@@ -0,0 +1,89 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Katas;
+
+    // Hardcoded graphs used for testing the vertex coloring problem:
+    //  - trivial graph with zero edges
+    //  - complete graph with 4 vertices (4-colorable)
+    //  - disconnected graph
+    //  - random connected graph with more edges and vertices (3-colorable)
+    //  - regular-ish graph with 5 vertices (3-colorable, as shown at https://en.wikipedia.org/wiki/File:3-coloringEx.svg without one vertex)
+    //  - 6-vertex graph from https://en.wikipedia.org/wiki/File:3-coloringEx.svg
+    function ExampleGraphs() : (Int, (Int, Int)[])[] {
+        return [(3, []),
+                (4, [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]),
+                (5, [(4, 0), (2, 1), (3, 1), (3, 2)]),
+                (5, [(0, 1), (1, 2), (1, 3), (3, 2), (4, 2), (3, 4)]),
+                (5, [(0, 1), (0, 2), (0, 4), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]),
+                (6, [(0, 1), (0, 2), (0, 4), (0, 5), (1, 2), (1, 3), (1, 5), (2, 3), (2, 4), (3, 4), (3, 5), (4, 5)])];
+        // Graphs with 6+ vertices can take several minutes to be processed; 
+        // in the interest of keeping test runtime reasonable we're limiting most of the testing to graphs with 5 vertices or fewer.
+    }
+
+
+    function IsVertexColoringValid_Reference (V : Int, edges: (Int, Int)[], colors: Int[]) : Bool {
+        for (start, end) in edges {
+            if colors[start] == colors[end] {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    operation ReadColoring_Reference(nBits : Int, qs : Qubit[]) : Int[] {
+        let colorPartitions = Chunks(nBits, qs);
+        let measureColor = qs => ResultArrayAsInt(Reversed(MeasureEachZ(qs)));
+        return ForEach(measureColor, colorPartitions);
+    }
+
+
+    // Helper function specific to Graph Coloring kata.
+    operation CheckOracleRecognizesColoring (
+        V : Int,
+        edges : (Int, Int)[],
+        oracle : (Int, (Int, Int)[],Qubit[], Qubit) => Unit,
+        classicalFunction : (Int, (Int, Int)[], Int[]) -> Bool
+    ) : Bool {
+        // Message($"Testing V = {V}, edges = {edges}");
+        let N = 2 * V;
+        use (coloringRegister, target) = (Qubit[N], Qubit());
+        // Try all possible colorings of 4 colors on V vertices and check if they are calculated correctly.
+        // Hack: fix the color of the first vertex, since all colorings are agnostic to the specific colors used.
+        for k in 0 .. (1 <<< (N - 2)) - 1 {
+            // Prepare k-th coloring
+            let binary = [false, false] + IntAsBoolArray(k, N - 2);
+            ApplyPauliFromBitString(PauliX, true, binary, coloringRegister);
+
+            // Read out the coloring (convert one bitmask into V integers) - does not change the state
+            let coloring = ReadColoring_Reference(2, coloringRegister);
+
+            // Apply the oracle
+            oracle(V, edges, coloringRegister, target);
+
+            // Check that the oracle result matches the classical result
+            let val = classicalFunction(V, edges, coloring);
+            if val {
+                X(target);
+            }
+            // Uncompute
+            ApplyPauliFromBitString(PauliX, true, binary, coloringRegister);
+
+            if not CheckAllZero(coloringRegister + [target]) {
+                Message($"Incorrect result for V = {V}, edges = {edges}");
+                Message($"For test case with input = {BoolArrayAsKetState(binary)}, coloring = {coloring}");
+                if not CheckZero(target) {
+                    Message($"Expected answer = {val}, got {not val}");
+                } else {
+                    Message("The input state should not change");
+                }
+                ResetAll(coloringRegister + [target]);
+                return false;
+            }
+        }
+
+        true
+    }
+}
\ No newline at end of file
diff --git a/katas/content/solving_graph_coloring/color_equality/Placeholder.qs b/katas/content/solving_graph_coloring/color_equality/Placeholder.qs
new file mode 100644
index 00000000..1d97cd0d
--- /dev/null
+++ b/katas/content/solving_graph_coloring/color_equality/Placeholder.qs
@@ -0,0 +1,6 @@
+namespace Kata {
+    operation Oracle_ColorEquality(x0 : Qubit[], x1 : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+}
diff --git a/katas/content/solving_graph_coloring/color_equality/Solution.qs b/katas/content/solving_graph_coloring/color_equality/Solution.qs
new file mode 100644
index 00000000..95c23dab
--- /dev/null
+++ b/katas/content/solving_graph_coloring/color_equality/Solution.qs
@@ -0,0 +1,11 @@
+namespace Kata {
+    operation Oracle_ColorEquality(x0 : Qubit[], x1 : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        within {
+            for i in 0..Length(x0) - 1 {
+                CNOT(x0[i], x1[i]);
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, x1, y);
+        }
+    }
+}
diff --git a/katas/content/solving_graph_coloring/color_equality/Verification.qs b/katas/content/solving_graph_coloring/color_equality/Verification.qs
new file mode 100644
index 00000000..57fe0437
--- /dev/null
+++ b/katas/content/solving_graph_coloring/color_equality/Verification.qs
@@ -0,0 +1,33 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Katas;
+    open Microsoft.Quantum.Arrays;
+
+    function F_ColorEquality(args : Bool[]) : Bool {
+        // Check that the first half equals the second half
+        let nBits = Length(args) / 2;
+        for (f, s) in Zipped(args[...nBits - 1], args[nBits...]) {
+            if f != s {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    operation Oracle_ColorEquality_Wrapper(x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        let nBits = Length(x) / 2;
+        Kata.Oracle_ColorEquality(x[...nBits - 1], x[nBits...], y);
+    }
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for nBits in 1..3 {
+            if not CheckOracleImplementsFunction(2 * nBits, Oracle_ColorEquality_Wrapper, F_ColorEquality) {
+                Message($"Test failed for nBits = {nBits}");
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/solving_graph_coloring/color_equality/index.md b/katas/content/solving_graph_coloring/color_equality/index.md
new file mode 100644
index 00000000..ee7de074
--- /dev/null
+++ b/katas/content/solving_graph_coloring/color_equality/index.md
@@ -0,0 +1,12 @@
+**Inputs:**
+
+1. An array of $nBits$ qubits in an arbitrary state $\ket{x_0}$ representing the first color.
+2. An array of $nBits$ qubits in an arbitrary state $\ket{x_1}$ representing the second color.
+3. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+
+**Goal:**
+Implement a quantum oracle which checks whether the two given colors are equal.
+In other words, flip the state of qubit $\ket{y}$ if $x_0 = x_1$, and leave it unchanged otherwise.
+    
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/solving_graph_coloring/color_equality/solution.md b/katas/content/solving_graph_coloring/color_equality/solution.md
new file mode 100644
index 00000000..733ea7f6
--- /dev/null
+++ b/katas/content/solving_graph_coloring/color_equality/solution.md
@@ -0,0 +1,13 @@
+The goal is to flip the qubit $\ket{y}$ if and only if each of the matching pairs of qubits in the arrays `x0` and `x1` are in the same state.
+
+You can check whether two qubits are in the same state by computing their $\textrm{XOR}$: if their state was the same, their $\textrm{XOR}$ will be $0$. You can use the $\textrm{CNOT}$ gate to compute $\textrm{XOR}$ of two qubits in place, for example, using the qubit of the array `x0` as the control and the qubit of the array `x1` as the target.
+
+Once you've used $nBits$ $\textrm{CNOT}$ gates to compute all pairwise $\textrm{XOR}$'s, you'll need to flip the target qubit $\ket{y}$ only if all  qubits in `x1` are in the $\ket{0}$ state. 
+This can be done by using zero-controlled $X$ gate, that is, `ApplyControlledOnInt(0, X, x1, y)`.
+
+Finally, you need to uncompute the bitwise $\textrm{XOR}$'s to ensure that the qubits in `x1` are returned to their original state.
+
+@[solution]({
+    "id": "solving_graph_coloring__color_equality_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/solving_graph_coloring/index.md b/katas/content/solving_graph_coloring/index.md
new file mode 100644
index 00000000..43e80aa2
--- /dev/null
+++ b/katas/content/solving_graph_coloring/index.md
@@ -0,0 +1,87 @@
+# Solving Graph Coloring Using Grover's Algorithm
+
+@[section]({
+    "id": "solving_graph_coloring__overview",
+    "title": "Overview"
+})
+
+The key part of solving a classical problem using Grover's search algorithm is implementing the quantum oracle for that problem.
+In practice, implementing a quantum oracle for a specific problem can be quite challenging.
+
+This kata walks you through implementing the quantum oracles for several kinds of graph coloring problems - 
+problems that look for an assignment of labels, commonly referred to as "colors", to vertices or edges of the graph 
+in a way that satisfies the given set of constraints.
+It also encourages you to experiment with using these oracles to solve graph coloring problems with Grover's search.
+
+**This kata covers the following topics:**
+
+- Implementation of marking oracles for several versions of graph coloring problems
+- Using Grover's search algorithm to solve graph coloring problems
+
+**What you should know to start working on this kata:**
+
+- Fundamental quantum concepts
+- Controlled gates
+- Oracles, in particular marking oracles
+- Grover's search algorithm
+
+@[section]({
+    "id": "solving_graph_coloring__vertex_coloring",
+    "title": "Vertex Coloring Problem"
+})
+
+Vertex coloring problem is the simplest form of a graph coloring problem. In it, you look for a coloring of graph vertices which labels each vertex with one of the given colors so that no two vertices of the same color are connected by an edge. In other words, the colors of any pair of vertices that is connected by an edge must be different.
+
+In this lesson, you will implement the marking oracle for the vertex coloring problem, as well as several building blocks you'll need for an end-to-end implementation of Grover's search for this problem.
+
+@[exercise]({
+    "id": "solving_graph_coloring__vertex_coloring_classical",
+    "title": "Is Vertex Coloring Valid? (Classical)",
+    "path": "./vertex_coloring_classical/"
+})
+
+@[exercise]({
+    "id": "solving_graph_coloring__read_coloring",
+    "title": "Read Coloring From a Qubit Array",
+    "path": "./read_coloring/"
+})
+
+@[exercise]({
+    "id": "solving_graph_coloring__color_equality",
+    "title": "Are Colors Equal?",
+    "path": "./color_equality/"
+})
+
+@[exercise]({
+    "id": "solving_graph_coloring__vertex_coloring_quantum",
+    "title": "Is Vertex Coloring Valid? (Quantum)",
+    "path": "./vertex_coloring/"
+})
+
+
+@[section]({
+    "id": "solving_graph_coloring__weak_coloring",
+    "title": "Weak Coloring Problem"
+})
+
+TODO: finish migration
+
+
+@[section]({
+    "id": "solving_graph_coloring__using_grover",
+    "title": "Using Grover's Algorithm to Solve Graph Coloring Problems"
+})
+
+In this lesson, you will experiment with using Grover's algorithm to solve graph coloring problems.
+
+Notice that in this case, it's not as easy to know the number of solutions to the problem upfront as it was for the prefix function used in the "Grover's Search Algorithm" kata.
+Experiment with choosing the number of iterations at random. How does this affect the success probability?
+
+TODO: add demo
+
+@[section]({
+    "id": "solving_graph_coloring__conclusion",
+    "title": "Conclusion"
+})
+
+Congratulations! In this kata you learned to solve graph coloring problems using Grover's search.
diff --git a/katas/content/solving_graph_coloring/read_coloring/Placeholder.qs b/katas/content/solving_graph_coloring/read_coloring/Placeholder.qs
new file mode 100644
index 00000000..b3f93a42
--- /dev/null
+++ b/katas/content/solving_graph_coloring/read_coloring/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    operation ReadColoring(nBits : Int, qs : Qubit[]) : Int[] {
+        // Implement your solution here...
+
+        return [];
+    }
+}
diff --git a/katas/content/solving_graph_coloring/read_coloring/Solution.qs b/katas/content/solving_graph_coloring/read_coloring/Solution.qs
new file mode 100644
index 00000000..2989bc33
--- /dev/null
+++ b/katas/content/solving_graph_coloring/read_coloring/Solution.qs
@@ -0,0 +1,10 @@
+namespace Kata {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+
+    operation ReadColoring(nBits : Int, qs : Qubit[]) : Int[] {
+        let colorPartitions = Chunks(nBits, qs);
+        let measureColor = qs => ResultArrayAsInt(Reversed(MeasureEachZ(qs)));
+        return ForEach(measureColor, colorPartitions);
+    }
+}
diff --git a/katas/content/solving_graph_coloring/read_coloring/Verification.qs b/katas/content/solving_graph_coloring/read_coloring/Verification.qs
new file mode 100644
index 00000000..d460bb15
--- /dev/null
+++ b/katas/content/solving_graph_coloring/read_coloring/Verification.qs
@@ -0,0 +1,52 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Diagnostics;
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Convert;
+    open Microsoft.Quantum.Katas;
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for nBits in 1..3 {
+            for V in 1..3 {
+                use qs = Qubit[V * nBits];
+                for state in 0..(1 <<< (V * nBits)) - 1 {
+                    // Prepare the register in the input state
+                    let binaryState = IntAsBoolArray(state, V * nBits);
+                    ApplyPauliFromBitString(PauliX, true, binaryState, qs);
+
+                    // Call the solution
+                    let result = Kata.ReadColoring(nBits, qs);
+
+                    // verify that the register remained in the same state
+                    ApplyPauliFromBitString(PauliX, true, binaryState, qs);
+                    if not CheckAllZero(qs) {
+                        Message("The input state should not change");
+                        ResetAll(qs);
+                        return false;
+                    }
+
+                    // Get the expected coloring by splitting binaryState into parts and converting them into integers
+                    // (remember to use big endian)
+                    let partitions = Chunks(nBits, binaryState);
+                    let partitionToInt = bits -> BoolArrayAsInt(Reversed(bits));
+                    let expectedColors = Mapped(partitionToInt, partitions);
+
+                    // Verify the return value
+                    if Length(result) != V {
+                        Message($"Unexpected number of colors for V = {V}, nBits = {nBits} : {Length(result)}");
+                        return false;
+                    }
+                    for (expected, actual) in Zipped(expectedColors, result) {
+                        if expected != actual {
+                            Message($"Unexpected colors for V = {V}, nBits = {nBits}, " + 
+                                $"state = {BoolArrayAsKetState(binaryState)} : expected {expectedColors}, got {result}");
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/solving_graph_coloring/read_coloring/index.md b/katas/content/solving_graph_coloring/read_coloring/index.md
new file mode 100644
index 00000000..012cbd09
--- /dev/null
+++ b/katas/content/solving_graph_coloring/read_coloring/index.md
@@ -0,0 +1,16 @@
+In this task, you need to read the coloring of an $V$-vertex graph stored in a qubit array.
+This is the last step of Grover's search algorithm: performing measurements and interpreting the results as the potential solution to your problem.
+
+**Inputs**:
+
+  1. The number of bits in each color of the coloring $nBits$.
+  2. An array of $V \cdot nBits$ qubits which are guaranteed to be in a basis state.
+
+**Output**:
+
+An array of $V$ $nBits$-bit integers that represent this basis state.
+$i$-th integer of the array is stored in qubits with indices $i \cdot nBits$, $i \cdot nBits + 1$, ..., $(i + 1) \cdot nBits - 1$ in big-endian format. 
+
+For example, the input $nBits = 2$ and basis state $\ket{000110}$ correspond to two-bit colors $00, 01, 10$, or, in decimal, $0, 1, 2$.
+
+The operation should not change the state of the qubits.
diff --git a/katas/content/solving_graph_coloring/read_coloring/solution.md b/katas/content/solving_graph_coloring/read_coloring/solution.md
new file mode 100644
index 00000000..d22bb820
--- /dev/null
+++ b/katas/content/solving_graph_coloring/read_coloring/solution.md
@@ -0,0 +1,15 @@
+The solution to this exercise consists of two parts:
+
+1. Read an integer from an array of qubits of length `nBits`.
+   Since you are guaranteed that the qubits in the array are in a basis state, simply measuring them will give you the necessary information and leave the state of the qubits unchanged. 
+   You can use the library operation `MeasureEachZ` to measure each qubit in the array without resetting it to the $\ket{0}$ state afterwards. 
+   Then, you can convert the array of measurement results to an integer using the function `ResultArrayAsInt`.
+   Note that this function does the conversion from a little endian binary encoding, and the exercise asks for the colors to be represented in big endian, so you need to reverse the array of measurement results using the function `Reversed`.
+2. Split the given array of qubits into chunks of length `nBits` and read an integer from each of them.
+   A convenient library function `Chunks` does exactly that, splitting the array into chunks of the given length.
+   Finally, you can apply the integer readout operation to each chunk using the operation `ForEach`.
+
+@[solution]({
+    "id": "solving_graph_coloring__read_coloring_solution",
+    "codePath": "Solution.qs"
+})
diff --git a/katas/content/solving_graph_coloring/vertex_coloring/Placeholder.qs b/katas/content/solving_graph_coloring/vertex_coloring/Placeholder.qs
new file mode 100644
index 00000000..72717d38
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring/Placeholder.qs
@@ -0,0 +1,17 @@
+namespace Kata {
+    operation Oracle_VertexColoring(V : Int, edges: (Int, Int)[], x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        // Implement your solution here...
+
+    }
+
+    // You might find this helper operation from an earlier task useful.
+    operation Oracle_ColorEquality(x0 : Qubit[], x1 : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        within {
+            for i in 0..Length(x0) - 1 {
+                CNOT(x0[i], x1[i]);
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, x1, y);
+        }
+    }
+}
diff --git a/katas/content/solving_graph_coloring/vertex_coloring/Solution.qs b/katas/content/solving_graph_coloring/vertex_coloring/Solution.qs
new file mode 100644
index 00000000..36d5e179
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring/Solution.qs
@@ -0,0 +1,25 @@
+namespace Kata {
+    operation Oracle_VertexColoring(V : Int, edges: (Int, Int)[], x : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        let edgesNumber = Length(edges);
+        use conflicts = Qubit[edgesNumber];
+        within {
+            for i in 0 .. edgesNumber - 1 {
+                let (v0, v1) = edges[i];
+                Oracle_ColorEquality(x[2 * v0 .. 2 * v0 + 1], 
+                                     x[2 * v1 .. 2 * v1 + 1], conflicts[i]);
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, conflicts, y);
+        }
+    }
+
+    operation Oracle_ColorEquality(x0 : Qubit[], x1 : Qubit[], y : Qubit) : Unit is Adj + Ctl {
+        within {
+            for i in 0..Length(x0) - 1 {
+                CNOT(x0[i], x1[i]);
+            }
+        } apply {
+            ApplyControlledOnInt(0, X, x1, y);
+        }
+    }
+}
diff --git a/katas/content/solving_graph_coloring/vertex_coloring/Verification.qs b/katas/content/solving_graph_coloring/vertex_coloring/Verification.qs
new file mode 100644
index 00000000..f80c7aa6
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring/Verification.qs
@@ -0,0 +1,16 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Katas;
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        for (V, edges) in Most(ExampleGraphs()) {
+            if not CheckOracleRecognizesColoring(V, edges, Kata.Oracle_VertexColoring, IsVertexColoringValid_Reference) {
+                return false;
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/solving_graph_coloring/vertex_coloring/index.md b/katas/content/solving_graph_coloring/vertex_coloring/index.md
new file mode 100644
index 00000000..f00c0dc8
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring/index.md
@@ -0,0 +1,19 @@
+**Inputs:**
+
+1. The number of vertices in the graph $V$ ($V \leq 6$).
+2. An array of $E$ tuples of integers, representing the edges of the graph ($E \leq 12$).
+Each tuple gives the indices of the start and the end vertices of the edge.
+The vertices are numbered $0$ through $V - 1$.
+3. An array of $2V$ qubits in an arbitrary state $\ket{x}$ representing the assigned coloring of the vertices using four colors ($nBits = 2$) in the same format as in the exercise "Read Coloring From a Qubit Array".
+4. A qubit in an arbitrary state $\ket{y}$ (output/target qubit).
+
+For example, the graph `0 -- 1 -- 2` has $V = 3$ and `edges = [(0, 1), (1, 2)]`. 
+The color assignments for this graph are represented with $6$ qubits.
+Some of the valid colorings are $\ket{000110}$ (vertices `0, 1, 2` assigned colors `0, 1, 2`) and $\ket{011001}$ (vertices `0, 1, 2` assigned colors `1, 2, 1`).
+
+**Goal:**
+Implement a quantum oracle which checks whether the given coloring of this graph is a valid vertex coloring,
+that is, whether the colors assigned to each pair of vertices connected with an edge are different.
+    
+Leave the qubits in the input register in the same state they started in.
+Your solution should work on inputs in superposition, and not use any measurements.
\ No newline at end of file
diff --git a/katas/content/solving_graph_coloring/vertex_coloring/solution.md b/katas/content/solving_graph_coloring/vertex_coloring/solution.md
new file mode 100644
index 00000000..b00c287b
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring/solution.md
@@ -0,0 +1,13 @@
+You need to allocate an array of auxiliary qubits, one for each edge of the graph, that will have their states flipped if the vertices connected by the corresponding edge have the same color. 
+This can be done easily using with the operation `Oracle_ColorEquality` from the previous task. 
+
+Then, you need to check if these auxiliary qubits are still in state $\ket{0...0}$; if they are, all the necessary pairs of colors are distinct, the coloring is valid.
+
+Since the coloring is provided as an array of qubits, with two qubits per vertex (2 qubits = 4 basis states = 4 colors), you have to take the correct chunks of the coloring to extract the color of each vertex. You can deduce that the coloring of vertex $j$ is encoded in qubits in positions $2j$ and $2j+1$.
+
+Make sure to uncompute the changes to the auxiliary qubits after you evaluate the final result to leave them clean before their release.
+
+@[solution]({
+    "id": "solving_graph_coloring__vertex_coloring_quantum_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/katas/content/solving_graph_coloring/vertex_coloring_classical/Placeholder.qs b/katas/content/solving_graph_coloring/vertex_coloring_classical/Placeholder.qs
new file mode 100644
index 00000000..27f3a087
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring_classical/Placeholder.qs
@@ -0,0 +1,7 @@
+namespace Kata {
+    function IsVertexColoringValid(V : Int, edges: (Int, Int)[], colors: Int[]) : Bool {
+        // Implement your solution here...
+
+        return false;
+    }
+}
diff --git a/katas/content/solving_graph_coloring/vertex_coloring_classical/Solution.qs b/katas/content/solving_graph_coloring/vertex_coloring_classical/Solution.qs
new file mode 100644
index 00000000..eac5ca8a
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring_classical/Solution.qs
@@ -0,0 +1,10 @@
+namespace Kata {
+    function IsVertexColoringValid(V : Int, edges: (Int, Int)[], colors: Int[]) : Bool {
+        for (v0, v1) in edges {
+            if colors[v0] == colors[v1] {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/katas/content/solving_graph_coloring/vertex_coloring_classical/Verification.qs b/katas/content/solving_graph_coloring/vertex_coloring_classical/Verification.qs
new file mode 100644
index 00000000..6f686062
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring_classical/Verification.qs
@@ -0,0 +1,27 @@
+namespace Kata.Verification {
+    open Microsoft.Quantum.Arrays;
+    open Microsoft.Quantum.Katas;
+
+    @EntryPoint()
+    operation CheckSolution() : Bool {
+        let testGraphs = ExampleGraphs()[0 .. 4];
+        let testColorings = [
+            [([0, 0, 0], true), ([2, 1, 3], true)],
+            [([0, 2, 1, 3], true), ([3, 0, 1, 2], true), ([0, 2, 1, 0], false)],
+            [([0, 1, 2, 3, 4], true), ([0, 2, 1, 0, 3], true), ([1, 0, 1, 2, 1], false), ([0, 0, 0, 0, 0], false)],
+            [([0, 1, 0, 2, 1], true), ([0, 2, 0, 1, 3], true), ([0, 1, 0, 1, 2], false)],
+            [([1, 2, 3, 1, 2], true), ([1, 2, 3, 4, 1], false)]
+        ];
+        for ((V, edges), colorings) in Zipped(testGraphs, testColorings) {
+            for (coloring, expected) in colorings {
+                if Kata.IsVertexColoringValid(V, edges, coloring) != expected {
+                    Message($"Coloring {coloring} evaluated incorrectly for graph V = {V}, edges = {edges}: expected {expected}");
+                    return false;
+                }
+            }
+        }
+
+        Message("Correct!");
+        true
+    }
+}
diff --git a/katas/content/solving_graph_coloring/vertex_coloring_classical/index.md b/katas/content/solving_graph_coloring/vertex_coloring_classical/index.md
new file mode 100644
index 00000000..86676a2e
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring_classical/index.md
@@ -0,0 +1,15 @@
+**Inputs:**
+
+1. The number of vertices in the graph $V$ ($V \leq 6$).
+2. An array of $E$ tuples of integers, representing the edges of the graph ($E \leq 12$).
+Each tuple gives the indices of the start and the end vertices of the edge.
+The vertices are numbered $0$ through $V - 1$.
+3. An array of $V$ integers, representing the vertex coloring of the graph. 
+$i$-th element of the array is the color of the vertex number $i$.
+
+For example, the graph `0 -- 1 -- 2` has $V = 3$ and `edges = [(0, 1), (1, 2)]`. 
+Some of the valid colorings for it would be `[0, 1, 0]` and `[-1, 5, 18]`.
+Notice that in this exercise, unlike in some of the later ones, the coloring is not limited to numbers from $0$ to $3$.
+
+**Output:**
+True if the given vertex coloring is valid (that is, no two vertices connected by an edge have the same color), and false otherwise.
\ No newline at end of file
diff --git a/katas/content/solving_graph_coloring/vertex_coloring_classical/solution.md b/katas/content/solving_graph_coloring/vertex_coloring_classical/solution.md
new file mode 100644
index 00000000..878e0a9c
--- /dev/null
+++ b/katas/content/solving_graph_coloring/vertex_coloring_classical/solution.md
@@ -0,0 +1,8 @@
+A graph coloring is valid when the nodes connected by every edge have a different color. This means that we have to check every edge, see if the nodes have the same color, and if it is the case, return that the graph coloring is invalid. If every edge passed the test, we can safely say the graph coloring is valid.
+
+Since the color of vertex $n$  is the $n$-th element of the `colors` array, we simply loop through every edge, which is a pair of vertex indices, and compare their colors.
+
+@[solution]({
+    "id": "solving_graph_coloring__vertex_coloring_classical_solution",
+    "codePath": "./Solution.qs"
+})
diff --git a/library/qs_source/src/std/canon.qs b/library/qs_source/src/std/canon.qs
index 2e09e825..727c672a 100644
--- a/library/qs_source/src/std/canon.qs
+++ b/library/qs_source/src/std/canon.qs
@@ -348,7 +348,7 @@ namespace Microsoft.Quantum.Canon {
     /// ```
     operation ApplyPauliFromBitString(pauli : Pauli, bitApply : Bool, bits : Bool[], qubits : Qubit[]) : Unit is Adj + Ctl {
         let nBits = Length(bits);
-        Fact(nBits == Length(qubits), "Number of control bits must be equal to number of control qubits.");
+        Fact(nBits == Length(qubits), "Number of bits must be equal to number of qubits.");
         for i in 0..nBits - 1 {
             if bits[i] == bitApply {
                 ApplyP(pauli, qubits[i]);
diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py
index a333cba9..aa9cfaae 100644
--- a/pip/qsharp/_qsharp.py
+++ b/pip/qsharp/_qsharp.py
@@ -206,6 +206,9 @@ def run(
     """
     ipython_helper()
 
+    if shots < 1:
+        raise QSharpError("The number of shots must be greater than 0.")
+
     results: List[ShotResult] = []
 
     def print_output(output: Output) -> None:
@@ -215,7 +218,7 @@ def run(
         # Append the output to the last shot's output list
         results[-1]["events"].append(output)
 
-    for _ in range(shots):
+    for shot in range(shots):
         results.append({"result": None, "events": []})
         run_results = get_interpreter().run(
             entry_expr, on_save_events if save_events else print_output
@@ -223,6 +226,10 @@ def run(
         results[-1]["result"] = run_results
         if on_result:
             on_result(results[-1])
+        # For every shot after the first, treat the entry expression as None to trigger
+        # a rerun of the last executed expression without paying the cost for any additional
+        # compilation.
+        entry_expr = None
 
     if save_events:
         return results
diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs
index c07a8230..bbaab8cf 100644
--- a/pip/src/interpreter.rs
+++ b/pip/src/interpreter.rs
@@ -194,7 +194,7 @@ impl Interpreter {
     fn run(
         &mut self,
         py: Python,
-        entry_expr: &str,
+        entry_expr: Option<&str>,
         callback: Option<PyObject>,
     ) -> PyResult<PyObject> {
         let mut receiver = OptionalCallbackReceiver { callback, py };
diff --git a/resource_estimator/src/estimates/physical_estimation.rs b/resource_estimator/src/estimates/physical_estimation.rs
index dc38a98a..2d47a891 100644
--- a/resource_estimator/src/estimates/physical_estimation.rs
+++ b/resource_estimator/src/estimates/physical_estimation.rs
@@ -1167,15 +1167,12 @@ impl<
             .iter()
             .map(|factory| {
                 let magic_states_per_run = max_factories * factory.num_output_states();
-                let required_runs = ((self
+                let required_runs = self
                     .layout_overhead
                     .num_magic_states(&self.error_budget, magic_state_index)
-                    as f64)
-                    / magic_states_per_run as f64)
-                    .ceil() as u64;
+                    .div_ceil(magic_states_per_run);
                 let required_duration = required_runs * factory.duration();
-                let num = (required_duration as f64 / logical_patch.logical_cycle_time() as f64)
-                    .ceil() as u64;
+                let num = required_duration.div_ceil(logical_patch.logical_cycle_time());
                 (factory.clone(), num)
             })
             .filter(|(_, num_cycles)| *num_cycles <= max_allowed_num_cycles_for_code_parameter)
@@ -1184,13 +1181,12 @@ impl<
                     .normalized_volume()
                     .partial_cmp(&q.normalized_volume())
                     .expect("Could not compare factories normalized volume");
-                if comp1 == Ordering::Equal {
+
+                comp1.then_with(|| {
                     num_p
                         .partial_cmp(num_q)
                         .expect("Could not compare factories num cycles")
-                } else {
-                    comp1
-                }
+                })
             })
     }
 
@@ -1310,15 +1306,26 @@ impl<
         factory: &Builder::Factory,
         num_cycles: u64,
     ) -> u64 {
-        let magic_states_per_cycles =
+        // first, try with the exact calculation; if that does not work, use
+        // floating-point arithmetic, which may cause numeric imprecision
+        if let Some(total_duration) = num_cycles.checked_mul(logical_patch.logical_cycle_time()) {
+            // number of magic states that one factory can compute in num_cycles
+            let num_states_per_run =
+                (total_duration / factory.duration()) * factory.num_output_states();
             self.layout_overhead
-                .num_magic_states(&self.error_budget, magic_state_index) as f64
-                / (factory.num_output_states() * num_cycles) as f64;
+                .num_magic_states(&self.error_budget, magic_state_index)
+                .div_ceil(num_states_per_run)
+        } else {
+            let magic_states_per_cycles =
+                self.layout_overhead
+                    .num_magic_states(&self.error_budget, magic_state_index) as f64
+                    / (factory.num_output_states() * num_cycles) as f64;
 
-        let factory_duration_fraction =
-            factory.duration() as f64 / logical_patch.logical_cycle_time() as f64;
+            let factory_duration_fraction =
+                factory.duration() as f64 / logical_patch.logical_cycle_time() as f64;
 
-        (magic_states_per_cycles * factory_duration_fraction).ceil() as _
+            (magic_states_per_cycles * factory_duration_fraction).ceil() as _
+        }
     }
 }
 
diff --git a/resource_estimator/src/lib.rs b/resource_estimator/src/lib.rs
index aa3b0103..95a0ed69 100644
--- a/resource_estimator/src/lib.rs
+++ b/resource_estimator/src/lib.rs
@@ -57,7 +57,7 @@ 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)
+        .run_with_sim(&mut counter, &mut out, Some(expr))
         .map_err(|e| e.into_iter().map(Error::Interpreter).collect::<Vec<_>>())?
         .map_err(|e| vec![Error::Interpreter(e[0].clone())])?;
     estimate_physical_resources(counter.logical_resources(), params)
diff --git a/version.py b/version.py
index 59f035e8..ee1ff05a 100755
--- a/version.py
+++ b/version.py
@@ -7,7 +7,7 @@ import os
 from datetime import datetime, timezone
 
 # To be updated every time we start a new major.minor version.
-major_minor = "1.6"
+major_minor = "1.7"
 
 # Default to 'dev' builds
 BUILD_TYPE = os.environ.get("BUILD_TYPE") or "dev"
diff --git a/vscode/src/createProject.ts b/vscode/src/createProject.ts
index 08edc472..8b178b9a 100644
--- a/vscode/src/createProject.ts
+++ b/vscode/src/createProject.ts
@@ -183,13 +183,6 @@ export async function initProjectCreator(context: vscode.ExtensionContext) {
 
   // TODO: Replace with a list of legitimate known Q# projects on GitHub
   const githubProjects: { [name: string]: GitHubProjectRef } = {
-    assert: {
-      github: {
-        owner: "swernli",
-        repo: "MyQsLib",
-        ref: "29dc67f",
-      },
-    },
     // Add a template to the end of the list users can use to easily add their own
     "<id>": {
       github: {


Test patch:
diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs
index 1ade346b..b948b4bf 100644
--- a/compiler/qsc/src/interpret/tests.rs
+++ b/compiler/qsc/src/interpret/tests.rs
@@ -28,7 +28,7 @@ mod given_interpreter {
     ) -> (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), receiver.dump())
+        (interpreter.run(&mut receiver, Some(expr)), receiver.dump())
     }
 
     fn entry(
diff --git a/compiler/qsc_eval/src/tests.rs b/compiler/qsc_eval/src/tests.rs
index f801ff19..da44e16d 100644
--- a/compiler/qsc_eval/src/tests.rs
+++ b/compiler/qsc_eval/src/tests.rs
@@ -3,8 +3,6 @@
 
 #![allow(clippy::needless_raw_string_hashes)]
 
-use std::rc::Rc;
-
 use crate::{
     backend::{Backend, SparseSim},
     debug::Frame,
@@ -15,7 +13,7 @@ use crate::{
 use expect_test::{expect, Expect};
 use indoc::indoc;
 use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags};
-use qsc_fir::fir::{self, ExecGraphNode, StmtId};
+use qsc_fir::fir::{self, ExecGraph, StmtId};
 use qsc_fir::fir::{PackageId, PackageStoreLookup};
 use qsc_frontend::compile::{self, compile, PackageStore, SourceMap};
 use qsc_lowerer::map_hir_package_to_fir;
@@ -26,7 +24,7 @@ use qsc_passes::{run_core_passes, run_default_passes, PackageType};
 /// # Errors
 /// Returns the first error encountered during execution.
 pub(super) fn eval_graph(
-    graph: Rc<[ExecGraphNode]>,
+    graph: ExecGraph,
     sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
     globals: &impl PackageStoreLookup,
     package: PackageId,
diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py
index dbd29902..5a8ce6e3 100644
--- a/pip/tests/test_qsharp.py
+++ b/pip/tests/test_qsharp.py
@@ -305,3 +305,21 @@ def test_run_with_result_callback(capsys) -> None:
     stdout = capsys.readouterr().out
     assert stdout == ""
     assert called
+
+
+def test_run_with_invalid_shots_produces_error() -> None:
+    qsharp.init()
+    qsharp.eval('operation Foo() : Result { Message("Hello, world!"); Zero }')
+    try:
+        qsharp.run("Foo()", -1)
+    except qsharp.QSharpError as e:
+        assert str(e) == "The number of shots must be greater than 0."
+    else:
+        assert False
+
+    try:
+        qsharp.run("Foo()", 0)
+    except qsharp.QSharpError as e:
+        assert str(e) == "The number of shots must be greater than 0."
+    else:
+        assert False
diff --git a/resource_estimator/src/system/tests.rs b/resource_estimator/src/system/tests.rs
index 6f487fdb..7633e827 100644
--- a/resource_estimator/src/system/tests.rs
+++ b/resource_estimator/src/system/tests.rs
@@ -472,21 +472,21 @@ pub fn test_hubbard_e2e_increasing_max_num_qubits() -> Result<()> {
 fn prepare_chemistry_estimation_with_expected_majorana(
 ) -> PhysicalResourceEstimation<Protocol, TFactoryBuilder, LogicalResourceCounts> {
     let ftp = Protocol::floquet_code();
-    let qubit = Rc::new(PhysicalQubit::qubit_maj_ns_e4());
+    let qubit = Rc::new(PhysicalQubit::qubit_maj_ns_e6());
 
     let value = r#"{
         "numQubits": 1318,
-        "tCount": 55321680,
-        "rotationCount": 205612244,
-        "rotationDepth": 205151230,
-        "cczCount": 134678785904,
+        "tCount": 96,
+        "rotationCount": 11987084,
+        "rotationDepth": 11986482,
+        "cczCount": 67474931068,
         "ccixCount": 0,
-        "measurementCount": 1374743748
+        "measurementCount": 63472407520
     }"#;
 
     let counts: LogicalResourceCounts = serde_json::from_str(value).expect("json should be valid");
 
-    let partitioning = ErrorBudgetSpecification::Total(1e-3)
+    let partitioning = ErrorBudgetSpecification::Total(0.01)
         .partitioning(&counts)
         .expect("partitioning should succeed");
     PhysicalResourceEstimation::new(
@@ -540,33 +540,32 @@ pub fn test_chemistry_based_max_duration() -> Result<()> {
     // constraint is not violated
     assert!(result.runtime() <= max_duration_in_nanoseconds);
 
-    assert_eq!(logical_qubit.code_parameter(), &19);
-    assert_eq!(logical_qubit.logical_cycle_time(), 5700);
+    assert_eq!(logical_qubit.code_parameter(), &9);
+    assert_eq!(logical_qubit.logical_cycle_time(), 2700);
 
     assert_eq!(result.layout_overhead().logical_qubits(), 2740);
-    assert_eq!(result.algorithmic_logical_depth(), 411_005_967_364);
-    assert_eq!(part.copies(), 2);
-    assert_eq!(result.physical_qubits_for_factories(), 572_000);
-    assert_eq!(result.physical_qubits_for_algorithm(), 4_351_120);
-    assert_eq!(result.physical_qubits(), 4_923_120);
+    assert_eq!(result.algorithmic_logical_depth(), 266_172_890_508);
+    assert_eq!(part.copies(), 1);
+    assert_eq!(result.physical_qubits_for_factories(), 16_640);
+    assert_eq!(result.physical_qubits_for_algorithm(), 1_063_120);
+    assert_eq!(result.physical_qubits(), 1_079_760);
 
-    assert_eq!(result.runtime(), 22_363_183_367_607_300);
+    assert_eq!(result.runtime(), 10_050_079_976_035_200);
 
-    assert_eq!(tfactory.physical_qubits(), 286_000);
-    assert_eq!(tfactory.num_rounds(), 4);
-    assert_eq!(tfactory.num_units_per_round(), vec![19994, 275, 16, 1]);
+    assert_eq!(tfactory.physical_qubits(), 16_640);
+    assert_eq!(tfactory.num_rounds(), 3);
+    assert_eq!(tfactory.num_units_per_round(), vec![303, 16, 1]);
     assert_eq!(
         tfactory.unit_names(),
         vec![
-            String::from("15-to-1 space efficient"),
-            String::from("15-to-1 space efficient"),
             String::from("15-to-1 RM prep"),
+            String::from("15-to-1 space efficient"),
             String::from("15-to-1 RM prep"),
         ]
     );
     assert_eq!(
         tfactory.code_parameter_per_round(),
-        vec![Some(&1), Some(&3), Some(&5), Some(&15)]
+        vec![Some(&1), Some(&3), Some(&7)]
     );
 
     assert_eq!(
@@ -598,32 +597,31 @@ pub fn test_chemistry_based_max_num_qubits() -> Result<()> {
     // constraint is not violated
     assert!(result.physical_qubits() <= max_num_qubits);
 
-    assert_eq!(logical_qubit.code_parameter(), &19);
-    assert_eq!(logical_qubit.logical_cycle_time(), 5700);
+    assert_eq!(logical_qubit.code_parameter(), &9);
+    assert_eq!(logical_qubit.logical_cycle_time(), 2700);
 
     assert_eq!(result.layout_overhead().logical_qubits(), 2740);
-    assert_eq!(result.algorithmic_logical_depth(), 411_005_967_364);
-    assert_eq!(part.copies(), 2);
-    assert_eq!(result.physical_qubits_for_factories(), 572_000);
-    assert_eq!(result.physical_qubits_for_algorithm(), 4_351_120);
-    assert_eq!(result.physical_qubits(), 4_923_120);
-    assert_eq!(result.runtime(), 22_363_183_367_607_300);
-
-    assert_eq!(tfactory.physical_qubits(), 286_000);
-    assert_eq!(tfactory.num_rounds(), 4);
-    assert_eq!(tfactory.num_units_per_round(), vec![19994, 275, 16, 1]);
+    assert_eq!(result.algorithmic_logical_depth(), 266_172_890_508);
+    assert_eq!(part.copies(), 231);
+    assert_eq!(result.physical_qubits_for_factories(), 3_843_840);
+    assert_eq!(result.physical_qubits_for_algorithm(), 1_063_120);
+    assert_eq!(result.physical_qubits(), 4_906_960);
+    assert_eq!(result.runtime(), 718_666_804_371_600);
+
+    assert_eq!(tfactory.physical_qubits(), 16_640);
+    assert_eq!(tfactory.num_rounds(), 3);
+    assert_eq!(tfactory.num_units_per_round(), vec![303, 16, 1]);
     assert_eq!(
         tfactory.unit_names(),
         vec![
-            String::from("15-to-1 space efficient"),
-            String::from("15-to-1 space efficient"),
             String::from("15-to-1 RM prep"),
+            String::from("15-to-1 space efficient"),
             String::from("15-to-1 RM prep"),
         ]
     );
     assert_eq!(
         tfactory.code_parameter_per_round(),
-        vec![Some(&1), Some(&3), Some(&5), Some(&15)]
+        vec![Some(&1), Some(&3), Some(&7)]
     );
 
     assert_eq!(
@@ -802,6 +800,25 @@ fn prepare_ising20x20_estimation_with_pessimistic_gate_based(
     )
 }
 
+#[test]
+fn test_chemistry_based_max_factories() {
+    for max_factories in 1..=14 {
+        let mut estimation = prepare_chemistry_estimation_with_expected_majorana();
+        estimation.set_max_factories(max_factories);
+
+        let result = estimation.estimate().expect("failed to estimate");
+        let actual_factories = result.factory_parts()[0]
+            .as_ref()
+            .expect("has factories")
+            .copies();
+
+        assert!(
+            actual_factories <= max_factories,
+            "failed for {max_factories} maximum factories"
+        );
+    }
+}
+
 #[test]
 fn build_frontier_test() {
     let estimation = prepare_ising20x20_estimation_with_pessimistic_gate_based();
diff --git a/samples/testing/operations/README.md b/samples/testing/operations/README.md
new file mode 100644
index 00000000..86a7244b
--- /dev/null
+++ b/samples/testing/operations/README.md
@@ -0,0 +1,34 @@
+# Testing Operations in the QDK
+This sample project demonstrates different approaches to testing operations in the QDK, both via Python and within Q# itself.
+
+## Testing Methods
+
+There are two primary ways to test operations in the QDK:
+
+1. **Using Operation Matrix Representation:**
+   - Use the `dump_operation` Python API to retrieve the operation's representation as a matrix and compare it against the expected matrix.
+
+2. **Q# `Fact` Assertions:**
+   - Use a `Fact` function in your Q# code that uses the `CheckOperationsAreEqual` operation to verify if two operations are identical up to a global phase. The `Fact` function asserts that the check returns `true`.
+
+## Project Structure
+This sample project is a multi-file Q# project that showcases both testing methods. The project structure is as follows:
+
+- src
+    - `BellState.qs`: Q# file containing the `AllBellStates` operation to be tested
+    - `CustomOperation.qs`: Q# file containing the `ApplySWAP` operation to be tested
+    - `OperationEquivalence.qs`: Q# file containing the `TestEquivalence` operation to be called in Python wrapper
+- `qsharp.json`: Q# project manifest file, instructing compiler to include all files in `src` directory.
+- `test_dump_operation.py`: Python wrapper containing tests.
+
+## Installation
+- Install the `qsharp` Python package by following the instructions mentioned [here](https://learn.microsoft.com/azure/quantum/install-overview-qdk#add-support-for-python-and-jupyter-notebooks).
+- Install `pytest` Python package.
+
+## Running the sample
+Open the `samples/testing/operations` directory, and run `pytest` command.
+
+## Reference Links:
+- [Q# Testing guide](https://learn.microsoft.com/azure/quantum/user-guide/testing-debugging).
+- [Getting started with the QDK](https://learn.microsoft.com/azure/quantum/install-overview-qdk)
+- [Getting started with Pytest](https://docs.pytest.org/en/stable/getting-started.html)
diff --git a/samples/testing/operations/qsharp.json b/samples/testing/operations/qsharp.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/samples/testing/operations/qsharp.json
@@ -0,0 +1,1 @@
+{}
diff --git a/samples/testing/operations/src/BellState.qs b/samples/testing/operations/src/BellState.qs
new file mode 100644
index 00000000..4fe97537
--- /dev/null
+++ b/samples/testing/operations/src/BellState.qs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace BellState {
+    /// # Summary
+    /// Operation that generates all bell states for testing with `dump_operation.py`.
+    ///
+    /// # Input
+    /// ## qs
+    /// Input qubit register
+    ///
+    /// ## choice
+    /// Bell state to construct.
+    /// 0: |Φ+〉(PhiPlus)
+    /// 1: |Φ-〉(PhiMinus)
+    /// 2: |Ψ+〉(PsiPlus)
+    /// 3: |Ψ-〉(PsiMinus)
+
+    operation AllBellStates(qs : Qubit[], choice : Int) : Unit is Ctl + Adj {
+        open Microsoft.Quantum.Convert;
+
+        H(qs[0]);
+        CNOT(qs[0], qs[1]);
+
+        let bitmask = IntAsBoolArray(choice, 2);
+        if bitmask[1] {
+            X(qs[1]);
+        }
+
+        if bitmask[0] {
+            Z(qs[0]);
+        }
+
+    }
+}
diff --git a/samples/testing/operations/src/CustomOperation.qs b/samples/testing/operations/src/CustomOperation.qs
new file mode 100644
index 00000000..8e2bf3a0
--- /dev/null
+++ b/samples/testing/operations/src/CustomOperation.qs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace CustomOperation {
+    // # Summary
+    /// CNOT based operation for testing with `TestEquivalence` operation.
+    ///
+    /// # Input
+    /// ## q1
+    /// First input qubit
+    /// ## q2
+    /// Second input qubit
+    operation ApplySWAP(q1 : Qubit, q2 : Qubit) : Unit is Ctl + Adj {
+        CNOT(q1, q2); // q1, q1 xor q2
+        CNOT(q2, q1); // q2, q1 xor q2
+        CNOT(q1, q2); // q2, q1
+    }
+}
diff --git a/samples/testing/operations/src/OperationEquivalence.qs b/samples/testing/operations/src/OperationEquivalence.qs
new file mode 100644
index 00000000..beec0787
--- /dev/null
+++ b/samples/testing/operations/src/OperationEquivalence.qs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace OperationEquivalence {
+    open Microsoft.Quantum.Diagnostics;
+    open CustomOperation;
+    /// # Summary
+    /// Verifies the equivalence of quantum operations up to a global phase using `Fact` function
+    /// and the `CheckOperationsAreEqual` operation. You can either run this here,
+    /// by clicking `Run` in VsCode or call `TestEquivalence` operation in Python.
+    @EntryPoint()
+    operation TestEquivalence() : Unit {
+        let actual = qs => CustomOperation.ApplySWAP(qs[0], qs[1]);
+        let expected = qs => SWAP(qs[0], qs[1]);
+        Fact(CheckOperationsAreEqual(2, actual, expected), "Actual and expected operation should be same");
+    }
+}
diff --git a/samples/testing/operations/test_operations.py b/samples/testing/operations/test_operations.py
new file mode 100644
index 00000000..cf2664e8
--- /dev/null
+++ b/samples/testing/operations/test_operations.py
@@ -0,0 +1,109 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import pytest
+import qsharp
+from qsharp.utils import dump_operation
+
+@pytest.fixture(autouse=True)
+def setup():
+    """Fixture to execute before a test is run"""
+    # Setting the project root to current folder.
+    qsharp.init(project_root=".")
+    yield # this is where the testing happens
+
+def test_empty_operation() -> None:
+    res = dump_operation("qs => ()", 1)
+    assert res == [
+        [1, 0],
+        [0, 1],
+    ]
+
+
+def test_single_qubit_not_gate() -> None:
+    res = dump_operation("qs => X(qs[0])", 1)
+    assert res == [
+        [0, 1],
+        [1, 0],
+    ]
+
+def test_single_qubit_hadamard_gate() -> None:
+    res = dump_operation("qs => H(qs[0])", 1)
+    assert res == [
+        [0.707107, 0.707107],
+        [0.707107, -0.707107],
+    ]
+
+def test_two_qubit_cnot_gate() -> None:
+    res = dump_operation("qs => CNOT(qs[0], qs[1])", 2)
+    assert res == [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 0, 1],
+        [0, 0, 1, 0],
+    ]
+
+def test_custom_operation() -> None:
+    qsharp.eval(
+        "operation ApplySWAP(qs : Qubit[]) : Unit is Ctl + Adj { SWAP(qs[0], qs[1]); }"
+    )
+
+    res = dump_operation("ApplySWAP", 2)
+    assert res == [
+        [1, 0, 0, 0],
+        [0, 0, 1, 0],
+        [0, 1, 0, 0],
+        [0, 0, 0, 1],
+    ]
+
+def test_operation_no_args_in_qsharp_file() -> None:
+    res = dump_operation("qs => CustomOperation.ApplySWAP(qs[0], qs[1])", 2)
+    assert res == [
+        [1, 0, 0, 0],
+        [0, 0, 1, 0],
+        [0, 1, 0, 0],
+        [0, 0, 0, 1],
+    ]
+
+def test_operation_with_args_in_qsharp_file() -> None:
+    res0 = dump_operation("BellState.AllBellStates(_, 0)", 2)
+
+    assert res0 == [
+        [0.707107, 0.0, 0.707107, 0.0],
+        [0.0, 0.707107, 0.0, 0.707107],
+        [0.0, 0.707107, 0.0, -0.707107],
+        [0.707107, 0.0, -0.707107, 0.0],
+    ]
+
+    res1 = dump_operation("BellState.AllBellStates(_, 1)", 2)
+
+    assert res1 == [
+        [0.707107, 0.0, 0.707107, 0.0],
+        [0.0, 0.707107, 0.0, 0.707107],
+        [0.0, -0.707107, 0.0, 0.707107],
+        [-0.707107, 0.0, 0.707107, 0.0],
+    ]
+
+    res2 = dump_operation("BellState.AllBellStates(_, 2)", 2)
+
+    assert res2 == [
+        [0.0, 0.707107, 0.0, 0.707107],
+        [0.707107, 0.0, 0.707107, 0.0],
+        [0.707107, 0.0, -0.707107, 0.0],
+        [0.0, 0.707107, 0.0, -0.707107],
+    ]
+
+    res3 = dump_operation("BellState.AllBellStates(_, 3)", 2)
+
+    assert res3 == [
+        [0.0, 0.707107, 0.0, 0.707107],
+        [0.707107, 0.0, 0.707107, 0.0],
+        [-0.707107, 0.0, 0.707107, 0.0],
+        [0.0, -0.707107, 0.0, 0.707107],
+    ]
+
+
+def test_operation_equivalence_using_fact() -> None:
+    qsharp.eval(
+        "OperationEquivalence.TestEquivalence()"
+    )
diff --git a/samples/testing/operations/test_requirements.txt b/samples/testing/operations/test_requirements.txt
new file mode 100644
index 00000000..e079f8a6
--- /dev/null
+++ b/samples/testing/operations/test_requirements.txt
@@ -0,0 +1,1 @@
+pytest
diff --git a/vscode/test/suites/extensionUtils.ts b/vscode/test/suites/extensionUtils.ts
index 736513ee..947cd05b 100644
--- a/vscode/test/suites/extensionUtils.ts
+++ b/vscode/test/suites/extensionUtils.ts
@@ -8,8 +8,15 @@ import { type ExtensionApi } from "../../src/extension";
 const extensionLogLevel = "warn";
 
 export async function activateExtension() {
-  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  const ext = vscode.extensions.getExtension("quantum.qsharp-lang-vscode-dev")!;
+  // Check for pre-release or stable builds of the extension, as could be in release pipeline
+  const ext =
+    vscode.extensions.getExtension("quantum.qsharp-lang-vscode-dev") ??
+    vscode.extensions.getExtension("quantum.qsharp-lang-vscode");
+
+  if (!ext) {
+    throw new Error("qsharp extension not found");
+  }
+
   if (ext.isActive) {
     return;
   }