Metadata-Version: 2.4
Name: quiltwright
Version: 0.7.0
Summary: Holographic output for Looking Glass displays: off-axis multi-view quilts from PyVista or POV-Ray scenes
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: looking-glass,light-field,holographic-display,quilt,povray,pyvista,stereoscopy
Author: Eric G. Suchanek, PhD
Author-email: suchanek@flux-frontiers.com
Requires-Python: >=3.12,<3.14
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Provides-Extra: video
Provides-Extra: viz
Requires-Dist: click (>=8.1.0,<9)
Requires-Dist: imageio-ffmpeg (>=0.4) ; extra == "video"
Requires-Dist: numpy (>=1.26)
Requires-Dist: pillow (>=10.0)
Requires-Dist: pyvista (>=0.44) ; extra == "viz"
Project-URL: Documentation, https://github.com/suchanek/quiltwright/tree/main/docs
Project-URL: Homepage, https://github.com/suchanek/quiltwright
Project-URL: Issues, https://github.com/suchanek/quiltwright/issues
Project-URL: Repository, https://github.com/suchanek/quiltwright
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/suchanek/quiltwright/v0.7.0/assets/logo_pack/quiltwright_logo_512.png" alt="Quiltwright" width="512"/>
</p>

[![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue.svg)](https://www.python.org/)
[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/quiltwright.svg)](https://pypi.org/project/quiltwright/)
[![Version](https://img.shields.io/badge/version-0.7.0-blue.svg)](https://github.com/suchanek/quiltwright/releases)
[![Tests](https://github.com/suchanek/quiltwright/actions/workflows/tests.yml/badge.svg)](https://github.com/suchanek/quiltwright/actions/workflows/tests.yml)
[![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21798503-blue.svg)](https://doi.org/10.5281/zenodo.21798503)

**Quiltwright** -- holographic output for scientific visualisation.

*Eric G. Suchanek, PhD -- Flux-Frontiers*

Quiltwright is the last stage of a scientific rendering pipeline -- any
pipeline that ends in a scene. It takes what you already have, whether that is
a PyVista or VTK scene built in memory, a POV-Ray scene on disk, or a `.pov`
file written thirty years ago by someone who is no longer around to explain
it, and puts it on holographic hardware in glasses-free depth. Nothing is
rewritten to get there: a POV-Ray scene is ray-traced unmodified, with a
camera appended per view.

It is used that way by [WaveRider](https://github.com/Flux-Frontiers/waverider)
for geometric ML manifolds and by
[pdb2pov](https://github.com/suchanek/pdb2pov) for molecular structures, but
neither is a prerequisite. If you can render it, you can hang it in the air.

![Eric's Science Museum, the canonical POV-Ray render](https://raw.githubusercontent.com/suchanek/quiltwright/v0.7.0/renders/stills/museum.png)

*A career in structural biophysics, arranged as exhibits: B-DNA and Z-DNA
under bell jars, Ras and my original **DNA Under Glass** on the walls. The molecular
models were generated by pdb2pov in 1997; the room dates to 1995. Quiltwright
ray-traces it into a 48-view light-field quilt for Looking Glass light-field
panels, or into 2-D video for Hololuminescent displays. A third output -- a
23-view sweep for LitiHolo's desktop hologram printer -- is in development.*
[About the image](docs/about-the-image.md)

---

## Latest news

**v0.7.0 (2026-08-18).** The CLI now covers the whole tail of the pipeline.
`quiltwright cast` puts a saved quilt on the panel, recovering its tiling from
the filename; `quiltwright wallpaper` completes the no-Bridge path, hanging a
woven frame on the desktop of the panel it was woven for, matched by serial;
and `quiltwright bridge status` / `reset` exist because Bridge keeps answering
HTTP after it has crashed, so a cast can report success against a daemon that
will never draw. Every full quilt now writes a provenance record to
`renders/reports/` -- scene hash, commit, camera, measured depths, the depth
budget verbatim and the output's digest -- because a quilt is a gitignored
25-40 MB PNG that says nothing about where it came from. A render started
outside `make` no longer takes every core on the machine.

_Full history: [CHANGELOG.md](CHANGELOG.md) and
[releases](https://github.com/suchanek/quiltwright/releases)._

---

## What it's for

```
     scene sources                  quiltwright                  outputs

  PyVista / VTK  -----+        +------------------+        +-->  LFD  light-field panels
   (WaveRider, TVB)   |        |  off-axis views  |        |          multi-view quilts
                      +------->|  depth budget    |------->+-->  HLD  hololuminescent
  POV-Ray  -----------+        |  quilt assembly  |        |          2-D video
   (pdb2pov)                   |  view sweeps     |        +-->  LitiHolo  hogel sweeps
                               +------------------+                       (in development)
```

**Two backends, not two pipelines.** `render_quilt()` sweeps any PyVista/VTK
scene held in memory. `render_pov_quilt()` ray-traces any POV-Ray scene on
disk, appending a camera per view and modifying nothing -- which is what lets
it render files written decades ago, by tools that no longer exist, without
touching them. The two meet at a shared, renderer-agnostic assembler, so
everything downstream of that point is indifferent to which one produced the
views.

What feeds the backends is open. WaveRider's voxel and manifold visualiser and
pdb2pov's PDB conversion are the two that drove the design, but
`quiltwright.tvb_data` pulls real brain geometry from
[The Virtual Brain](docs/tvb-data.md), PyVista's own example datasets work
as-is, `quiltwright.povgen` writes POV-Ray from analytic primitives, and a
plain `.pov` file off your disk needs no pipeline at all.

**Two display technologies**, which are easy to confuse because one company
sells both. *Light-field displays* (LFD -- Portrait, Go, 16"/27"/32"/65") are
lenticular panels that consume **quilts**: N views of the same scene tiled into
one image, fused optically into real depth. *Hololuminescent displays* (HLD --
16"/27"/86") play **ordinary 2-D video** behind a fixed holographic optic, and
need styling rather than parallax -- dark field, high contrast, generous safe
margins. `quiltwright.lfd` targets the first; `quiltwright.hld` the second.

The shared middle is what makes this a package rather than two scripts: quilt
geometry and device presets, the depth-budget arithmetic that decides whether a
scene will fuse before you spend an hour rendering it, filename conventions
Looking Glass software parses, video encoding, and direct Bridge control.

**A third output, under development.** That middle also serves consumers that
are not panels at all: `render_pov_views()` writes the sweep as separate frames,
and `sweep_spec()` / `LITIHOLO_SWEEP` give the single-row layout a hologram
printer's prime view count needs and a quilt grid cannot express -- so one scene
feeds a light-field panel and a hologram printer without being rebuilt. Nothing
has been through a printer's software yet, so the claim is a sweep matching
LitiHolo's published specification rather than verified compatibility;
[docs/lfd.md](docs/lfd.md#view-sweeps--when-the-consumer-is-not-a-panel) records
what is still open.

### The part that is easy to get wrong

Each view must use an **off-axis (asymmetric-frustum) projection** -- the camera
slides sideways while continuing to face the same direction, with the image
plane sheared back onto the original view axis.

The intuitive alternative is to swivel each camera to keep the subject centred.
That is "toe-in", and it introduces vertical parallax and keystone distortion,
so the display cannot fuse the views: you get ghosting instead of depth. It is
the single most common way light-field renders go wrong, and it produces output
that looks perfectly plausible in any individual frame. Quiltwright does the
off-axis projection correctly in both backends, and gives you the arithmetic to
know in advance whether a scene will fuse.

---

## Install

```bash
pip install quiltwright              # core: quilt geometry + Bridge control
pip install "quiltwright[viz]"       # + PyVista/VTK rendering backend
```

The POV-Ray backend needs a `povray` binary on `PATH` rather than a Python
package:

```bash
brew install povray                  # macOS
```

For the complete stack -- renderers, ffmpeg, Looking Glass Bridge, pdb2pov --
see the [installation guide](docs/install.md).

---

## Quick start

### From a PyVista scene

```python
import pyvista as pv
from quiltwright import QUILT_PRESETS, render_quilt, save_quilt

p = pv.Plotter(off_screen=True)
p.add_mesh(pv.ParametricTorus())

spec = QUILT_PRESETS["portrait"]
save_quilt(render_quilt(p, spec), "torus", spec)   # -> torus_qs8x6a0.75.png
```

### From a POV-Ray scene

The scene file is never modified -- each view wraps it with `#include` and
appends one camera.

```python
from quiltwright import QUILT_PRESETS, PovCamera, render_pov_quilt, save_quilt

camera = PovCamera(location=(15, 20, 6), look_at=(44, 19.2, 45.1), fov=53.13)
spec = QUILT_PRESETS["16-landscape"]
quilt = render_pov_quilt("pov-scenes/museum/museum.pov", spec, camera,
                         include_paths=["pov-scenes/myinclude", "pov-scenes"])
save_quilt(quilt, "museum", spec)
```

The museum scene above ships in [pov-scenes/](pov-scenes/), and
[scripts/render_museum_hologram.py](scripts/render_museum_hologram.py) renders
it end-to-end with a measured depth budget -- the worked case study in
[docs/povray.md](docs/povray.md), and the scene itself in
[docs/about-the-image.md](docs/about-the-image.md).

Two more scene trees ship alongside it -- the bell-jar DNA still lifes the
museum's pedestals were built from, and porin's beta-barrel over water. What is in
each, and how to render them directly, is in
[pov-scenes/README.md](pov-scenes/README.md).

### Send it to the display

```python
from quiltwright import cast_quilt, pause_quilt, resume_quilt, stop_quilt

cast_quilt("museum_qs8x6a1.77778.png", spec)   # needs Looking Glass Bridge >= 2.2
```

`save_quilt` takes the array and `cast_quilt` takes a path, and mixing them up
only surfaces minutes into a ray-traced render. `save_and_cast_quilt` composes
the two in the right order, and returns a failed cast rather than raising, so a
Bridge that isn't running never costs you the render:

```python
from quiltwright import save_and_cast_quilt

path, error = save_and_cast_quilt(quilt, "museum", spec)
```

Saved filenames carry the `_qs<cols>x<rows>a<aspect>` suffix that Looking Glass
Studio and Bridge parse, so playback settings are detected automatically.

### Send it to a hologram printer (in development)

A printer wants the views as **separate frames**, not tiled, and LitiHolo's
published input specification asks for 23 of them per hogel -- a prime count, so
no `columns × rows` grid can express it. `LITIHOLO_SWEEP` is that single-row
spec, and the camera sweep behind it is the same off-axis geometry a quilt is
built from:

```python
from quiltwright import LITIHOLO_SWEEP, format_depth_budget, render_pov_views

print(format_depth_budget(LITIHOLO_SWEEP, camera, {"near": 31, "far": 96}))

paths = render_pov_views("pov-scenes/museum/museum.pov", LITIHOLO_SWEEP,
                         camera, "sweep/",
                         include_paths=["pov-scenes/myinclude", "pov-scenes"])
# -> sweep/view000.png ... sweep/view022.png, view 0 leftmost
```

Print the budget first rather than after. 23 views over 45° is **2.05° between
adjacent views** against a Portrait quilt's 0.74° -- about 2.75× coarser sampling,
so a sweep has *less* margin than a quilt, not more. The museum, framed as
above, reports ~43 px of adjacent-view disparity at that cone: far past the
~8 px ghosting threshold, and exactly the sort of thing worth learning before
the ray-tracer starts rather than after.

This path is POV-Ray only for now, and no file has been through the printer's
software: what it emits is a sweep matching the published specification, which
is a narrower claim than compatibility. The two open questions -- whether a hogel
slicer expects off-axis frusta or a toe-in arc, and whether 2.05° is too coarse
-- are written up in
[docs/lfd.md](docs/lfd.md#what-this-does-and-does-not-establish).

---

## Driving it from the shell

Everything above is the library, which is where a scene arrives from whichever
pipeline built it. Two things sit around that: a `Makefile` for the scenes this
repository happens to ship, and a CLI for the stage *after* the assembler,
which does not care what produced the quilt.

### `make` -- the bundled archive

The repository ships the 1993-99 POV-Ray scenes, and a `Makefile` that renders
them with their measured depth budgets already dialled in, so a hologram from a
clean clone is one command rather than a script you have to write. This covers
the *bundled* scenes only. A WaveRider manifold or a freshly converted
structure does not come through here -- it comes through the library, or through
[scripts/render_pyvista_hologram.py](scripts/render_pyvista_hologram.py) for the
PyVista subjects -- and it lands in the same `renders/quilts/`, where the CLI
below picks it up regardless of origin.

```bash
make                      # the default goal is help; rendering is always explicit
make help                 # every target, and the still names
```

#### Stills

One full-quality frame per scene, into `renders/stills/`. These are committed:
they are the diffable record of what each scene looks like.

```bash
make stills                       # all of them
make still-bell_jar_bj_holo       # just one
```

Each renders at **its own declared aspect** -- POV-Ray maps `right` to image
width whatever pixel dimensions you ask for, so a mismatched frame stretches
silently. The Makefile carries the correct size per scene; the table is in
[docs/pov-workflow.md](docs/pov-workflow.md).

#### Quilts

Into `renders/quilts/`, through the render scripts, which inject a device
camera and place the focal plane from measured near/far depths rather than
from the scene's own aim point:

```bash
make quilts                       # bell jar, porin, lambda, museum
make quilt-bell-jar-holo          # one, 16:9
make quilt-bell-jar-portrait      # the 9:16 companion, for tall panels
make preview-museum               # quarter-size, for iterating on composition
```

Preview first when you are changing a composition -- a preview is seconds per
view where a full quilt is minutes, and the depth budget it prints is the same
one the full render will use.

Two knobs worth knowing:

```bash
make quilt-porin EXTRA_ARGS="--cast"          # send it to the panel when done
make quilt-museum EXTRA_ARGS="--antialias 0.1"
make quilts RENDER_THREADS=$(sysctl -n hw.ncpu)   # use the whole box
```

`RENDER_THREADS` defaults to **`ncpu - 2`**, leaving two cores for the rest of
the machine so a multi-minute render does not make the desktop unusable. It
reaches POV-Ray through a generated `POVINI`, because the render scripts invoke
`povray` themselves and a command-line `+WT` would override them. `JOBS` stays
at 1 on purpose: POV-Ray already threads one render across every core, so extra
processes only split it.

The same two cores are held back when you call a render script directly, where
there is no `POVINI` to carry the Makefile's value -- `--threads N` sets it
explicitly, and `--threads 0` lets POV-Ray take everything, which is its own
default. A `Work_Threads` line in `POVINI` always wins over the courtesy cap,
so `make quilts RENDER_THREADS=...` keeps working.

#### Run reports

Every full quilt writes a Markdown provenance record to `renders/reports/`.
A quilt is a 25-40 MB gitignored release asset; the report is the committed
record of how it was made -- scene file *and its SHA-256*, repository commit and
whether the tree was dirty, camera and measured depths, the depth budget
verbatim, the parallelism actually used, timings, and the output's own digest.
Pass `--report` to either render script to get one outside `make`.

### `quiltwright` -- the CLI, downstream of both backends

Installed as `quiltwright`, core-only (numpy, pillow, click). Everything here
operates on a *quilt*, which is where the two scene sources have already met:
a manifold swept out of PyVista and a molecular scene ray-traced from POV-Ray
produce the same artifact, and these commands treat them identically.

```bash
quiltwright bridge status       # is Bridge actually able to draw?
quiltwright bridge reset        # kill and relaunch a wedged daemon

quiltwright cast renders/quilts/bell-jar-holo_qs8x6a1.77778.png
quiltwright cast --check        # which displays can Bridge see?

quiltwright weave renders/quilts/bell-jar-holo_qs8x6a1.77778.png --cal visual.json
quiltwright wallpaper bell-jar-holo_native_LKG-J00332.png
```

`cast` recovers the tiling from the `_qs<cols>x<rows>a<aspect>` filename suffix
that `save_quilt()` writes, so it usually needs no flags whatever produced the
views. `weave` then `wallpaper` is the **no-Bridge path**:
a woven frame is already interleaved for one panel, so setting it as that
panel's desktop picture makes the desktop a hologram with nothing running.
`wallpaper` matches the frame to the right display by the panel serial both
carry.

When the glass stays black, `bridge status` is the first thing to run. Bridge
keeps its HTTP port open and keeps issuing session tokens after crashing
internally, so a cast can report success at every step against a daemon that
will never draw -- `status` checks the port, the session, the device list *and*
whether any device is actually a Looking Glass, then gives a verdict.

---

## The depth budget

Whether a hologram fuses comes down to **adjacent-view disparity**: how far a
feature moves between neighbouring views. Roughly 4-5 px is the practical
ceiling; past ~8 px, hard edges ghost.

```python
from quiltwright import QUILT_PRESETS, focal_distance_for_range, view_disparity

# Put the focal plane where near and far content are equally penalised.
focal = focal_distance_for_range(near=31, far=96)       # harmonic mean, not midpoint
view_disparity(QUILT_PRESETS["16-landscape"], fov=53.13,
               focal_distance=focal, depth=31)          # -> px between adjacent views
```

Those two depths are measured, not guessed --
[`scripts/measure_depth_range.py`](scripts/measure_depth_range.py) sweeps an
opaque plane along the view axis and reports where a scene's content actually
begins and ends.

Three results worth knowing before you frame a shot:

- Content **at** the focal plane has zero disparity -- it is welded to the glass.
- The focal plane belongs at the **harmonic mean** of the depth range, not the
  midpoint. Disparity is asymmetric in depth, and near content is the expensive
  side.
- A **narrower field of view increases** disparity. Zooming in magnifies the
  scene and the parallax with it. The widely repeated "use ~14° FOV" advice is
  specific to object-centric scenes; applied to an interior it makes ghosting
  worse.

For interiors there is a fourth trap that no arithmetic will warn you about:
the camera sweep physically travels `focal_distance × tan(cone/2)` sideways,
and in a room that path can run through a wall. See
[docs/povray.md](docs/povray.md#3-sweep-clearance--the-constraint-peculiar-to-interiors).

---

## Supported devices

`QUILT_PRESETS` carries the official quilt settings for Portrait, Go, and the
16"/27"/32"/65" panels in both orientations. The 16" Gen3 Landscape entry is
verified against what Bridge reports for real hardware.

```python
from quiltwright import QUILT_PRESETS
QUILT_PRESETS["16-landscape"]      # 8x6 views, 7680x4320, aspect 1.7778
```

---

## Documentation

| Document | Contents |
|----------|----------|
| [docs/install.md](docs/install.md) | Installing the full stack: package extras, POV-Ray, ffmpeg, Bridge, pdb2pov |
| [docs/lfd.md](docs/lfd.md) | Light-field output, Bridge/Studio setup, device presets, the PyVista path, view sweeps for hologram printers |
| [docs/pyvista-datasets.md](docs/pyvista-datasets.md) | PyVista dataset ideas for holograms: topography, the Allen mouse brain atlas, other strong-depth candidates |
| [docs/tvb-data.md](docs/tvb-data.md) | Brain geometry from The Virtual Brain: cortical surfaces, connectomes, parcellations, downloaded on demand |
| [docs/povray.md](docs/povray.md) | The POV-Ray backend: off-axis camera derivation, depth budget, sweep clearance, a worked case study |
| [docs/povgen.md](docs/povgen.md) | Writing POV-Ray scenes from analytic primitives, so a scene composed in Python can be ray-traced rather than rasterised |
| [docs/pov-workflow.md](docs/pov-workflow.md) | The procedure: taking an archive scene from "won't parse" to a quilt that fuses, step by step |
| [docs/pdb2pov.md](docs/pdb2pov.md) | Rendering molecular structures from PDB and mmCIF files as holograms, from the shell or in-process |
| [docs/hld.md](docs/hld.md) | Hololuminescent Displays, which play ordinary 2-D video rather than quilts |
| [docs/about-the-image.md](docs/about-the-image.md) | The museum scene: what is on display, and the thirty-year pipeline behind it |
| [docs/gallery.md](docs/gallery.md) | The reference stills every quilt is swept from, one per scene, and how to regenerate them |

---

## Testing

```bash
pip install -e ".[viz]" && pip install pytest
pytest
```

Rendering tests skip cleanly on machines with no OpenGL stack, and the POV-Ray
tests skip when no `povray` binary is present. Under a headless CI runner, use
`xvfb-run -a pytest` to exercise them.

---

## The pipelines this serves

- [WaveRider](https://github.com/Flux-Frontiers/waverider) -- manifold-aware
  geometric ML. Its voxel and manifold visualiser builds the PyVista scenes
  that `render_quilt()` sweeps.
- [pdb2pov](https://github.com/suchanek/pdb2pov) -- PDB and mmCIF to POV-Ray
  converter, written in C in 1993 and still building from a fresh clone. It
  produced the molecular models in the image above, and still feeds the
  POV-Ray backend. `pypdb2pov`, its Python port, writes byte-identical scenes
  and imports, so a conversion and a quilt render fit in one script.
- [proteusPy](https://github.com/suchanek/proteusPy) -- protein disulfide bond
  analysis and rendering.

## Citation

If you use Quiltwright in your work, please cite it. Citation metadata is in
[CITATION.cff](CITATION.cff); GitHub's "Cite this repository" button generates
BibTeX/APA from it, and the DOI badge above resolves to the archived release
on Zenodo.

```bibtex
@software{suchanek_quiltwright,
  author  = {Suchanek, Eric G.},
  title   = {Quiltwright: Holographic Output for Looking Glass Displays},
  url     = {https://github.com/suchanek/quiltwright},
  doi     = {10.5281/zenodo.21798503},
  version = {0.7.0},
  year    = {2026}
}
```

## License

BSD 3-Clause. See [LICENSE](LICENSE).

