Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ac743a7
build: scaffold the display_patterns package with uv and hatchling
repentsinner Aug 10, 2026
7f46b73
ci: run ruff, pyright, pytest, and a core-only wheel install
repentsinner Aug 10, 2026
fd8032a
ci: lint governance docs via the shared symphonize workflow
repentsinner Aug 10, 2026
565c9cc
ci: automate release PRs with release-please
repentsinner Aug 10, 2026
ec95ad2
docs(readme): add installation, usage, and API sections
repentsinner Aug 10, 2026
68a6f20
docs(requirements): reword 'must' phrasing for the vale requirements …
repentsinner Aug 10, 2026
5ac4107
feat: move the bmd-signal-gen pattern and chart catalog in verbatim
repentsinner Aug 10, 2026
4f8b156
docs(governance): record extraction-spine progress
repentsinner Aug 10, 2026
1617466
refactor(tests): run one core-footprint probe from the tool and the s…
repentsinner Aug 10, 2026
49499ce
refactor(charts): lazy-load conversion and renderer with actionable e…
repentsinner Aug 10, 2026
e14caa6
fix(tests): find the bmd-signal-gen checkout from a nested worktree
repentsinner Aug 10, 2026
95c83ac
ci: split lint from the test matrix and gate equivalence on every PR
repentsinner Aug 10, 2026
0af90f8
docs(roadmap): defer the PyPI release pending org confirmation
repentsinner Aug 10, 2026
124368e
ci: restore the setup-uv SHA pin; no floating v9 tag exists
repentsinner Aug 10, 2026
4f844d4
build: swap release automation from release-please to flywheel
repentsinner Aug 10, 2026
f92b8c5
build: derive the package version from git tags via hatch-vcs
repentsinner Aug 10, 2026
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
7 changes: 7 additions & 0 deletions .flywheel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
flywheel:
streams:
- name: main-line
branches:
- name: main
release: production
auto_merge: [fix, chore, docs]
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
# ruff and pyright are pinned to py312 in pyproject.toml, so one leg
# carries the full lint signal; only pytest is version-sensitive.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- run: uv sync --all-extras
- run: uv run ruff format --check .
- run: uv run ruff check .
- run: uv run pyright

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --all-extras
- run: uv run pytest

# Prove the numpy-only core: install the built wheel without extras into a
# clean environment and run the core probe (§spec:package-shape).
core-install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # hatch-vcs derives the version from tag history
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- run: uv build
- run: uv run --isolated --no-project --with dist/*.whl python tools/check_core_install.py

# The extraction gate: moved modules render array-for-array equal to
# bmd-signal-gen's in-tree implementation (§spec:extraction).
equivalence:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v5
with:
repository: OpenLEDEval/bmd-signal-gen
path: bmd-signal-gen
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- run: uv sync --all-extras
- run: uv run pytest tests/test_bmd_equivalence.py
env:
BMD_SIGNAL_GEN_REPO: ${{ github.workspace }}/bmd-signal-gen
26 changes: 26 additions & 0 deletions .github/workflows/flywheel-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Flywheel — PR
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, edited]
concurrency:
group: flywheel-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
conduct:
# Skip drafts; ignore `edited` events from bots (only a human editing the
# title/body should retrigger the conventional-commit rewrite). Skips
# cleanly until the flywheel GitHub App is installed on the org and its
# credentials are configured.
if: |
vars.FLYWHEEL_GH_APP_ID != '' &&
github.event.pull_request.draft == false &&
(github.event.action != 'edited' || github.event.sender.type == 'User')
runs-on: ubuntu-latest
steps:
# SHA-pinned (no floating tags for third-party actions); the trailing
# # vX.Y.Z comment is for human review. Mirrors pydecklink's setup.
- uses: point-source/flywheel@59758d1940ebe054ce9fdd4b7a7dc79a0cf235e0 # v2.1.0
with:
event: pull_request
app-id: ${{ vars.FLYWHEEL_GH_APP_ID }}
app-private-key: ${{ secrets.FLYWHEEL_GH_APP_PRIVATE_KEY }}
22 changes: 22 additions & 0 deletions .github/workflows/flywheel-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Flywheel — Push
on:
push:
branches: ["**"]
concurrency:
group: flywheel-push-${{ github.ref_name }}
cancel-in-progress: false
jobs:
release:
# Skips cleanly until the flywheel GitHub App is installed on the org
# and its credentials are configured (vars.FLYWHEEL_GH_APP_ID,
# secrets.FLYWHEEL_GH_APP_PRIVATE_KEY).
if: vars.FLYWHEEL_GH_APP_ID != ''
runs-on: ubuntu-latest
steps:
# SHA-pinned (no floating tags for third-party actions); the trailing
# # vX.Y.Z comment is for human review. Mirrors pydecklink's setup.
- uses: point-source/flywheel@59758d1940ebe054ce9fdd4b7a7dc79a0cf235e0 # v2.1.0
with:
event: push
app-id: ${{ vars.FLYWHEEL_GH_APP_ID }}
app-private-key: ${{ secrets.FLYWHEEL_GH_APP_PRIVATE_KEY }}
11 changes: 11 additions & 0 deletions .github/workflows/governance-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Governance Lint
on:
push:
branches: [main]
pull_request:

jobs:
lint:
uses: repentsinner/symphonize/.github/workflows/governance-lint.yml@notation--v0
with:
readme-type: "library"
1 change: 1 addition & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"MD013": false, "MD024": false, "MD036": false}
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
8 changes: 8 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
StylesPath = styles
MinAlertLevel = warning

[SPEC.md]
BasedOnStyles = Requirements

[REQUIREMENTS.md]
BasedOnStyles = Requirements
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,53 @@ which consumes it back over SDI. Sibling consumers and context:
[color-wrangler](https://github.com/Fuse-Technical-Group/color-wrangler)
(LED-surface characterization umbrella) and its component repos.

## Installation

```sh
pip install display-patterns # core: numpy only
pip install "display-patterns[charts,io]" # chart authoring + TIFF export
```

Until the first PyPI release, install from a checkout:
`pip install .` (or `uv pip install .`).

## Usage

Render a 12-bit checkerboard at exact code values:

```python
from display_patterns.image_generators import ROI, PatternGenerator

generator = PatternGenerator(
bit_depth=12, width=1920, height=1080, roi=ROI(0, 0, 1920, 1080)
)
frame = generator.generate([[4095, 2048, 0], [0, 0, 0]]) # uint16 (1080, 1920, 3)
```

With the `charts` and `io` extras, author a chart in YAML, render it,
and write a 16-bit TIFF:

```python
from display_patterns.charts import render_chart, write_chart_tiff
from display_patterns.charts.loaders import load_chart

layout = load_chart("my_chart.yaml")
image = render_chart(layout, bit_depth=12)
write_chart_tiff("my_chart.tiff", image, layout)
```

## API

- `display_patterns.image_generators` — core pattern math:
`PatternGenerator`, `ROI`, `ColorRangeError`. Numpy only.
- `display_patterns.charts` — chart types, colorimetric conversion, and
rendering (`charts` extra); TIFF read/write (`io` extra).
- `display_patterns.charts.loaders` — YAML chart definitions
(`charts` extra).

All public functions and classes carry NumPy-style docstrings; the
package ships `py.typed` for static type checking.

## Governance

- [REQUIREMENTS.md](REQUIREMENTS.md) — the problem space
Expand Down
4 changes: 2 additions & 2 deletions REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ real-time render graphs, and measurement pipelines.

Test-pattern math lives trapped inside device tools. bmd-signal-gen's
checkerboard and chart generation imports nothing from its DeckLink
layer, yet a consumer who wants only the patterns must install the
layer, yet a consumer who wants only the patterns installs the
whole device tool. backlit_molecule re-derived its own frame-counter
panel math because no importable source existed. Each new consumer
rewrites geometry that is pure, deterministic, and identical across
Expand Down Expand Up @@ -108,7 +108,7 @@ caller's back cannot make a checkable radiometric claim.

## Priorities §req:priorities

Must-have, in adoption order:
Essential, in adoption order:

1. Core catalog with exact code values: solid, checkerboard, region
of interest, bit-depth validation.
Expand Down
25 changes: 6 additions & 19 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,13 @@ Walking skeleton: an installable package whose patterns are
bmd-signal-gen's, moved verbatim so the bit-identical adoption claim
is checkable before any reshaping (§spec:extraction).

### Package scaffold §road:package-scaffold

Scaffold the Python project (uv, hatchling, ruff/pyright/pytest,
governance-lint CI, release-please) with the `display_patterns`
package, empty `charts` and `io` extras, and Python 3.12+ metadata.
§spec:package-shape.

### Verbatim module move §road:verbatim-move

Move `bmd_sg/image_generators/` and `bmd_sg/charts/` with their tests
into `display_patterns`, changing imports only — core fills in the
base package, chart and TIFF modules behind the `charts` and `io`
extras. §spec:extraction, §spec:catalog, §spec:file-export.
Depends on §road:package-scaffold.

### First release §road:first-release

Publish 0.1 to PyPI via trusted publishing (configuring the PyPI
project is a human step). §spec:package-shape. Depends on
§road:verbatim-move.
Publish 0.1 to PyPI via trusted publishing. §spec:package-shape.
Blocked — deferred until the first rounds of cross-repo integration
testing complete and the PyPI org is confirmed (pending the possible
OpenLEDEval → OpenDisplayEval rename); consumers integrate via a git
dependency meanwhile.

**Verify:** In a fresh environment, `pip install display-patterns`
imports and renders a checkerboard with numpy alone; `pip install
Expand All @@ -48,7 +35,7 @@ equivalence is banked — visible, separate changes (§spec:extraction).
Reshape catalog entry points to the pure rendering signature —
parameters, frame index, caller-supplied array namespace with
optional device, numpy default — with stills ignoring the index.
§spec:render-model. Depends on §road:verbatim-move.
§spec:render-model. Depends on §road:extraction-spine.

### Counter panel codec §road:counter-panel

Expand Down
8 changes: 4 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ there).

## Package shape §spec:package-shape

*Status: not started*
*Status: in progress*

The distribution is `display-patterns` on PyPI; the import package is
`display_patterns`. The core installs with numpy as its only
Expand Down Expand Up @@ -81,7 +81,7 @@ caller.

## Catalog §spec:catalog

*Status: not started*
*Status: in progress*

The core catalog, renderable with numpy alone (§req:success-criteria):

Expand Down Expand Up @@ -117,7 +117,7 @@ model and carries a decode side when one is meaningful.

## Extraction and compatibility §spec:extraction

*Status: not started*
*Status: in progress*

The initial code is bmd-signal-gen's `bmd_sg/image_generators/` and
`bmd_sg/charts/` with their tests, moved verbatim before any
Expand All @@ -134,7 +134,7 @@ after the equivalence claim is banked.

## File export §spec:file-export

*Status: not started*
*Status: in progress*

The `io` extra writes rendered arrays to 16-bit TIFF, serving the
chart review workflow (§req:user-stories). Export is one-way — the
Expand Down
7 changes: 7 additions & 0 deletions display_patterns/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
display-patterns: deterministic display test patterns, device-free and exact.

The core package depends on numpy alone. Chart authoring and TIFF export
live in the ``display_patterns.charts`` subpackage behind the ``charts``
and ``io`` extras.
"""
57 changes: 57 additions & 0 deletions display_patterns/charts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Chart production: colorimetric patch lists rendered to display RGB
(§spec:catalog).

``color_types`` (numpy-only) loads eagerly; the conversion, renderer,
and TIFF modules load on first attribute access, so importing this
package pays only for what the caller uses — colour-science and
Pillow arrive with the ``charts`` extra, tifffile with ``io``
(§spec:package-shape).
"""

import importlib
from typing import TYPE_CHECKING

from display_patterns.charts.color_types import ChartLayout, ColorValue, Patch

if TYPE_CHECKING:
from display_patterns.charts.conversion import xyz_to_display_rgb
from display_patterns.charts.renderer import render_chart
from display_patterns.charts.tiff_reader import TiffMetadata, load_chart_tiff
from display_patterns.charts.tiff_writer import write_chart_tiff

# name -> (submodule, extra supplying its dependencies). The runtime
# source of truth for the lazy exports; __all__ derives from it, and
# the TYPE_CHECKING block above mirrors it for static analysis.
_LAZY_EXPORTS = {
"xyz_to_display_rgb": ("display_patterns.charts.conversion", "charts"),
"render_chart": ("display_patterns.charts.renderer", "charts"),
"TiffMetadata": ("display_patterns.charts.tiff_reader", "io"),
"load_chart_tiff": ("display_patterns.charts.tiff_reader", "io"),
"write_chart_tiff": ("display_patterns.charts.tiff_writer", "io"),
}

# Literal so ruff recognizes the re-exports (F401); a test asserts every
# name resolves, keeping this list and _LAZY_EXPORTS from drifting.
__all__ = [
"ChartLayout",
"ColorValue",
"Patch",
"TiffMetadata",
"load_chart_tiff",
"render_chart",
"write_chart_tiff",
"xyz_to_display_rgb",
]


def __getattr__(name: str) -> object:
if name not in _LAZY_EXPORTS:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
module, extra = _LAZY_EXPORTS[name]
try:
return getattr(importlib.import_module(module), name)
except ModuleNotFoundError as error:
raise ModuleNotFoundError(
f"{name} needs the {extra!r} extra: pip install 'display-patterns[{extra}]'"
) from error
Loading
Loading