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}