Skip to main content

colorthief_dataset/
lib.rs

1//! Static xkcd color-hierarchy table for human-vocabulary color naming.
2//!
3//! Each entry exposes five hierarchical names (xkcd → design → common →
4//! family → kind), plus the entry's RGB and a pre-computed CIE LAB triple
5//! used by [`Color::nearest_to`] for nearest-neighbor lookup.
6//!
7//! # Why LAB is pre-computed at codegen time
8//!
9//! The sRGB → LAB pipeline involves a power function (gamma decode) and a
10//! cube root (LAB f-function), both transcendental. Computing those for
11//! all 950 entries on every query would dwarf the actual nearest-neighbor
12//! scan. Pre-computing at `cargo xtask codegen` time pushes that cost out
13//! of the hot path; the runtime cost of `nearest_to` is one sRGB→LAB on
14//! the query plus 950 squared-distance comparisons (no `sqrt` — squared
15//! distance preserves ordering).
16//!
17//! # Distance metric
18//!
19//! Choose via [`Algorithm`]; the default ([`Algorithm::Ciede2000Exact`])
20//! is the modern perceptual gold-standard. Faster alternatives are
21//! available via [`Color::nearest_to`] (Delta E 76, ~470 ns NEON) and
22//! [`Color::nearest_to_cie94`] (CIE94, ~620 ns NEON) for throughput-
23//! sensitive callers willing to trade borderline accuracy for speed.
24//!
25//! # Attribution
26//!
27//! The color hierarchy is sourced from Stitch Fix's `colornamer` (Apache
28//! 2.0); see `THIRD_PARTY_NOTICES.md` for the full upstream attribution.
29
30#![cfg_attr(not(feature = "std"), no_std)]
31#![cfg_attr(docsrs, feature(doc_cfg))]
32#![cfg_attr(docsrs, allow(unused_attributes))]
33#![deny(missing_docs)]
34// `unsafe_code` is `deny`-not-`forbid` because the per-arch NEON kernel in
35// `nearest::aarch64_neon` needs `unsafe` to call `core::arch::aarch64`
36// intrinsics (the NEON entry function is `#[target_feature(enable = "neon")]`
37// and therefore `unsafe fn`). That module carries a local
38// `#[allow(unsafe_code)]` and is the ONLY place unsafe code is allowed.
39#![deny(unsafe_code)]
40
41mod generated;
42mod nearest;
43
44pub use generated::{COLORS, Family, Kind};
45// `Algorithm` is defined below alongside `Color`; re-exported to
46// crate root for ergonomics — `colorthief_dataset::Algorithm` is
47// where users expect to find it.
48
49/// **Not a stable API.**
50///
51/// Hidden helpers used by `colorthief-dataset/benches/nearest.rs` to
52/// compare backends head-to-head. Calling these directly bypasses the
53/// dispatcher in [`Color::nearest_to`] — production code should use
54/// the public method.
55#[doc(hidden)]
56pub mod __bench {
57  pub use crate::nearest::{
58    cie94::{delta_e_94_sq, nearest_idx as cie94_nearest_idx},
59    ciede2000::{
60      delta_e_2000_sq, nearest_idx as ciede2000_nearest_idx,
61      nearest_idx_prefiltered as ciede2000_prefiltered_nearest_idx,
62    },
63    scalar::{delta_e_76_sq, nearest_idx as scalar_nearest_idx},
64  };
65
66  #[cfg(feature = "lut")]
67  pub use crate::nearest::ciede2000_lut::nearest_idx as ciede2000_lut_nearest_idx;
68
69  // `target_feature = "neon"` (not just `target_arch = "aarch64"`):
70  // see `nearest::aarch64_neon` mod decl for the
71  // `aarch64-unknown-none-softfloat` rationale.
72  #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
73  pub use crate::nearest::cie94_aarch64_neon::nearest_idx as cie94_aarch64_neon_nearest_idx;
74  #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
75  pub use crate::nearest::cie94_wasm_simd128::nearest_idx as cie94_wasm_simd128_nearest_idx;
76  #[cfg(target_arch = "x86_64")]
77  pub use crate::nearest::cie94_x86_avx2::nearest_idx as cie94_x86_avx2_nearest_idx;
78  #[cfg(target_arch = "x86_64")]
79  pub use crate::nearest::cie94_x86_avx512::nearest_idx as cie94_x86_avx512_nearest_idx;
80  #[cfg(target_arch = "x86_64")]
81  pub use crate::nearest::cie94_x86_sse41::nearest_idx as cie94_x86_sse41_nearest_idx;
82
83  #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
84  pub use crate::nearest::aarch64_neon::nearest_idx as aarch64_neon_nearest_idx;
85
86  // x86 backends are `unsafe fn` (the `#[target_feature]` attribute
87  // enforces the safety boundary). Re-export them as-is — the bench
88  // wraps the `unsafe { ... }` after a runtime feature check.
89  #[cfg(target_arch = "x86_64")]
90  pub use crate::nearest::x86_avx2::nearest_idx as x86_avx2_nearest_idx;
91  #[cfg(target_arch = "x86_64")]
92  pub use crate::nearest::x86_avx512::nearest_idx as x86_avx512_nearest_idx;
93  #[cfg(target_arch = "x86_64")]
94  pub use crate::nearest::x86_sse41::nearest_idx as x86_sse41_nearest_idx;
95
96  #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
97  pub use crate::nearest::wasm_simd128::nearest_idx as wasm_simd128_nearest_idx;
98
99  /// Public re-export of the crate-private `rgb_to_lab` so benches can
100  /// pre-convert RGB queries without duplicating the math.
101  pub fn rgb_to_lab(rgb: [u8; 3]) -> [f32; 3] {
102    super::rgb_to_lab(rgb)
103  }
104}
105
106/// One named entry in the xkcd color hierarchy.
107///
108/// Carries every column from the upstream `color_hierarchy.csv`:
109/// xkcd / design / common name, hex, and RGB triples for each level,
110/// plus the family / kind / neutrality classification. The xkcd LAB
111/// triple is pre-computed at codegen time for nearest-neighbor lookup
112/// in [`Self::nearest_to`].
113#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct Color {
115  pub(crate) name: &'static str,
116  pub(crate) hex: &'static str,
117  pub(crate) rgb: [u8; 3],
118  pub(crate) lab: [f32; 3],
119  pub(crate) design_name: &'static str,
120  pub(crate) design_hex: &'static str,
121  pub(crate) design_rgb: [u8; 3],
122  pub(crate) common_name: &'static str,
123  pub(crate) common_hex: &'static str,
124  pub(crate) common_rgb: [u8; 3],
125  pub(crate) family: Family,
126  pub(crate) kind: Kind,
127  pub(crate) is_neutral: bool,
128}
129
130impl Color {
131  /// xkcd-survey name (~950 unique values, e.g. `"burnt orange"`,
132  /// `"vermilion"`).
133  #[cfg_attr(not(tarpaulin), inline(always))]
134  pub const fn name(&self) -> &'static str {
135    self.name
136  }
137
138  /// xkcd hex string, e.g. `"#bd6c48"`.
139  #[cfg_attr(not(tarpaulin), inline(always))]
140  pub const fn hex(&self) -> &'static str {
141    self.hex
142  }
143
144  /// xkcd RGB triple, e.g. `[189, 108, 72]`. The exact 8-bit value the
145  /// xkcd survey reports for this name.
146  #[cfg_attr(not(tarpaulin), inline(always))]
147  pub const fn rgb(&self) -> [u8; 3] {
148    self.rgb
149  }
150
151  /// Pre-computed CIE LAB (D65 illuminant, 2° observer) for [`Self::rgb`].
152  /// Used internally by [`Self::nearest_to`]; exposed publicly so callers
153  /// can implement their own distance metric (e.g. CIEDE2000) on top of
154  /// the same cached values.
155  #[cfg_attr(not(tarpaulin), inline(always))]
156  pub const fn lab(&self) -> [f32; 3] {
157    self.lab
158  }
159
160  /// Coarser design-palette name (~250 unique, e.g. `"russet brown"`).
161  #[cfg_attr(not(tarpaulin), inline(always))]
162  pub const fn design_name(&self) -> &'static str {
163    self.design_name
164  }
165
166  /// Hex string for the design-palette anchor color.
167  #[cfg_attr(not(tarpaulin), inline(always))]
168  pub const fn design_hex(&self) -> &'static str {
169    self.design_hex
170  }
171
172  /// RGB triple for the design-palette anchor color (the canonical
173  /// 8-bit representation of [`Self::design_name`]). Differs from
174  /// [`Self::rgb`] when the xkcd entry sits at the edge of its
175  /// design-palette bucket.
176  #[cfg_attr(not(tarpaulin), inline(always))]
177  pub const fn design_rgb(&self) -> [u8; 3] {
178    self.design_rgb
179  }
180
181  /// Coarser still common name (~120 unique, e.g. `"sienna"`). The
182  /// search-friendly default for indexing pipelines.
183  #[cfg_attr(not(tarpaulin), inline(always))]
184  pub const fn common_name(&self) -> &'static str {
185    self.common_name
186  }
187
188  /// Hex string for the common-name anchor color.
189  #[cfg_attr(not(tarpaulin), inline(always))]
190  pub const fn common_hex(&self) -> &'static str {
191    self.common_hex
192  }
193
194  /// RGB triple for the common-name anchor color.
195  #[cfg_attr(not(tarpaulin), inline(always))]
196  pub const fn common_rgb(&self) -> [u8; 3] {
197    self.common_rgb
198  }
199
200  /// Color family classification (26 values, e.g. [`Family::Yellow`],
201  /// [`Family::BlueGreen`], [`Family::Neutral`]). Call
202  /// [`Family::as_str`] for the original CSV string.
203  #[cfg_attr(not(tarpaulin), inline(always))]
204  pub const fn family(&self) -> Family {
205    self.family
206  }
207
208  /// Color kind / texture classification (11 values, e.g.
209  /// [`Kind::NeonColor`], [`Kind::PainterlyNeutral`]). Call
210  /// [`Kind::as_str`] for the original CSV string.
211  #[cfg_attr(not(tarpaulin), inline(always))]
212  pub const fn kind(&self) -> Kind {
213    self.kind
214  }
215
216  /// `true` if the entry is classified as a neutral (vs a chromatic
217  /// color). Drives the `color_or_neutral` axis in the original Stitch
218  /// Fix taxonomy.
219  #[cfg_attr(not(tarpaulin), inline(always))]
220  pub const fn is_neutral(&self) -> bool {
221    self.is_neutral
222  }
223
224  /// Every entry in the dataset, in CSV (alphabetical-by-`name`) order.
225  #[cfg_attr(not(tarpaulin), inline(always))]
226  pub const fn all() -> &'static [&'static Color] {
227    COLORS
228  }
229
230  /// Find the entry whose pre-computed LAB is closest to the given query
231  /// RGB by **Delta E 76** (squared Euclidean LAB).
232  ///
233  /// Always returns an entry — `COLORS` is non-empty and verified at
234  /// codegen time. The scan dispatches to a per-arch SIMD backend
235  /// (NEON / AVX2 / SSE4.1 / WASM SIMD128) on every target that has
236  /// one; other targets fall through to the scalar reference. Every
237  /// backend is bit-identical — see [`crate::nearest`] for the
238  /// dispatch contract and parity tests.
239  ///
240  /// # When to use this method
241  ///
242  /// Delta E 76 is the *fast* metric: against this crate's well-
243  /// clustered 949-entry xkcd palette it picks the same named entry
244  /// as CIEDE2000 in the overwhelming majority of cases, at ~150×
245  /// the throughput of [`Algorithm::Ciede2000Exact`] (the default
246  /// returned by [`Algorithm::default`]). Reach for it when you've
247  /// measured the slower default bottlenecking real workloads and
248  /// can tolerate borderline misnamings near the gray / yellow
249  /// boundary.
250  pub fn nearest_to(rgb: [u8; 3]) -> &'static Color {
251    crate::nearest::nearest(rgb_to_lab(rgb))
252  }
253
254  /// Find the entry whose pre-computed LAB is closest to the given
255  /// query RGB by **CIEDE2000** — the modern perceptual gold-standard
256  /// colour-difference formula.
257  ///
258  /// CIEDE2000 corrects Delta E 76's known biases (over-weighting
259  /// yellows, under-weighting blues, hue-rotation in the saturated
260  /// blue region) at the cost of `atan2` / `sin` / `cos` / `exp` per
261  /// pair plus branchy hue-wraparound logic.
262  ///
263  /// # Implementation
264  ///
265  /// - With `feature = "lut"` (the default): O(1) cell lookup → small
266  ///   candidate scan via the pre-computed candidate-set LUT
267  ///   ([`crate::nearest::ciede2000_lut`]). Provably exact at u8 RGB
268  ///   resolution; ~few-hundred-ns/query.
269  /// - Without `feature = "lut"`: full-scan reference over all 949
270  ///   palette entries (~71 µs/query). Same correctness guarantee,
271  ///   slower.
272  ///
273  /// Both modes are scalar — CIEDE2000's transcendentals don't
274  /// vectorise usefully (see [`crate::nearest::ciede2000`]).
275  pub fn nearest_to_ciede2000(rgb: [u8; 3]) -> &'static Color {
276    crate::nearest::nearest_ciede2000(rgb)
277  }
278
279  /// Strict CIEDE2000 nearest-neighbor. Behaviorally equivalent to
280  /// [`Self::nearest_to_ciede2000`] — both are exact under both
281  /// feature configurations (the LUT path is provably exact, the
282  /// no-LUT path is full-scan). Retained as a distinct entry point
283  /// for API stability; consumers picking between the two by name
284  /// should prefer [`Self::nearest_to_ciede2000`].
285  pub fn nearest_to_ciede2000_exact(rgb: [u8; 3]) -> &'static Color {
286    crate::nearest::nearest_ciede2000(rgb)
287  }
288
289  /// Find the entry whose pre-computed LAB is closest to the given
290  /// query RGB by **CIE94** (Delta E 94, graphic-arts weighting).
291  ///
292  /// CIE94 sits between Delta E 76 and CIEDE2000 in both perceptual
293  /// accuracy and arithmetic cost. It uses no transcendentals beyond
294  /// `sqrt`, so unlike CIEDE2000 the formula vectorises cleanly —
295  /// SIMD backends are a planned follow-up that will mirror the
296  /// Delta E 76 module structure.
297  ///
298  /// CIE94 is **asymmetric** (the S_C / S_H scale factors depend on
299  /// the reference's chroma C₁); this implementation treats the
300  /// palette entry as the reference and the query as the sample.
301  pub fn nearest_to_cie94(rgb: [u8; 3]) -> &'static Color {
302    crate::nearest::nearest_cie94(rgb_to_lab(rgb))
303  }
304}
305
306/// Color-difference algorithm used to map an arbitrary RGB query to
307/// its nearest [`Color`] in the xkcd palette.
308///
309/// Each variant corresponds to one of the per-metric
310/// `Color::nearest_to_*` methods. The enum exists so callers can
311/// store the choice as a value and pass it to higher-level APIs like
312/// `colorthief::extract_with` without committing to a specific
313/// `Color::nearest_to_*` callsite.
314///
315/// Marked `#[non_exhaustive]` so adding a future variant (e.g. CMC,
316/// CIEDE2010) is a non-breaking change for downstream consumers.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
318#[non_exhaustive]
319#[repr(u8)]
320pub enum Algorithm {
321  /// **Delta E 76** — squared Euclidean LAB distance. Fastest by a
322  /// wide margin (~470 ns/query on Apple Silicon NEON, ~940 ns
323  /// scalar). SIMD-dispatched on every supported arch with bit-
324  /// identical parity. Recommended for search-vocabulary indexing
325  /// where throughput matters more than borderline accuracy.
326  DeltaE76,
327
328  /// **CIE94 (Delta E 94, graphic-arts weighting)** — middle ground
329  /// between Delta E 76's speed and CIEDE2000's accuracy. Uses only
330  /// `sqrt` for transcendentals, so it vectorises cleanly. ~900 ns
331  /// NEON / ~4.4 µs scalar. Asymmetric: the palette entry is the
332  /// reference, the query is the sample.
333  Cie94,
334
335  /// **CIEDE2000** — the modern perceptual gold-standard formula.
336  /// With `feature = "lut"` (the default) routes through the
337  /// candidate-set LUT (~230 ns/query, provably exact at u8 RGB);
338  /// without the feature falls back to the full-scan reference
339  /// (~71 µs/query, also provably exact). Behaviorally equivalent
340  /// to [`Self::Ciede2000Exact`] under both feature configurations.
341  Ciede2000,
342
343  /// **CIEDE2000**, retained as a distinct variant for API
344  /// stability. Behaviorally equivalent to [`Self::Ciede2000`] —
345  /// both go through the LUT when `feature = "lut"` is enabled and
346  /// the full-scan reference otherwise. **Default.** Returned by
347  /// [`Algorithm::default`] so consumers of [`Algorithm::extract`]
348  /// and crate-level entry points like `colorthief::extract` get
349  /// the perceptual gold-standard out of the box.
350  #[default]
351  Ciede2000Exact,
352}
353
354impl Algorithm {
355  /// Find the [`Color`] whose pre-computed LAB is closest to the
356  /// given RGB under this algorithm's distance metric.
357  ///
358  /// Equivalent to dispatching to the corresponding
359  /// `Color::nearest_to*` method by hand:
360  ///
361  /// | Variant                    | Equivalent call                                |
362  /// |----------------------------|------------------------------------------------|
363  /// | [`Self::DeltaE76`]         | [`Color::nearest_to`]                          |
364  /// | [`Self::Cie94`]            | [`Color::nearest_to_cie94`]                    |
365  /// | [`Self::Ciede2000`]        | [`Color::nearest_to_ciede2000`]                |
366  /// | [`Self::Ciede2000Exact`]   | [`Color::nearest_to_ciede2000_exact`]          |
367  #[inline]
368  pub fn extract(&self, rgb: [u8; 3]) -> &'static Color {
369    match self {
370      Self::DeltaE76 => Color::nearest_to(rgb),
371      Self::Cie94 => Color::nearest_to_cie94(rgb),
372      Self::Ciede2000 => Color::nearest_to_ciede2000(rgb),
373      Self::Ciede2000Exact => Color::nearest_to_ciede2000_exact(rgb),
374    }
375  }
376
377  /// Stable string identifier for this algorithm — useful for log
378  /// lines, telemetry, and search-index metadata. Mirrors the
379  /// [`Family::as_str`] / [`Kind::as_str`] convention.
380  #[inline]
381  pub const fn as_str(&self) -> &'static str {
382    match self {
383      Self::DeltaE76 => "delta-e-76",
384      Self::Cie94 => "cie94",
385      Self::Ciede2000 => "ciede2000",
386      Self::Ciede2000Exact => "ciede2000-exact",
387    }
388  }
389}
390
391/// Convert sRGB byte triple → CIE LAB (D65 illuminant, 2° observer).
392///
393/// Pipeline: byte → normalized [0, 1] → linearized (sRGB EOTF) → XYZ
394/// (sRGB→XYZ matrix, D65) → LAB (CIE 1976 transfer).
395pub(crate) fn rgb_to_lab(rgb: [u8; 3]) -> [f32; 3] {
396  let r = srgb_to_linear(rgb[0] as f32 / 255.0);
397  let g = srgb_to_linear(rgb[1] as f32 / 255.0);
398  let b = srgb_to_linear(rgb[2] as f32 / 255.0);
399
400  // sRGB → XYZ (D65, 2°). Coefficients from IEC 61966-2-1, rounded to
401  // f32 precision (~7 decimal digits — trailing zeros that clippy flagged
402  // as excessive precision are dropped here).
403  let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
404  let y = r * 0.2126729 + g * 0.7151522 + b * 0.072175;
405  let z = r * 0.0193339 + g * 0.119192 + b * 0.9503041;
406
407  // D65 reference white (CIE 1931, 2°).
408  const XN: f32 = 0.95047;
409  const YN: f32 = 1.00000;
410  const ZN: f32 = 1.08883;
411
412  let fx = lab_f(x / XN);
413  let fy = lab_f(y / YN);
414  let fz = lab_f(z / ZN);
415
416  let l = 116.0 * fy - 16.0;
417  let a = 500.0 * (fx - fy);
418  let b_lab = 200.0 * (fy - fz);
419  [l, a, b_lab]
420}
421
422/// sRGB electro-optical transfer function (gamma decode).
423fn srgb_to_linear(c: f32) -> f32 {
424  if c <= 0.04045 {
425    c / 12.92
426  } else {
427    libm::powf((c + 0.055) / 1.055, 2.4)
428  }
429}
430
431/// CIE LAB transfer function (`f` in the standard).
432fn lab_f(t: f32) -> f32 {
433  // delta = 6/29; threshold where the linear/cube-root segments meet.
434  const DELTA_CUBED: f32 = 216.0 / 24389.0; // (6/29)^3
435  const KAPPA_OVER_3: f32 = 841.0 / 108.0; // 1 / (3 * (6/29)^2)
436  const OFFSET: f32 = 4.0 / 29.0;
437  if t > DELTA_CUBED {
438    libm::cbrtf(t)
439  } else {
440    KAPPA_OVER_3 * t + OFFSET
441  }
442}
443
444#[cfg(test)]
445mod tests {
446  use super::*;
447
448  /// The dataset must always be non-empty (xtask validates at codegen
449  /// time, but a runtime smoke test catches a regression where someone
450  /// edits `generated.rs` by hand).
451  #[test]
452  fn dataset_is_non_empty() {
453    assert!(!COLORS.is_empty());
454  }
455
456  /// Snapshot the entry count. Acts as a canary if upstream colornamer
457  /// updates the CSV: a count change is a deliberate regen, not a silent
458  /// drift. Updating this number is the right action when intentional.
459  #[test]
460  fn dataset_entry_count_matches_csv() {
461    assert_eq!(
462      COLORS.len(),
463      949,
464      "regenerate via `cargo xtask codegen` if the upstream CSV changed",
465    );
466  }
467
468  /// Pure sRGB red must map to an entry that's at least red-flavored.
469  /// Strict equality on the xkcd label is fragile (the closest match
470  /// depends on the palette and could be `"red"`, `"bright red"`,
471  /// `"true red"`, etc.); the family is a stable invariant.
472  #[test]
473  fn nearest_to_pure_red_is_in_red_family() {
474    let c = Color::nearest_to([255, 0, 0]);
475    assert!(
476      c.family().as_str().contains("red") || c.name().contains("red"),
477      "nearest to pure red was name={:?} family={:?}",
478      c.name(),
479      c.family().as_str(),
480    );
481  }
482
483  /// Pure sRGB blue must map to a blue-family entry.
484  #[test]
485  fn nearest_to_pure_blue_is_in_blue_family() {
486    let c = Color::nearest_to([0, 0, 255]);
487    assert!(
488      c.family().as_str().contains("blue") || c.name().contains("blue"),
489      "nearest to pure blue was name={:?} family={:?}",
490      c.name(),
491      c.family().as_str(),
492    );
493  }
494
495  /// Mid-gray must map to a neutral. Tests that the `is_neutral` axis
496  /// reaches readers correctly via the lookup path.
497  #[test]
498  fn nearest_to_mid_gray_is_neutral() {
499    let c = Color::nearest_to([128, 128, 128]);
500    assert!(
501      c.is_neutral(),
502      "nearest to (128,128,128) was name={:?} is_neutral={}",
503      c.name(),
504      c.is_neutral(),
505    );
506  }
507
508  /// sRGB → LAB on the D65 reference white must produce L=100, a=0, b=0
509  /// to within float tolerance.
510  #[test]
511  fn rgb_to_lab_d65_white() {
512    let lab = rgb_to_lab([255, 255, 255]);
513    assert!((lab[0] - 100.0).abs() < 0.01, "L was {}", lab[0]);
514    assert!(lab[1].abs() < 0.01, "a was {}", lab[1]);
515    assert!(lab[2].abs() < 0.01, "b was {}", lab[2]);
516  }
517
518  /// sRGB → LAB on absolute black must produce L=0, a=0, b=0 exactly
519  /// (the linear segment of the sRGB EOTF carries 0 through unchanged).
520  #[test]
521  fn rgb_to_lab_black() {
522    let lab = rgb_to_lab([0, 0, 0]);
523    assert!(lab[0].abs() < 1e-6, "L was {}", lab[0]);
524    assert!(lab[1].abs() < 1e-6, "a was {}", lab[1]);
525    assert!(lab[2].abs() < 1e-6, "b was {}", lab[2]);
526  }
527}