From 97c561e99020f9dbbf17fc4d01689ce776634170 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Mon, 9 Sep 2024 11:10:00 -0400 Subject: [PATCH] Add partial implementation of xx-decomposer in Rust --- crates/accelerate/src/gates.rs | 50 +++++ crates/accelerate/src/lib.rs | 2 + crates/accelerate/src/two_qubit_decompose.rs | 11 +- .../accelerate/src/xx_decompose/circuits.rs | 173 ++++++++++++++++++ .../accelerate/src/xx_decompose/decomposer.rs | 28 +++ crates/accelerate/src/xx_decompose/mod.rs | 18 ++ crates/accelerate/src/xx_decompose/types.rs | 89 +++++++++ .../accelerate/src/xx_decompose/utilities.rs | 34 ++++ crates/accelerate/src/xx_decompose/weyl.rs | 137 ++++++++++++++ rust-toolchain.toml | 2 +- 10 files changed, 538 insertions(+), 6 deletions(-) create mode 100644 crates/accelerate/src/gates.rs create mode 100644 crates/accelerate/src/xx_decompose/circuits.rs create mode 100644 crates/accelerate/src/xx_decompose/decomposer.rs create mode 100644 crates/accelerate/src/xx_decompose/mod.rs create mode 100644 crates/accelerate/src/xx_decompose/types.rs create mode 100644 crates/accelerate/src/xx_decompose/utilities.rs create mode 100644 crates/accelerate/src/xx_decompose/weyl.rs diff --git a/crates/accelerate/src/gates.rs b/crates/accelerate/src/gates.rs new file mode 100644 index 000000000000..749552302584 --- /dev/null +++ b/crates/accelerate/src/gates.rs @@ -0,0 +1,50 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//use num_complex::{Complex64, ComplexFloat}; +use num_complex::Complex64; +use ndarray::prelude::*; +// For Complex64::zero() +// use num_traits::Zero; +use qiskit_circuit::util::{c64, C_ZERO}; + +pub(crate) fn rz_matrix(theta: f64) -> Array2 { + let ilam2 = c64(0., 0.5 * theta); + array![[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] +} + +pub(crate) fn rxx_matrix(theta: f64) -> Array2 { + let theta2 = theta / 2.0; +// let cos = c64::new(theta2.cos(), 0.0); + let cos = c64(theta2.cos(), 0.0); + let isin = c64(0.0, theta2.sin()); + let cz = C_ZERO; + array![ + [cos, cz, cz, -isin], + [cz, cos, -isin, cz], + [cz, -isin, cos, cz], + [-isin, cz, cz, cos], + ] +} + +pub(crate) fn ryy_matrix(theta: f64) -> Array2 { + let theta2 = theta / 2.0; + let cos = Complex64::new(theta2.cos(), 0.0); + let isin = Complex64::new(0.0, theta2.sin()); + let cz = C_ZERO; + array![ + [cos, cz, cz, isin], + [cz, cos, -isin, cz], + [cz, -isin, cos, cz], + [isin, cz, cz, cos], + ] +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 9111f932e270..e96f66a7c206 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -45,6 +45,8 @@ pub mod two_qubit_decompose; pub mod uc_gate; pub mod utils; pub mod vf2_layout; +mod gates; +mod xx_decompose; mod rayon_ext; #[cfg(test)] diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 92ad4724682f..017ea39a7762 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -46,6 +46,7 @@ use crate::euler_one_qubit_decomposer::{ }; use crate::utils; use crate::QiskitError; +use crate::gates::{rz_matrix}; use rand::prelude::*; use rand_distr::StandardNormal; @@ -144,7 +145,7 @@ pub trait TraceToFidelity { impl TraceToFidelity for Complex64 { fn trace_to_fid(self) -> f64 { - (4.0 + self.abs().powi(2)) / 20.0 + (4.0 + self.norm_sqr()) / 20.0 } } @@ -296,10 +297,10 @@ fn ry_matrix(theta: f64) -> Array2 { array![[cos, -sin], [sin, cos]] } -fn rz_matrix(theta: f64) -> Array2 { - let ilam2 = c64(0., 0.5 * theta); - array![[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] -} +// fn rz_matrix(theta: f64) -> Array2 { +// let ilam2 = c64(0., 0.5 * theta); +// array![[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] +// } fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { let identity = aview2(&ONE_QUBIT_IDENTITY); diff --git a/crates/accelerate/src/xx_decompose/circuits.rs b/crates/accelerate/src/xx_decompose/circuits.rs new file mode 100644 index 000000000000..b586a38b1957 --- /dev/null +++ b/crates/accelerate/src/xx_decompose/circuits.rs @@ -0,0 +1,173 @@ +use std::f64::consts::PI; +use ndarray::prelude::*; +use ndarray::linalg::kron; +use qiskit_circuit::circuit_data::CircuitData; +use crate::xx_decompose::utilities::{safe_acos, Square, EPSILON}; +use crate::xx_decompose::weyl; +use crate::gates::{rz_matrix, rxx_matrix, ryy_matrix}; +use crate::xx_decompose::types::{GateData, Coordinate}; + +const PI2 : f64 = PI / 2.0; + +fn decompose_xxyy_into_xxyy_xx( + a_target: f64, + b_target: f64, + a_source: f64, + b_source: f64, + interaction: f64, +) -> [f64; 6] { + let cplus = (a_source + b_source).cos(); + let cminus = (a_source - b_source).cos(); + let splus = (a_source + b_source).sin(); + let sminus = (a_source - b_source).sin(); + let ca = interaction.cos(); + let sa = interaction.sin(); + + let uplusv = + 1. / 2. * + safe_acos(cminus.sq() * ca.sq() + sminus.sq() * sa.sq() - (a_target - b_target).cos().sq(), + 2. * cminus * ca * sminus * sa); + + let uminusv = + 1. / 2. + * safe_acos( + cplus.sq() * ca.sq() + splus.sq() * sa.sq() - (a_target + b_target).cos().sq(), + 2. * cplus * ca * splus * sa, + ); + + let (u, v) = ((uplusv + uminusv) / 2., (uplusv - uminusv) / 2.); + + let middle_matrix = rxx_matrix(2. * a_source) + .dot(&ryy_matrix(2. * b_source)) + .dot(&kron(&rz_matrix(2. * u), &rz_matrix(2. * v))) + .dot(&rxx_matrix(2. * interaction)); + + let phase_solver = { + let q = 1. / 4.; + let mq = - 1. / 4.; + array![ + [q, q, q, q], + [q, mq, mq, q], + [q, q, mq, mq], + [q, mq, q, mq], + ] + }; + let inner_phases = array![ + middle_matrix[[0, 0]].arg(), middle_matrix[[1, 1]].arg(), + middle_matrix[[1, 2]].arg() + PI2, + middle_matrix[[0, 3]].arg() + PI2, + ]; + let [mut r, mut s, mut x, mut y] = { + let p = phase_solver.dot(&inner_phases); + [p[0],p[1],p[2],p[3]] + }; + + let generated_matrix = + kron(&rz_matrix(2. * r), &rz_matrix(2. * s)) + .dot(&middle_matrix) + .dot(&kron(&rz_matrix(2. * x), &rz_matrix(2. * y))); + + // If there's a phase discrepancy, need to conjugate by an extra Z/2 (x) Z/2. + if ((generated_matrix[[3, 0]].arg().abs() - PI2) < 0.01 + && a_target > b_target) + || ((generated_matrix[[3, 0]].arg().abs() + PI2) < 0.01 + && a_target < b_target) { + x += PI / 4.; + y += PI / 4.; + r -= PI / 4.; + s -= PI / 4.; + } + [r, s, u, v, x, y] +} + +// Builds a single step in an XX-based circuit. +// +// `source` and `target` are positive canonical coordinates; `strength` is the interaction strength +// at this step in the circuit as a canonical coordinate (so that CX = RZX(pi/2) corresponds to +// pi/4); and `embodiment` is a Qiskit circuit which enacts the canonical gate of the prescribed +// interaction `strength`. +fn xx_circuit_step(source: &Coordinate, strength: f64, target: &Coordinate, + embodiment: CircuitData) -> Result<(), String> { + + let mut permute_source_for_overlap: Option> = None; + let mut permute_target_for_overlap: Option> = None; + + for reflection_name in &weyl::REFLECTION_NAMES { + let (reflected_source_coord, source_reflection, reflection_phase_shift) = weyl::apply_reflection(*reflection_name, source); + for source_shift_name in &weyl::SHIFT_NAMES { + let (shifted_source_coord, source_shift, shift_phase_shift) = weyl::apply_shift( + *source_shift_name, &reflected_source_coord); + + // check for overlap, back out permutation + let (mut source_shared, mut target_shared) = (None, None); + for (i, j) in [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)] { + if shifted_source_coord.distance(target, i, j) < EPSILON || + shifted_source_coord.distance(target, j, i) < EPSILON + { + (source_shared, target_shared) = (Some(i), Some(j)); + break; + } + } + if source_shared.is_none() { + continue; + } + // Return [0, 1, 2] with `iex` deleted. + fn exclude_one(iex: i32) -> [usize; 2] { + let mut pair = [-1, -1]; + let mut j = 0; + for i in 0..3 { + if i != iex { + pair[j] = i; + j += 1; + } + } + [pair[0] as usize, pair[1] as usize] + } // exclude_one + + let [source_first, source_second] = exclude_one(source_shared.unwrap()); + let [target_first, target_second] = exclude_one(target_shared.unwrap()); + + let decomposed_coords = decompose_xxyy_into_xxyy_xx( + target[target_first], + target[target_second], + shifted_source_coord[source_first], + shifted_source_coord[source_second], + strength, + ); + + if decomposed_coords.iter().any(|val| val.is_nan()) { + continue; + } + + let [r, s, u, v, x, y] = decomposed_coords; + // OK: this combination of things works. + // save the permutation which rotates the shared coordinate into ZZ. + permute_source_for_overlap = weyl::canonical_rotation_circuit(source_first, source_second); + permute_target_for_overlap = weyl::canonical_rotation_circuit(target_first, target_second); + break; + } // for source_shift_name + + if permute_source_for_overlap.is_some() { + break; + } + } // for reflection_name + + if permute_source_for_overlap.is_none() { + // TODO: Decide which error to return. + return Err(format!("Error during RZX decomposition: Could not find a suitable Weyl reflection to match {:?} to {:?} along {:?}.", + source, target, strength + )); + } + +// the basic formula we're trying to work with is: +// target^p_t_f_o = +// rs * (source^s_reflection * s_shift)^p_s_f_o * uv * operation * xy +// but we're rearranging it into the form +// target = affix source prefix +// and computing just the prefix / affix circuits. + + + return Ok(()) +} + +// fn canonical_xx_circuit(target, strength_sequence, basis_embodiments): diff --git a/crates/accelerate/src/xx_decompose/decomposer.rs b/crates/accelerate/src/xx_decompose/decomposer.rs new file mode 100644 index 000000000000..b7efbef79f5f --- /dev/null +++ b/crates/accelerate/src/xx_decompose/decomposer.rs @@ -0,0 +1,28 @@ +use crate::xx_decompose::utilities::Square; + +struct Point { + a: f64, + b: f64, + c: f64, +} + +/// Computes the infidelity distance between two points p, q expressed in positive canonical +/// coordinates. +fn _average_infidelity(p: Point, q: Point) -> f64 { + let Point { + a: a0, + b: b0, + c: c0, + } = p; + let Point { + a: a1, + b: b1, + c: c1, + } = q; + + 1. - 1. / 20. + * (4. + + 16. + * ((a0 - a1).cos().sq() * (b0 - b1).cos().sq() * (c0 - c1).cos().sq() + + (a0 - a1).sin().sq() * (b0 - b1).sin().sq() * (c0 - c1).sin().sq())) +} diff --git a/crates/accelerate/src/xx_decompose/mod.rs b/crates/accelerate/src/xx_decompose/mod.rs new file mode 100644 index 000000000000..715dc400e751 --- /dev/null +++ b/crates/accelerate/src/xx_decompose/mod.rs @@ -0,0 +1,18 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +pub mod circuits; +pub mod decomposer; +pub mod utilities; +mod types; +mod weyl; +mod polytopes; diff --git a/crates/accelerate/src/xx_decompose/types.rs b/crates/accelerate/src/xx_decompose/types.rs new file mode 100644 index 000000000000..a6b38e6789b5 --- /dev/null +++ b/crates/accelerate/src/xx_decompose/types.rs @@ -0,0 +1,89 @@ +use std::f64::consts::PI; +use std::ops::Index; +use num_complex::Complex64; +use qiskit_circuit::operations::StandardGate; + +// One-qubit, one-or-zero parameter, gates +// Only rotation gates and H gate are supported fully. +pub(crate) struct GateData { + pub gate: StandardGate, + pub param: Option, + pub qubit: i32, +} + +impl GateData { + // TODO: This method works, but is obsolete. Need fewer ways to instantiate. + pub(crate) fn with_param(gate: StandardGate, param: f64, qubit: i32) -> GateData { + GateData { gate, param: Some(param), qubit } + } +} + +// A circuit composed of 1Q gates. +// Circuit may have more than one qubit. +pub(crate) struct Circuit1Q { + gates: Vec, + phase: Complex64, +} + +impl Circuit1Q { + + // Reverse the quantum circuit by reversing the order of gates, + // reflecting the parameter (angle) in rotation gates, and reversing + // the circuit phase. This is correct for the gates used in this decomposer. + // This decomposer has only rotation gates and H gates until the last step, + // at which point we introduce Python and CircuitData. + fn reverse(&self) -> Circuit1Q { + let gates: Vec = self.gates + .iter() + .rev() + .map(|g| match g { + // Reverse rotations + GateData{ gate, param: Some(param), qubit } => GateData { gate: *gate, param: Some(-param), qubit: *qubit }, + // Copy other gates + GateData{ gate, param: None, qubit } => GateData { gate: *gate, param: None, qubit: *qubit }, + }) + .collect(); + Circuit1Q {gates, phase: -self.phase} + } +} + +// TODO: Need Display for user-facing error message. +#[derive(Debug)] +pub(crate) struct Coordinate { + data: [f64; 3] +} + +impl Coordinate { + + pub(crate) fn reflect(&self, scalars: &[i32; 3]) -> Coordinate { + Coordinate { + data: [self.data[0] * (scalars[0]) as f64, + self.data[1] * (scalars[1]) as f64, + self.data[2] * (scalars[2]) as f64,] + } + } + + pub(crate) fn shift(&self, scalars: &[i32; 3]) -> Coordinate { + let pi2 = PI / 2.0; + Coordinate { + data: [pi2 * self.data[0] + (scalars[0]) as f64, + pi2 * self.data[1] + (scalars[1]) as f64, + pi2 * self.data[2] + (scalars[2]) as f64,] + } + } + + /// Unsigned distance between self, axis `i` and other, axis `j`. + pub(crate) fn distance(&self, other: &Self, i: i32, j: i32) -> f64 { + let d = self.data[i as usize] - other.data[j as usize]; + (d.abs() % PI).abs() + } +} + +// Forward indexing into `Coordinate` to the field `data`. +impl Index for Coordinate { + type Output = f64; + + fn index(&self, index: usize) -> &Self::Output { + &self.data[index] + } +} diff --git a/crates/accelerate/src/xx_decompose/utilities.rs b/crates/accelerate/src/xx_decompose/utilities.rs new file mode 100644 index 000000000000..7c309c5fbd06 --- /dev/null +++ b/crates/accelerate/src/xx_decompose/utilities.rs @@ -0,0 +1,34 @@ +pub(crate) const EPSILON: f64 = 1e-6; + +use std::f64::consts::PI; + +// The logic in `safe_acos` is copied from the Python original. +// The following comment is copied as well. +// TODO: THIS IS A STOPGAP!!! +/// Has the same behavior as `f64::acos` except that an +/// argument a bit greater than `1` or less than `-1` is +/// valid and returns the value at `1` or `-1`. +/// Larger or smaller arguments will cause the same error to be +/// raised that `f64::acos` raises. +pub(crate) fn safe_acos(numerator: f64, denominator: f64) -> f64 { + let threshold: f64 = 0.005; + if numerator.abs() > denominator.abs() { + if (numerator - denominator).abs() < threshold { + return 0.0; + } else if (numerator + denominator).abs() < threshold { + return PI; + } + } + (numerator / denominator).acos() +} + +// powi(2) everywhere is a bit ugly +pub trait Square { + fn sq(&self) -> f64; +} + +impl Square for f64 { + fn sq(&self) -> f64 { + self * self + } +} diff --git a/crates/accelerate/src/xx_decompose/weyl.rs b/crates/accelerate/src/xx_decompose/weyl.rs new file mode 100644 index 000000000000..3780d06bcff8 --- /dev/null +++ b/crates/accelerate/src/xx_decompose/weyl.rs @@ -0,0 +1,137 @@ +use std::f64::consts::PI; +use num_complex::Complex64; +use qiskit_circuit::operations::StandardGate; +use crate::xx_decompose::types::{Coordinate, GateData}; +use qiskit_circuit::util::{C_ONE, C_M_ONE, M_IM, IM}; + +// These names aren't very functional. I think they were in the original +// Python source for debugging purposes. But they were part of the data +// structure. We preserve them for now. +#[derive(Clone, Copy)] +pub(crate) enum ReflectionName { + NoReflection = 0, + ReflectXXYY = 1, + ReflectXXZZ = 2, + ReflectYYZZ = 3, +} + +pub(crate) static REFLECTION_NAMES: [ReflectionName; 4] = [ReflectionName::NoReflection, ReflectionName::ReflectXXYY, + ReflectionName::ReflectXXZZ, ReflectionName::ReflectYYZZ,]; + + +// A table of available reflection transformations on canonical coordinates. +// Entries take the form +// readable_name: (reflection scalars, global phase, [gate constructors]), +// where reflection scalars (a, b, c) model the map (x, y, z) |-> (ax, by, cz), +// global phase is a complex unit, and gate constructors are applied in sequence +// and by conjugation to the first qubit and are passed pi as a parameter. +static reflection_options: [(&[i32; 3], Complex64, &[StandardGate]); 4] = + [(&[1, 1, 1], C_ONE, &[]), // 0 + (&[-1, -1, 1], C_ONE, &[StandardGate::RZGate]), // 1 + (&[-1, 1, -1], C_ONE, &[StandardGate::RYGate]), // 2 + (&[1, -1, -1], C_ONE, &[StandardGate::RXGate]), // 3 + ]; + + +#[derive(Clone, Copy)] +pub(crate) enum ShiftName { + NoShift = 0, + ZShift = 1, + YShift = 2, + YZShift = 3, + XShift = 4, + XZShift = 5, + YYShift = 6, + XYZShift = 7, +} + +pub(crate) static SHIFT_NAMES: [ShiftName; 8] = [ + ShiftName::NoShift, + ShiftName::ZShift, + ShiftName::YShift, + ShiftName::YZShift, + ShiftName::XShift, + ShiftName::XZShift, + ShiftName::YYShift, + ShiftName::XYZShift, + ]; + +static shift_options: [(&[i32; 3], Complex64, &[StandardGate]); 8] = +[(&[0, 0, 0], C_ONE, &[]), + (&[0, 0, 1], IM, &[StandardGate::RZGate]), + (&[0, 1, 0], M_IM, &[StandardGate::RYGate]), + (&[0, 1, 1], C_ONE, &[StandardGate::RYGate, StandardGate::RZGate]), + (&[1, 0, 0], M_IM, &[StandardGate::RXGate]), + (&[1, 0, 1], C_ONE, &[StandardGate::RXGate, StandardGate::RZGate]), + (&[1, 1, 0], C_M_ONE, &[StandardGate::RXGate, StandardGate::RYGate]), + (&[1, 1, 1], M_IM, &[StandardGate::RXGate, StandardGate::RYGate, StandardGate::RZGate]), + ]; + + +pub(crate) fn apply_reflection(reflection_name: ReflectionName, coordinate: &Coordinate) -> + (Coordinate, Vec, Complex64) +{ + let (reflection_scalars, reflection_phase_shift, source_reflection_gates) = + reflection_options[reflection_name as usize]; + + let reflected_coord = coordinate.reflect(reflection_scalars); + let source_reflection: Vec<_> = source_reflection_gates + .iter() + .map(|g| GateData::with_param(*g, PI, 0)) + .collect(); + return (reflected_coord, source_reflection, reflection_phase_shift) +} + +pub(crate) fn apply_shift(shift_name: ShiftName, coordinate: &Coordinate) -> + (Coordinate, Vec, Complex64) +{ + let (shift_scalars, shift_phase_shift, source_shift_gates) = + shift_options[shift_name as usize]; + let shifted_coord = coordinate.shift(shift_scalars); + + let source_shift: Vec<_> = source_shift_gates + .iter() + .flat_map(|g| [GateData::with_param(*g, PI, 0), + GateData::with_param(*g, PI, 1), + ]) + .collect(); + return (shifted_coord, source_shift, shift_phase_shift) +} + + +/// Given a pair of distinct indices 0 ≤ (first_index, second_index) ≤ 2, +/// produces a two-qubit circuit which rotates a canonical gate +/// +/// a0 XX + a1 YY + a2 ZZ +/// +/// into +/// +/// a[first] XX + a[second] YY + a[other] ZZ . +pub(crate) fn canonical_rotation_circuit(first_index: usize, second_index: usize) -> Option> { + let pi2 = Some(PI / 2.0); + let mpi2 = Some(-PI / 2.0); + let circuit = match (first_index, second_index) { + (0, 1) => return None, // Nothing to do. + (0, 2) => + vec![GateData {gate: StandardGate::RXGate, param: mpi2, qubit: 0}, + GateData {gate: StandardGate::RXGate, param: pi2, qubit: 1},], + (1, 0) => + vec![GateData {gate: StandardGate::RZGate, param: mpi2, qubit: 0}, + GateData {gate: StandardGate::RZGate, param: pi2, qubit: 1},], + (1, 2) => + vec![GateData {gate: StandardGate::RZGate, param: pi2, qubit: 0}, + GateData {gate: StandardGate::RZGate, param: pi2, qubit: 1}, + GateData {gate: StandardGate::RYGate, param: pi2, qubit: 0}, + GateData {gate: StandardGate::RYGate, param: mpi2, qubit: 1},], + (2, 0) => + vec![GateData {gate: StandardGate::RZGate, param: pi2, qubit: 0}, + GateData {gate: StandardGate::RZGate, param: pi2, qubit: 1}, + GateData {gate: StandardGate::RXGate, param: pi2, qubit: 0}, + GateData {gate: StandardGate::RXGate, param: mpi2, qubit: 1},], + (2, 1) => + vec![GateData {gate: StandardGate::RYGate, param: pi2, qubit: 0}, + GateData {gate: StandardGate::RYGate, param: mpi2, qubit: 1},], + (_, _) => unreachable!() + }; + Some(circuit) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6f581c2f9ab3..08226e8c879a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # Keep in sync with Cargo.toml's `rust-version`. -channel = "1.70" +# channel = "1.70" components = [ "cargo", "clippy",