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
1 change: 1 addition & 0 deletions docs/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The same routing as a quick table:
| Attach optional in-process panels to the existing Web or TUI viewer | [`extensions_api.md`](extensions_api.md) |
| Use the Web sidebar's Display / Analysis / Operations tabs and optional right-side extension panel | [`extensions_api.md#web-tool-placement`](extensions_api.md#web-tool-placement) |
| Control a local terminal view, select stable atom IDs, or run a stateful Python/JSONL agent session | [`tui_api.md`](tui_api.md) |
| Select an external terminal frame renderer without forking MatterVis | [`tui_renderer_api.md`](tui_renderer_api.md) |
| Read terminal pictures with element symbols, molecule silhouettes, text depth bands, crossing-bond gaps, layer slicing, and optional rotation parallax | [`tui_api.md`](tui_api.md) |
| Render CIF, Cube, VASP, XYZ, ASE trajectory, or LAMMPS files from the CLI | [`../cli.md`](../cli.md) |
| Label trajectory frames with physical simulation time while keeping playback FPS independent | [`../cli.md`](../cli.md#frames-and-animations) |
Expand Down
21 changes: 17 additions & 4 deletions docs/agents/tui_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ payload = observation.as_dict()
Snapshot-registry operations (`save_view`, `list_views`) do not alter active
view state and therefore do not increment it.
- `render_frame(charset="unicode"|"ascii7")` returns the current picture, including
rotation ghosts, through the same renderer used by Textual and sessions.
rotation ghosts, through the same renderer dispatch used by static CLI output
and sessions. The default backend is `mattervis.native-v1`. See
[`tui_renderer_api.md`](tui_renderer_api.md).
- `observe()` returns `TerminalObservation` with schema
`mattervis.tui.observation/v2`. `as_dict()` is JSON-safe.

Expand Down Expand Up @@ -86,9 +88,20 @@ that only touches the configured limit may continue without clipping.
Each `SessionObservation` contains exactly `width × height` padded
`screen_lines`, a SHA-256 of those rendered bytes, and a deterministic state
fingerprint over camera, display, focus, selection, viewport, and charset. The
fingerprint excludes coordinates, structure metadata, monotonic revision, and
source path; use the screen hash or an external content identifier when the
caller must bind an observation to a particular structure.
fingerprint excludes coordinates, structure metadata, monotonic revision,
renderer identity, and source path; use the screen hash or an external content
identifier when the caller must bind an observation to a particular structure.
Picture bytes are the cross-interpreter byte-identity contract. Viewport
floats inside the fingerprint can differ by one unit in the last place across
CPython builds, so the fingerprint value is compared within one process.
`TerminalSession(..., renderer="example.marks-v1", renderer_config=path,
observation_profile="legacy"|"model")` freezes the renderer for the episode.
It is not a `set_display` argument. The default profile and the default
renderer leave JSONL observations and `state_fingerprint` unchanged.
`replay_manifest()` is the host-owned renderer identity and is not part of
that fingerprint. `observation_profile="model"` drops chemistry warnings,
formulas, and the source title from the emitted observation. Details and the
external-plugin contract are in [`tui_renderer_api.md`](tui_renderer_api.md).

For subprocess callers, `mat-vis tui INPUT --session-format jsonl` reads one
`mattervis.tui.action/v1` object per stdin line and writes one
Expand Down
180 changes: 180 additions & 0 deletions docs/agents/tui_renderer_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Terminal renderer backends

MatterVis owns the structure, the camera, projection, visibility, selection,
session lifecycle, and JSONL action transport. A renderer owns only the
picture: glyphs, depth notation, labels, occlusion, and layout.

This is a Display-side extension. It does not add a second camera, a second
action reducer, or an Operate/Analysis tool. Selecting a renderer is episode
configuration. It is not an Agent `set_display` action, and it does not change
orbit, reset, viewport, or saved-view meanings.

The built-in id is `mattervis.native-v1`. With no renderer selected, static
`mat-vis tui` output and default JSONL sessions stay on that adapter.
`state_fingerprint` does not include renderer identity. Default observation
objects do not grow a `renderer` field. Picture bytes are the cross-interpreter
byte-identity contract for the default native backend. The fingerprint hashes
viewport floats as produced by that process. Those floats can differ by one
unit in the last place across CPython builds, so a full JSONL trace is not
required to be byte-identical across interpreters. Within one process the
fingerprint stays stable: reset restores it, and a camera change replaces it.

## Request and result

```python
from mat_viewer.tui import (
NATIVE_BACKEND_ID,
RENDER_API_VERSION,
TerminalSession,
render_picture,
resolve_renderer_binding,
)

binding = resolve_renderer_binding("example.marks-v1")
session = TerminalSession.from_file(
"structure.cif",
width=100,
height=36,
charset="ascii7",
renderer="example.marks-v1",
renderer_config="renderer.json",
)
manifest = session.replay_manifest()
```

`render_picture(...)` is the single dispatch used by
`compose_static_frame` and `DepthControls.render_frame`. `compose_frame`
remains the native implementation. The native adapter calls it with the host
crystal and camera, so the default picture is the compositor's picture.

Public schemas:

| Name | Value |
| --- | --- |
| API | `mattervis.tui.render/v1` |
| Request | `mattervis.tui.render_request/v1` |
| Display IR | `mattervis.tui.display_ir/v1` |
| Frame | `mattervis.tui.rendered_frame/v1` |
| Config | `mattervis.tui.renderer_config/v1` |
| Replay manifest | `mattervis.tui.renderer_replay/v1` |
| Entry point group | `mattervis.tui.renderers` |

`RenderRequest` carries display sites, manifested bonds, cell vectors, the
host camera basis, and the host `project_points` arrays. Cartesian positions
and the cell matrix are Angstrom. Basis rows are camera right, up, and
forward. Projected `xy` is the camera-plane coordinate returned by
`project_points` (orthographic values are divided by camera distance). Depth
is the camera-forward component; larger is closer. A null viewport means the
native compositor still computes its own fit. Active sessions pass the
controller's fitted viewport.

Bond provenance is graph-level (`provided`, `inferred`, or `unspecified`).
`explicit_bond_table: true` is provided input. Loader sources
`distance_heuristic`, `molcryskit`, and `canonical_scene` are inferred.
Any other metadata string is `unspecified` and is not copied into the
request. `bond_policy` is `manifested` (default, current drawing),
`provided_only` (inferred edges disabled, source graph not repaired), or
`bonds_off`.

The request has no field for `CrystalIR`, MolCrysKit chemistry, CIP or IUPAC
names, gold answers, scorers, or benchmark labels. Building it does not call
MolCrysKit.

`RenderedFrame.text` for the native backend is the compositor string.
External frames must be at most `width` by `height`, without a trailing
newline. `ascii7` is printable 7-bit ASCII (0x20–0x7E) with no ANSI escapes.
The static CLI status line can still contain a degree sign; JSONL screens do
not include that line.
The Unicode profile allows the terminal-safe characters the native viewer
already uses, including Braille and superscripts, and still rejects controls
and ANSI. The host recomputes `content_sha256` over those exact bytes
(`ascii` for ascii7, `utf-8` for Unicode). Session `frame.sha256` remains the
hash of the padded screen and is unchanged for default sessions.

## Registration

A backend is a zero-argument entry point in group `mattervis.tui.renderers`.
The entry point name is the backend id. The factory returns an object with:

- `backend_id`, `api_version` (`mattervis.tui.render/v1`)
- `deterministic` (`True`; other values are rejected)
- `package_version`, `implementation_digest` (lowercase sha256 hex)
- `validate_options(options) -> mapping`
- `render(request) -> str | BackendFrame`

`RendererBackend` is the `Protocol` for that object. `render` does not return
`RenderedFrame`; the host builds that record so a plugin cannot forge the
content hash or the backend digest.

`mattervis.native-v1` is reserved. An entry point cannot replace it.
Options are schema-checked by the backend. Unknown config fields, a missing
id, an API mismatch, a package or digest pin mismatch, a malformed frame, and
a plugin exception fail closed. Dispatch does not draw the native picture
instead.

CLI, for both `--no-interaction` and `--session-format jsonl`:

```bash
mat-vis tui structure.cif --no-interaction --charset ascii7 \
--width 80 --height 24 \
--renderer example.marks-v1 \
--renderer-config examples/mattervis_mark_renderer/pins/example.marks-v1.json
```

`--observation-profile model` is JSONL-only. `legacy` (the default) is the
historical observation. `model` keeps the frame, camera, display toggles,
selection identities, and `state_fingerprint`, and drops the source title,
chemistry warnings, and formula/composition scope fields. The title becomes
`display`.

An installed example lives in `examples/mattervis_mark_renderer`. It is its
own wheel. See that directory's README for install and pin commands.

`renderer_search_path` on `TerminalSession` / `resolve_renderer_binding`
discovers entry points in an extra directory of installed distributions. It
is an operator argument, not a JSONL action, and it is not read from CIF
metadata. The package still has to be importable. Do not pass
`module:function` strings from a model.

## Replay identity

`session.replay_manifest()` records backend id, API, package version,
implementation digest, config hash, options, bond policy, charset, and
viewport size. It is not part of `state_fingerprint` and is omitted from
default observations. Non-default episodes also attach that manifest, plus
the frame's `input_digest`, `view_digest`, and unpadded `content_sha256`,
to the observation under `renderer`.

Same display IR, view, config, and implementation digest produce the same
frame bytes for a deterministic backend. Nondeterministic backends are
rejected. Renderer choice is an experiment factor. It does not change task
identity, action names, the scoring oracle, or the allowed tool set. MatterVis
does not own those.

## Trust boundary

Loading a Python entry point executes code with host privileges. A `Protocol`
is not a sandbox. A trusted local plugin may be an installed wheel. An
untrusted contestant renderer must run in a separate process or container
with isolated mounts and network, receiving only the public request JSON.
This version does not ship a second subprocess renderer.

The native adapter is allowed to read the host crystal so it can keep calling
`compose_frame`. External `render` methods are not given that object. The
native molecule overview can still draw manifested species labels, because
that compositor reads `CrystalIR`. Experiments that must not expose those
labels should use atom level or an external renderer whose request has been
audited.

Diagnostics on `BackendFrame` are a host audit artifact. They are not written
into the agent observation.

## What this does not validate

Satisfying the interface does not show that a renderer cannot infer chemistry
from coordinates. It does not validate a benchmark, a scorer, a task split, or
a human reading of either picture. It does not implement research harness
issue 17, and it does not close MatterVis issue 163. Regression coverage here
is byte identity of the default compositor, fail-closed selection, charset
bounds, camera and action stability across backends, and canaries for hidden
chemistry fields.
18 changes: 17 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,23 @@ bytes.

`--charset unicode|ascii7` is available for both static and JSONL output.
`ascii7` selects printable ASCII geometry and implies monochrome output;
`unicode` preserves the default Braille renderer. JSONL sessions support
`unicode` preserves the default Braille renderer. `ascii7` frames are
printable 7-bit ASCII without ANSI escapes. The static status line above a
non-interactive frame can still contain a degree sign; it is not part of the
frame or of a JSONL observation. The Unicode profile may contain
Braille and superscripts; JSONL sessions stay monochrome, while interactive
and static colour Unicode can still contain ANSI from the built-in compositor.

`--renderer ID` and `--renderer-config FILE` select one frame backend for both
static output and `--session-format jsonl`. The default is `mattervis.native-v1`,
and omitting the flags leaves the picture and default JSONL traces unchanged.
A missing id, incompatible API, bad pin, or malformed frame exits with an
error and does not draw the native picture instead. Renderer choice is not a
JSONL action. `--observation-profile model` applies only to JSONL observations
and strips chemistry warnings, formulas, and the source title. See
[the renderer contract](agents/tui_renderer_api.md).

JSONL sessions support
`observe`, `reset`, `orbit`, `align`, `pan`, `zoom`, `fit`, `set_display`,
`select`, `focus`, `clear_selection`, `clear_focus`, and `close`.

Expand Down
23 changes: 23 additions & 0 deletions docs/dev-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ This file holds implementation-level details that were previously in
AGENTS.md. They are important for developers working on specific
modules but are too granular for the top-level developer contract.

## TUI renderer dispatch

`render_picture` is the only production path from `compose_static_frame` and
`DepthControls.render_frame` to a backend. `compose_frame` stays the native
implementation. The native adapter reads the host crystal through a context
variable set only around `NativeRenderer.render`. External backends receive
`RenderRequest` and must not be handed `CrystalIR`. Do not catch a backend
failure and call the native compositor as a fallback.

Default sessions omit renderer provenance so `state_fingerprint` and the
JSONL object stay on the historical contract. `replay_manifest()` is the
explicit replay record. `observation_profile="model"` is opt-in and does not
change the fingerprint inputs. Do not round viewport values before hashing:
that would change default fingerprints. Picture bytes are the byte-identity
contract across CPython versions. Episode JSONL compares those bytes exactly
and treats viewport floats as tolerant, because one ULP of `y_min`/`y_max`
changes the fingerprint without changing the frame.

The native molecule overview still reads manifested species labels from
`CrystalIR`. That is trusted built-in presentation, not a field on the public
request. An external renderer cannot see those labels unless they are already
encoded in approved display text such as site labels.

## Interactive figure scheduling and delivery

`capture_state` publishes state only. `update_view` schedules the ordinary
Expand Down
82 changes: 82 additions & 0 deletions examples/mattervis_mark_renderer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# mattervis-mark-renderer

Example terminal renderer for MatterVis. It is a separate distribution: install
it beside MatterVis and select it by id. The package is not part of
`mat_viewer` and does not replace `mattervis.native-v1`.

The picture is ASCII only. Each visible site is one character: `1` (front),
`2` (middle), or `3` (back) when depth marks are enabled, otherwise the
configured marker (`#` by default). Enabled bonds leave a `.` in an empty
midpoint cell. Element symbols, Braille, and the native axes footer are not
drawn. Host-projected coordinates are reused; this package does not own the
camera.

## Install

From a checkout that already has MatterVis installed:

```bash
python -m pip install --no-deps examples/mattervis_mark_renderer
```

`--no-deps` avoids resolving MatterVis again. The package version is `0.1.0`.

## Select it

Static frame:

```bash
mat-vis tui structure.cif --no-interaction --charset ascii7 \
--width 80 --height 24 \
--renderer example.marks-v1 \
--renderer-config examples/mattervis_mark_renderer/pins/example.marks-v1.json
```

Stateful JSONL session (same flags):

```bash
printf '%s\n' '{"action":"observe"}' '{"action":"orbit","arguments":{"yaw_deg":20}}' '{"action":"close"}' | \
mat-vis tui structure.cif --session-format jsonl --charset ascii7 \
--width 80 --height 24 \
--renderer example.marks-v1 \
--renderer-config examples/mattervis_mark_renderer/pins/example.marks-v1.json
```

Python, without a shell patch:

```python
from mat_viewer.tui import TerminalSession

session = TerminalSession.from_file(
"structure.cif",
width=80,
height=24,
charset="ascii7",
renderer="example.marks-v1",
renderer_config="examples/mattervis_mark_renderer/pins/example.marks-v1.json",
)
print(session.observe().as_dict()["frame"]["text"])
print(session.replay_manifest())
```

An isolated directory of wheels can be passed as `renderer_search_path=[path]`.
The package must also be importable. Renderer choice is fixed for the episode.
It is not a `set_display` action.

## Pin the implementation

`require_package_version` is in the sample config. To pin the source digest
too, read it from the installed backend and set
`require_implementation_digest` to that sha256 hex string:

```bash
python -c "from mattervis_mark_renderer import create_renderer; print(create_renderer().implementation_digest)"
```

A missing id, a wrong API version, an unknown option, a pin mismatch, or a
malformed frame fails the session. MatterVis does not draw the native picture
instead.

Python entry points run with host privileges. This package is a trusted local
plugin, not a sandbox. An untrusted renderer must run in a separate process
that only receives the public render request.
10 changes: 10 additions & 0 deletions examples/mattervis_mark_renderer/pins/example.marks-v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"schema": "mattervis.tui.renderer_config/v1",
"backend_id": "example.marks-v1",
"api_version": "mattervis.tui.render/v1",
"bond_policy": "manifested",
"require_package_version": "0.1.0",
"options": {
"example.marks-v1.marker": "#"
}
}
17 changes: 17 additions & 0 deletions examples/mattervis_mark_renderer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "mattervis-mark-renderer"
version = "0.1.0"
description = "Example MatterVis TUI renderer with a distinct ASCII depth policy"
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["matter-vis"]

[project.entry-points."mattervis.tui.renderers"]
"example.marks-v1" = "mattervis_mark_renderer:create_renderer"

[tool.setuptools.packages.find]
where = ["src"]
Loading
Loading