Source code for httk.analyse.crysviz
"""Open httk structures in the CrysViz viewer."""
import os
import tempfile
from pathlib import Path, PureWindowsPath
from typing import Any
from httk.core import register_citation, save
def _import_crysviz() -> Any:
try:
import crysviz # type: ignore[import-not-found]
except ImportError as exc:
raise ImportError("httk.analyse.crysviz requires crysviz; install httk-analyse[crysviz]") from exc
return crysviz
def _safe_filename(name: str) -> bool:
"""Return whether ``name`` is a non-path filename usable on every platform."""
return bool(
name
and name not in {".", ".."}
and not any(character in name for character in "/\\")
and not Path(name).is_absolute()
and not PureWindowsPath(name).is_absolute()
and not PureWindowsPath(name).drive
and all(character.isprintable() for character in name)
)
[docs]
def to_payload(
structure: Any,
*,
name: str | None = None,
format: str = "vasp-poscar",
) -> Any:
"""Serialize an httk structure as an in-memory CrysViz payload.
:param structure: Structure to serialize.
:param name: Optional filename for the payload, without a path.
:param format: Serialization format, either ``"vasp-poscar"`` or ``"cif"``.
:return: A CrysViz payload containing the serialized structure.
:raises ImportError: If CrysViz is not installed.
:raises ValueError: If ``format`` is not supported.
"""
if format not in {"vasp-poscar", "cif"}:
raise ValueError("format must be 'vasp-poscar' or 'cif'")
crysviz = _import_crysviz()
suffix = ".vasp" if format == "vasp-poscar" else ".cif"
if name:
if not _safe_filename(name):
raise ValueError("name must be a nonempty filename without a path")
filename = name
else:
formula = getattr(structure, "formula", None)
formula_text = str(formula) if formula is not None else ""
filename = formula_text if _safe_filename(formula_text) else "structure"
if not filename.casefold().endswith(suffix):
filename += suffix
with tempfile.TemporaryDirectory() as directory:
destination = Path(directory) / f"structure{suffix}"
save(structure, destination, format=format)
text = destination.read_text(encoding="utf-8")
return crysviz.Payload(filename, text)
[docs]
def show(*structures: Any, **viewer_kwargs: Any) -> Any:
r"""Open structures in CrysViz and return when its window is ready.
:param \*structures: CrysViz payloads, source paths, or httk structures to display.
:param \**viewer_kwargs: Keyword arguments forwarded to ``crysviz.show``.
:return: The ready CrysViz viewer, which also supports the context-manager protocol.
:raises ImportError: If CrysViz is not installed.
The call is non-blocking after the window is ready. Call ``viewer.wait()`` to
block until the window closes.
"""
crysviz = _import_crysviz()
sources: list[Any] = []
for structure in structures:
if isinstance(structure, (crysviz.Payload, str, os.PathLike)):
sources.append(structure)
else:
sources.append(to_payload(structure))
register_citation(
applies_to="Structure visualisation uses CrysViz",
references={
"authors": (
{"name": "Florian Trybel"},
{"name": "Abhijith S Parackal"},
{"name": "Oscar Bulancea-Lindvall"},
{"name": "Henricus R.A. ten Eikelder"},
{"name": "Rickard Armiento"},
),
"title": "CrysViz - Crystal Structure Visualisation & Analysis",
"url": "https://github.com/CrysViz/crysviz",
"year": "2026",
"bib_type": "misc",
},
)
return crysviz.show(sources, **viewer_kwargs)