Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: tests

# Runs the Python test suite and the checked-in artifact checks on every push
# to main and every pull request. The colour tools compare against the
# CIE 1931 2-degree CMF table, which is CC BY-SA 4.0 and not redistributed
# here: it is fetched at run time and rejected unless its SHA-256 matches the
# pin the tools themselves enforce.

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v6

- name: Fetch the pinned CIE 1931 2-degree CMF table
run: |
set -euo pipefail
curl -sSfL --retry 3 -o "$RUNNER_TEMP/CIE_xyz_1931_2deg.csv" \
https://files.cie.co.at/Publications-datasets/CIE_xyz_1931_2deg.csv
echo "fa663e3535a7e0763a745993a1f0a192eb0275ac46ad2d1befd7626841e713c1 $RUNNER_TEMP/CIE_xyz_1931_2deg.csv" \
| sha256sum -c -

- name: Unit tests
env:
DATASHEETS_CIE_CSV: ${{ runner.temp }}/CIE_xyz_1931_2deg.csv
run: uv run --no-project --with jsonschema python -m unittest discover -s tests -v

- name: Checked-in artifacts are current
env:
CIE: ${{ runner.temp }}/CIE_xyz_1931_2deg.csv
run: |
set -euo pipefail
uv run --no-project --with jsonschema python tools/check_profile_artifacts.py
uv run --no-project python tools/generate_intensity_sensitivity.py --check
uv run --no-project python tools/bound_legacy_correction_accuracy.py "$CIE" --check
105 changes: 105 additions & 0 deletions measured-profiles/LEGACY-ACCURACY-BOUND.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Legacy-correction accuracy, bounded from datasheet data

Generated by `tools/bound_legacy_correction_accuracy.py` for FastLED/FastLED#4035.
**This is a bound, not a measurement.** The datasheets publish dominant-wavelength
and luminous-intensity ranges, not chromaticity, so the ranges are sampled on a grid
(each channel's dominant wavelength min/mid/max × FWHM low/mid/high × intensity
min/mid/max) and every grid profile is scored, under two stated assumptions:

- each emitter's spectrum is a Gaussian whose dominant wavelength (about equal-energy
white E) matches the datasheet, with FWHM red 15–25 nm,
green 25–40 nm, blue 18–30 nm (assumed, typical of
AlGaInP / InGaN emitters; not from the datasheets);
- luminous intensity is proportional to Y at a common viewing geometry.

Evaluation follows A1: an ordinary buffer is linear sRGB (B6/B8); the target is the
sRGB colour Bradford-adapted from D65 to the device's full-drive white (Y=1); the
legacy path drives each emitter at `code/255 × correction`. ΔE2000 over a black-safe
neutral ramp (32–255), RGB primaries and CMY secondaries. Budget for a
datasheet-derived profile: median ≤ 3, p95 ≤ 6.

`nominal` is the grid's centre point: mid wavelength, mid FWHM, mid intensity. The
ranges are the minimum and maximum of each statistic over the sampled grid. They are
not a proof over the continuous ranges: a grid can miss a worse or better interior
point, so each range is an inner bound on the true one.

| part | model | nominal median | nominal p95 | median range | median spread | p95 range | verdict |
| --- | --- | ---: | ---: | --- | ---: | --- | --- |
| SK6812 | UncorrectedColor | 6.91 | 10.90 | 3.60–10.50 | 6.91 | 7.69–15.41 | outside budget at every sample |
| SK6812 | TypicalLEDStrip | 15.01 | 19.31 | 12.74–16.29 | 3.55 | 17.32–21.08 | outside budget at every sample |
| SK6812 | Typical8mmPixel | 12.39 | 19.59 | 10.57–14.48 | 3.91 | 17.92–21.06 | outside budget at every sample |
| WS2812B | UncorrectedColor | 1.22 | 14.45 | 1.07–3.69 | 2.62 | 12.64–16.52 | outside budget at every sample |
| WS2812B | TypicalLEDStrip | 15.11 | 19.71 | 13.48–16.41 | 2.93 | 18.94–20.42 | outside budget at every sample |
| WS2812B | Typical8mmPixel | 12.98 | 20.11 | 12.51–13.89 | 1.38 | 19.18–21.12 | outside budget at every sample |
| WS2813A | UncorrectedColor | 4.40 | 9.33 | 1.48–5.52 | 4.04 | 8.38–10.40 | outside budget at every sample |
| WS2813A | TypicalLEDStrip | 14.94 | 21.46 | 14.55–15.27 | 0.72 | 21.06–21.79 | outside budget at every sample |
| WS2813A | Typical8mmPixel | 10.64 | 18.15 | 10.20–11.84 | 1.64 | 17.21–19.16 | outside budget at every sample |
| WS2813B | UncorrectedColor | 3.99 | 9.34 | 1.32–5.31 | 3.99 | 8.36–10.44 | outside budget at every sample |
| WS2813B | TypicalLEDStrip | 15.12 | 21.64 | 14.71–15.46 | 0.75 | 21.24–21.97 | outside budget at every sample |
| WS2813B | Typical8mmPixel | 10.52 | 17.89 | 9.99–11.52 | 1.53 | 17.00–18.85 | outside budget at every sample |
| WS2813C | UncorrectedColor | 2.24 | 6.68 | 1.74–3.43 | 1.68 | 6.00–7.99 | samples straddle the budget |
| WS2813C | TypicalLEDStrip | 15.16 | 21.68 | 14.03–15.51 | 1.47 | 21.24–22.03 | outside budget at every sample |
| WS2813C | Typical8mmPixel | 11.72 | 18.47 | 10.75–12.81 | 2.06 | 17.20–19.77 | outside budget at every sample |
| WS2813D | UncorrectedColor | 2.72 | 7.05 | 2.04–4.03 | 1.99 | 6.22–8.42 | outside budget at every sample |
| WS2813D | TypicalLEDStrip | 14.96 | 21.48 | 14.14–15.29 | 1.15 | 21.05–21.82 | outside budget at every sample |
| WS2813D | Typical8mmPixel | 11.73 | 18.71 | 10.75–12.81 | 2.06 | 17.43–20.00 | outside budget at every sample |
| WS2815 | UncorrectedColor | 5.11 | 10.41 | 1.76–7.85 | 6.09 | 8.42–16.26 | outside budget at every sample |
| WS2815 | TypicalLEDStrip | 15.35 | 21.87 | 12.99–18.33 | 5.34 | 21.24–25.95 | outside budget at every sample |
| WS2815 | Typical8mmPixel | 10.36 | 17.55 | 9.92–14.38 | 4.46 | 17.00–20.00 | outside budget at every sample |
| WS2816B-2121 | UncorrectedColor | 6.67 | 15.44 | 2.64–11.21 | 8.57 | 7.77–26.70 | outside budget at every sample |
| WS2816B-2121 | TypicalLEDStrip | 16.92 | 21.13 | 14.24–20.08 | 5.84 | 19.09–27.92 | outside budget at every sample |
| WS2816B-2121 | Typical8mmPixel | 14.51 | 18.63 | 9.77–17.19 | 7.43 | 15.62–25.26 | outside budget at every sample |
| GS8208 LED5050 option | UncorrectedColor | 2.88 | 9.53 | 2.17–8.50 | 6.33 | 8.11–18.52 | outside budget at every sample |
| GS8208 LED5050 option | TypicalLEDStrip | 14.58 | 21.09 | 13.55–18.03 | 4.48 | 20.19–24.89 | outside budget at every sample |
| GS8208 LED5050 option | Typical8mmPixel | 10.84 | 18.55 | 10.55–16.58 | 6.03 | 17.49–22.27 | outside budget at every sample |

## What this establishes

- **Datasheet ranges cannot pin accuracy to the A1 budget.** In 14 of 27 part/model
cells the median ΔE2000 over the sampled grid alone spans more than the whole
3 median budget. A sampled spread can only understate the continuous one,
so this holds for the ranges themselves: no single datasheet-derived profile could
be defended at that budget, and a runtime profile needs xy or spectral data the
PDFs do not publish.
- **Legacy models under A1:** 26 of 27 cells are outside budget at every
sampled profile, 0 within budget at every one, and the rest straddle it. These
verdicts are over the grid, not proven over the continuous ranges.
`TypicalLEDStrip` and `Typical8mmPixel` score worse than `UncorrectedColor` under A1
because A1 is relative colorimetry: the target is adapted to the device's own
full-drive white, so a correction that tints that white is penalized. The
supplementary absolute table below scores the goal those corrections were built for.

## Supplementary: absolute colorimetry (not A1)

Target and reference white are D65 with no adaptation; nominal profile only. Context
for the legacy corrections' design intent, not an A1 acceptance number.

| part | model | median | p95 |
| --- | --- | ---: | ---: |
| SK6812 | UncorrectedColor | 24.48 | 30.97 |
| SK6812 | TypicalLEDStrip | 25.86 | 33.79 |
| SK6812 | Typical8mmPixel | 19.48 | 25.86 |
| WS2812B | UncorrectedColor | 22.75 | 28.84 |
| WS2812B | TypicalLEDStrip | 26.63 | 32.48 |
| WS2812B | Typical8mmPixel | 18.09 | 24.44 |
| WS2813A | UncorrectedColor | 16.32 | 23.27 |
| WS2813A | TypicalLEDStrip | 19.51 | 26.25 |
| WS2813A | Typical8mmPixel | 12.21 | 16.21 |
| WS2813B | UncorrectedColor | 15.51 | 22.05 |
| WS2813B | TypicalLEDStrip | 18.10 | 24.93 |
| WS2813B | Typical8mmPixel | 12.70 | 16.50 |
| WS2813C | UncorrectedColor | 19.48 | 26.26 |
| WS2813C | TypicalLEDStrip | 20.88 | 28.01 |
| WS2813C | Typical8mmPixel | 16.44 | 22.84 |
| WS2813D | UncorrectedColor | 19.80 | 26.78 |
| WS2813D | TypicalLEDStrip | 21.71 | 28.72 |
| WS2813D | Typical8mmPixel | 15.80 | 22.18 |
| WS2815 | UncorrectedColor | 15.31 | 21.70 |
| WS2815 | TypicalLEDStrip | 18.17 | 22.83 |
| WS2815 | Typical8mmPixel | 14.76 | 18.79 |
| WS2816B-2121 | UncorrectedColor | 6.55 | 15.59 |
| WS2816B-2121 | TypicalLEDStrip | 17.48 | 21.43 |
| WS2816B-2121 | Typical8mmPixel | 13.95 | 17.85 |
| GS8208 LED5050 option | UncorrectedColor | 17.29 | 24.27 |
| GS8208 LED5050 option | TypicalLEDStrip | 21.30 | 27.66 |
| GS8208 LED5050 option | Typical8mmPixel | 10.43 | 14.15 |
52 changes: 42 additions & 10 deletions measured-profiles/P1-CHARACTERIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,48 @@ value, but no individual-diode xy or spectral distribution. A neutral,
primary, secondary, luminance, or ΔE2000 comparison requires each emitter's xy
and relative radiometric/photometric scale; a luminous-intensity RGB ratio
alone cannot establish ΔE2000. Consequently there are **zero admissible
derived profiles**, no defensible clusters, and no numerical score for
`TypicalLEDStrip`, `Typical8mmPixel`, or `UncorrectedColor`. Reporting zero
error or a five-part illustrative sample would falsely imply full-catalog
coverage. This is a quantified artifact-completeness result: 0/10 integrated
parts with admitted extracted spectral/xy data; 0/10 with a measured uncertainty;
0/10 currently usable for the A1 normalized ΔE calculation. P1 characterization
remains incomplete pending source extraction and the required numerical report.

When payload access is restored, calculate the three legacy models across
*every* admissible integrated record: use the profile full-drive white as
derived profiles** at runtime: 0/10 integrated parts with admitted extracted
spectral/xy data, 0/10 with a measured uncertainty.

## Legacy-correction accuracy, as a bound

What the datasheets *do* publish -- a dominant-wavelength range and a
luminous-intensity range per emitter -- is sampled into a grid of profiles
(each range's ends and midpoint) by `tools/bound_legacy_correction_accuracy.py`,
and every legacy model is scored across that grid under A1 (relative colorimetry,
linear-sRGB source, Bradford to the full-drive white, ΔE2000 over a neutral
ramp, primaries and secondaries). Two assumptions are stated, not taken from
the PDFs: a Gaussian emitter spectrum with a per-technology FWHM interval, and
luminous intensity proportional to Y. The generated table is
[`LEGACY-ACCURACY-BOUND.md`](LEGACY-ACCURACY-BOUND.md)
(`legacy-correction-accuracy-bound-v1.json`), covering the 9 integrated rows
with wavelength and intensity data.

Results:

- `TypicalLEDStrip`, `Typical8mmPixel` and `UncorrectedColor` are quantified
as ΔE2000 ranges per part; under A1 legacy models miss the datasheet budget
(median ≤ 3.0 / p95 ≤ 6.0) at every sampled profile in nearly every
part/model cell (see the generated table for the count). The grid is an
inner bound on the continuous ranges, not a proof over them. The corrections score worse than
`UncorrectedColor` under A1 because they tint the device white, which
relative colorimetry penalizes; a supplementary absolute-colorimetry table
scores their D65 design intent.
- The median over the sampled grid alone spans more than the entire 3.0
budget in about half the cells. A sampled spread can only understate the
continuous one, so datasheet ranges **cannot pin a derived profile to the A1
budget**. That is the numerical reason for zero admissible derived profiles,
and why accuracy claims wait on P10 measurement.
- Clustering: per-part ranges overlap at this resolution, so no
datasheet-level clustering is defensible; the WS2813 A–D variants differ in
intensity only and are not a bin population.

These are datasheet bounds, not measurements, and are kept separate from any
P10 instrument result.

The bound implements the prescribed method, and P10 measured records reuse
it unchanged: score the three legacy models across *every* admissible record,
use the profile full-drive white as
relative/adaptive white (Y=1, dark surround), test black-safe neutral ramp,
RGB primaries and CMY secondaries, record neutral chromaticity/Y error and
ΔE2000, then report count, median, p95 and excluded-part reasons. Keep
Expand Down
Loading
Loading