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
5 changes: 1 addition & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
# display-report — Roadmap

This layer's roadmap. Cross-repo coordination and the pipeline-wide
sequencing live in
[color-wrangler](https://github.com/Fuse-Technical-Group/color-wrangler);
workstreams there are addressed by slug in backticks and resolve in
that repository's ROADMAP.md.
sequencing live in the umbrella project's roadmap.

## Contract-driven analysis §road:contract-analysis-impl

Expand Down
39 changes: 17 additions & 22 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
# display-report — Specification

This layer's specification. The pipeline-wide architecture and the
artifact chain live in
[color-wrangler](https://github.com/Fuse-Technical-Group/color-wrangler);
sections there are addressed by slug in backticks and resolve in that
repository's SPEC.md.
artifact chain live in the umbrella project's specification.

## Scope §spec:scope

Expand All @@ -15,18 +12,16 @@ serial port and no DeckLink, imports no device driver, and needs no
rig: a machine that reports needs a file and nothing else.

One tool touches instruments and signal hardware, and it is
display-measure (`§spec:session-ownership`). This repository consumes
what that tool emits.
display-measure. This repository consumes what that tool emits.

**Why the validator owns no measure path.** display-report validates
either this pipeline or a display-side calibration, and its value is
that it has no stake in the correction under test. A second measure
path living inside it would be an ungated one — the session gates
(`§spec:signal-contract`) live in display-measure, and a path that
skips them measures a display in an undeclared state and renders a
well-formed report of it. That is the failure the gates exist to
prevent, reintroduced in the repository whose independence is the
reason they matter.
live in display-measure, and a path that skips them measures a display
in an undeclared state and renders a well-formed report of it. That is
the failure the gates exist to prevent, reintroduced in the repository
whose independence is the reason they matter.

**Reproducibility is a property of the seam, not of a bundled loop.**
A third party reproduces a report by writing the seam file from
Expand All @@ -37,16 +32,16 @@ second device path inside the validator was never what delivered it.
Not owned here: instrument and signal-generator access
(display-measure), OCIO semantics and config generation
(ocio-display-gen), the show manifest and the promotion decision
(color-wrangler).
(the umbrella project).

## Report input §spec:report-input

*Status: complete*

The report's input is one file: the measurement seam file
(`§spec:measurement-seam`), carrying the measurements, the spectra
behind them, the protocol that produced them, the declared signal
contract, the attested panel state, and the hash chain.
The report's input is one file: the measurement seam file, carrying
the measurements, the spectra behind them, the protocol that produced
them, the declared signal contract, the attested panel state, and the
hash chain.

Analysis is a pure function of that file. Two runs over one file
produce one report.
Expand All @@ -61,7 +56,7 @@ catches it.

**Rows without a measured spectrum are legible as such.** A
disciplined session reads its dark end with a colorimeter, and those
rows carry a reconstructed spectrum or none (`§spec:spectral-retention`).
rows carry a reconstructed spectrum or none.
An analysis needing a measured spectrum reports which rows it excluded
and why, rather than treating a scaled estimate as a measurement.

Expand Down Expand Up @@ -113,8 +108,8 @@ judges from. Plot bounds derived from a hardcoded PQ inverse place the
measured points wrongly on a gamma session's page — the chart renders,
and it is wrong.

The page's content — what figures it carries and what they
discriminate — is specified in `§spec:report-metrics`.
The umbrella project's specification defines the page's content:
what figures it carries and what they discriminate.

## Programmatic surface §spec:report-api

Expand All @@ -126,9 +121,9 @@ rendered report as bytes. The command-line entry point is a caller
like any other and holds no logic of its own.

**Why an importable surface.** The operator's surface is a browser
served from the session host (`§spec:web-ui`), and it generates the
report from a loaded artifact without the operator leaving the page or
learning a second tool. A caller reduced to shelling out to a CLI and
served from the session host, and it generates the report from a
loaded artifact without the operator leaving the page or learning a
second tool. A caller reduced to shelling out to a CLI and
scraping a path cannot report a failure precisely.
display-report remains the only thing that decides what a report says;
which surface invokes it is a separate question.
Expand Down
5 changes: 2 additions & 3 deletions display_report/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,8 @@ def black(self) -> dict:
tmp = self._black = {}
tmp["measurements"] = measurements = self._data.measurements[mask]

# A disciplined session reads its dark end with a colorimeter
# (§spec:spectral-retention), so the black rows are exactly the ones
# most likely to carry no spectrum. Black's tristimulus is measured
# A disciplined session reads its dark end with a colorimeter, so
# the black rows are exactly the ones most likely to carry no spectrum. Black's tristimulus is measured
# either way; the spectral fields are only available when a black
# row carried a spectrum, and are None rather than zero when not --
# a zero here would read as a perfectly black display.
Expand Down
10 changes: 4 additions & 6 deletions display_report/gamut.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""Gamut arithmetic for the situational-awareness view (§spec:gamut-visualization).
"""Gamut arithmetic for the situational-awareness view.

Everything here works in CIE 1976 u'v'. The 1931 xy diagram exaggerates
green distances and crushes blue ones, which misleads exactly where
narrow-band LED primaries land; artifacts record xy and views convert
(§spec:gamut-visualization).
narrow-band LED primaries land; artifacts record xy and views convert.

**Coverage, never area ratio.** The fraction of a target a display can
actually reproduce is bounded above by 1.0. The ratio of triangle areas
is not, and it flatters a display that is large in a direction no
content uses: the bench display measures 131.6% of Rec.709 by area and
cannot reach Rec.709 blue (§spec:report-metrics).
cannot reach Rec.709 blue.

**A scalar hides where the shortfall is.** Coverage weights by
chromaticity area, so a thin slice of unreachable colour reads as a
Expand Down Expand Up @@ -165,8 +164,7 @@ def primary_deficits(display: list[Point], standard: str) -> dict[str, float]:
Read against the display's own native primaries rather than a
standard's wherever the question is what a *configuration* costs: no
display reaches Rec.709 blue, so that shortfall is a property of
the emitter and discriminates nothing between calibrations
(§spec:report-metrics).
the emitter and discriminates nothing between calibrations.
"""
target = [uv_from_xy(*p) for p in STANDARD_GAMUTS[standard]]
deficits = {}
Expand Down
9 changes: 4 additions & 5 deletions display_report/transfer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Transfer fidelity and quantization headroom (§spec:gamut-visualization).
"""Transfer fidelity and quantization headroom.

Two questions about the same ramp. Does the display follow the transfer
function its contract declares, and is the encoding precise enough that
Expand All @@ -17,7 +17,7 @@
here asks the same question of any encoding — a gamma contract at 12
bits, a PQ contract at 10 — rather than asking whether a display tracks
one particular curve. That generality is the point: it is the metric
that survives changing the contract (§road:lut-transfer-probe).
that survives changing the contract.

Its threshold is not a hard line. Barten's model takes viewing distance,
field size and spatial frequency, and the value used here is the peak of
Expand Down Expand Up @@ -46,7 +46,7 @@
Ramp = list[tuple[int, float]] | tuple[tuple[int, float], ...]

# Readings at or under this are instrument noise rather than the display's
# response; fitting through them fits the noise (§road:instrument-floors).
# response; fitting through them fits the noise.
FLOOR = 0.001

# The spatial frequency Barten's sensitivity peaks near, cycles per
Expand Down Expand Up @@ -182,8 +182,7 @@ def quantization_headroom(
measured. The ramp is driven at 12-bit codes, so a step at 10 bits
is four of them: this is how one contract's quantization gets
compared against another's on the same measured display, which is what
§road:lut-transfer-probe needs to settle whether a PQ-like LUT
contract beats the 12-bit gamma one.
settles whether a PQ-like LUT contract beats the 12-bit gamma one.
"""
codes_per_step = 2.0 ** (MEASURED_BIT_DEPTH - bit_depth)
rows: list[HeadroomRow] = []
Expand Down
4 changes: 2 additions & 2 deletions display_report/volume.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""The reproducible set as a solid (§spec:gamut-visualization).
"""The reproducible set as a solid.

A chromaticity triangle is one slice of a three-dimensional set, and it
hides what decides whether a colour is usable: the luminance it is
Expand All @@ -15,7 +15,7 @@
rather than drawing a capability nobody measured. This display sums to
within 0.75% of its measured white, which is what earns the picture.

**Why CIELAB.** §spec:report-metrics asks for a luminance-inclusive
**Why CIELAB.** The report's metrics call for a luminance-inclusive
volume in a perceptually uniform space, so that equal distances in the
picture mean roughly equal perceived differences and the solid's shape
carries information rather than the space's distortion. L* is relative
Expand Down
7 changes: 3 additions & 4 deletions tests/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ display-measure characterize --out hybrid_session.csmf --instrument doubles-hybr
72 rows from the `color-wrangler/characterize/3` protocol against
display-measure's deterministic display double, under a GAMMA 2.4 contract.
71 rows are spectral; the black row is colorimetric and carries no spectrum
at all, because a disciplined session reads its dark end with a colorimeter
(§spec:spectral-retention). That row is the point of the fixture — it is the
shape the analysis has to tolerate, and no hand-built file would have
produced it.
at all, because a disciplined session reads its dark end with a colorimeter.
That row is the point of the fixture — it is the shape the analysis has to
tolerate, and no hand-built file would have produced it.

Regenerate it from display-measure when the protocol or the seam format
changes. Do not hand-edit it.
Expand Down
7 changes: 3 additions & 4 deletions tests/test_gamut.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""Gamut arithmetic for the situational-awareness view (§spec:gamut-visualization).
"""Gamut arithmetic for the situational-awareness view.

The numbers here are the bench display's, measured under three different
panel configurations, because the arithmetic's job is to tell them apart
(§spec:report-metrics).
panel configurations, because the arithmetic's job is to tell them apart.
"""

import pytest
Expand Down Expand Up @@ -42,7 +41,7 @@ def test_a_gamut_covers_itself_entirely(self) -> None:
def test_coverage_never_exceeds_one(self) -> None:
"""The bench display's triangle is larger than Rec.709's by area and
still cannot reproduce all of it — the distinction area ratios
lose (§spec:report-metrics)."""
lose."""
for primaries in (AUG12, AUG29):
for name in STANDARD_GAMUTS:
target = [uv_from_xy(*p) for p in STANDARD_GAMUTS[name]]
Expand Down
4 changes: 2 additions & 2 deletions tests/test_transfer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Transfer fidelity and quantization headroom (§spec:gamut-visualization).
"""Transfer fidelity and quantization headroom.

The bench display's own gray ramp, dark-room capture. Its declared contract
is a 2.35 power law and it does not follow one: the measured response
Expand Down Expand Up @@ -108,7 +108,7 @@ def test_the_threshold_flag_tracks_the_ratio(self) -> None:

class TestBitDepthComparison:
"""The same measured display, judged against a different encoding — the
comparison §road:lut-transfer-probe needs."""
comparison that settles which LUT contract to run."""

def test_fewer_bits_make_every_step_coarser(self) -> None:
twelve = {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_volume.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""The reproducible set as a solid (§spec:gamut-visualization).
"""The reproducible set as a solid.

A chromaticity triangle is one slice of a three-dimensional set, and it
hides the thing that decides whether a colour is usable: the luminance
Expand Down
Loading