Metadata-Version: 2.5
Name: crystal-scene
Version: 0.1.0
Summary: Generate deterministic, renderer-neutral crystal scenes with pymatgen
Project-URL: Homepage, https://github.com/tiannianzhu/crystal-scene
Project-URL: Repository, https://github.com/tiannianzhu/crystal-scene
Project-URL: Issues, https://github.com/tiannianzhu/crystal-scene/issues
Author: CMPDC
License-Expression: MIT
License-File: LICENSE
Keywords: crystallography,materials-science,pymatgen,visualization
Requires-Python: >=3.12
Requires-Dist: pydantic<3,>=2.13
Requires-Dist: pymatgen>=2026.5.4
Description-Content-Type: text/markdown

# crystal-scene

`crystal-scene` converts one CIF string or a pymatgen `Structure` into a deterministic,
renderer-neutral Pydantic scene. It resolves bonds and periodic images on the backend
so HSC, Materiae, and other consumers can draw the same scientific structure consistently.

The package intentionally does not depend on Crystal Toolkit, Dash, a database, or a
frontend framework.

Python 3.12 or newer is required.

## Local integration

From an HSC or Materiae checkout managed by uv, add this checkout as an editable dependency:

```bash
uv add --editable /path/to/crystal-scene
```

Or install it into an active environment directly:

```bash
python -m pip install -e /path/to/crystal-scene
```

After publication, the package can be installed by name:

```bash
pip install crystal-scene
```

## Usage

```python
from pathlib import Path

from crystal_scene import build_crystal_scene

cif_text = Path("structure.cif").read_text(encoding="utf-8")
scene = build_crystal_scene(cif_text)
payload = scene.model_dump(mode="json")
```

`build_crystal_scene` also accepts a pymatgen `Structure`. Structure inputs are copied,
then all fractional coordinates are wrapped into the input unit cell; callers are not
mutated. CIF input must contain exactly one non-empty structure. Blank input, parser
errors, multiple structures, and empty structures raise `ValueError` or a pymatgen
parser exception.

## JSON contract

`scene.model_dump(mode="json")` is the canonical TypeScript-facing payload. Field names
are camel case and `schemaVersion` is currently `"1.0"`. A payload contains:

- `generator`: the CrystalNN implementation, exact pymatgen and pymatgen-core versions,
  disorder policy, and whether every input species has an oxidation state;
- `lattice.matrix`: three row vectors in angstroms;
- `sites`: stable input-cell indices, source labels, full species occupancies and oxidation
  states, and wrapped fractional coordinates;
- `siteImages`: every input-cell site plus the one-hop periodic site images needed as
  endpoints of the emitted bonds;
- `bonds`: each input-cell site's complete CrystalNN neighbor view, including distance in
  angstroms and CrystalNN topology weight.

A physical CrystalNN bond is undirected. Each serialized bond anchors one view of that
connection at `sourceSite` in the input cell, while `targetImage` translates `targetSite`
by an integer linear combination of the three lattice rows. The reverse neighbor view is
also emitted, so every input-cell site retains its complete coordination environment. In
fractional coordinates, the anchored bond vector is:

```text
sites[targetSite].fractional + targetImage - sites[sourceSite].fractional
```

Swapping the source and target therefore also requires negating `targetImage`. Renderers
can materialize `siteImages` directly and deduplicate coincident undirected segments.
Distinct images remain distinct connections, including bonds from a site to its own
periodic images.

For disordered sites, the scene always retains every species and occupancy. CrystalNN
uses pymatgen's `take_max_species` policy only to choose a representative species for
bond inference; equal maximum occupancies use pymatgen's deterministic first-maximum
behavior.

The input cell is preserved deliberately. Primitive, conventional, refined, or reduced
cells change site indices and may depend on symmetry tolerances, so they are not implicit
view switches in schema 1.0. Callers that need another cell must supply that pymatgen
`Structure` explicitly.

Colors, atomic radii, bond-cylinder dimensions, boundary-face duplicates, incomplete-bond
stubs, labels, and camera state are renderer policy and are not scene data. Magnetic
moments and coordination polyhedra are also deferred from 0.1.0: moments require an
explicit basis/unit contract, while polyhedra are derived geometry that renderers can
construct from the complete neighbor topology when needed. CrystalNN `weight` is a
method-specific topology value (and is normally `1` in the default unweighted mode), not
a chemical bond order.

## Development

```bash
uv sync
uv run pytest
uv run basedpyright
uv run ruff check .
uv run ruff format --check .
uv build
```

## Release

Update the package version, validate it locally, and push the release commit and tag:

```bash
uv version 0.2.0
uv run pytest
uv run basedpyright
uv run ruff check .
uv run ruff format --check .
uv build --no-sources
git add -- pyproject.toml uv.lock
git commit -m "Release crystal-scene 0.2.0"
git tag v0.2.0
git push origin main
git push origin v0.2.0
```

Create and publish a GitHub Release from that tag. The release workflow verifies that
the tag exactly matches `project.version`, repeats all checks, builds and validates the
wheel and source distribution, then publishes them to PyPI through Trusted Publishing.
The PyPI publisher must be configured for this repository, workflow `publish.yml`, and
GitHub environment `pypi`; no long-lived PyPI token is required.

The project is distributed under the terms of the MIT License. See [`LICENSE`](LICENSE).
