hofmann 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hofmann-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT Licence
2
+
3
+ Copyright (c) 2026 Benjamin J. Morgan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicence, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
hofmann-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: hofmann
3
+ Version: 0.1.0
4
+ Summary: A modern Python reimagining of the XBS ball-and-stick crystal structure viewer
5
+ Author-email: "Benjamin J. Morgan" <b.j.morgan@bath.ac.uk>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/bjmorgan/hofmann
8
+ Project-URL: Documentation, https://hofmann.readthedocs.io
9
+ Project-URL: Repository, https://github.com/bjmorgan/hofmann
10
+ Project-URL: Issues, https://github.com/bjmorgan/hofmann/issues
11
+ Keywords: crystallography,molecular-visualisation,ball-and-stick,xbs,matplotlib
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: matplotlib>=3.7
26
+ Requires-Dist: scipy>=1.10
27
+ Provides-Extra: pymatgen
28
+ Requires-Dist: pymatgen>=2024.1.1; extra == "pymatgen"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
32
+ Provides-Extra: docs
33
+ Requires-Dist: sphinx>=7.0; extra == "docs"
34
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
35
+ Requires-Dist: sphinx-autodoc-typehints>=2.0; extra == "docs"
36
+ Provides-Extra: all
37
+ Requires-Dist: hofmann[dev,docs,pymatgen]; extra == "all"
38
+ Dynamic: license-file
39
+
40
+ # hofmann
41
+
42
+ [![CI](https://github.com/bjmorgan/hofmann/actions/workflows/ci.yml/badge.svg)](https://github.com/bjmorgan/hofmann/actions/workflows/ci.yml)
43
+ [![Documentation](https://readthedocs.org/projects/hofmann/badge/?version=latest)](https://hofmann.readthedocs.io/en/latest/)
44
+
45
+ A modern Python reimagining of Methfessel's [XBS](https://www.ccl.net/cca/software/X-WINDOW/xbs/) ball-and-stick viewer (1995), named after [August Wilhelm von Hofmann](https://en.wikipedia.org/wiki/August_Wilhelm_von_Hofmann) who built the first ball-and-stick molecular models in 1865.
46
+
47
+ hofmann renders crystal and molecular structures as depth-sorted ball-and-stick images with static, publication-quality vector output (SVG, PDF) via matplotlib.
48
+
49
+ <p align="center">
50
+ <img src="docs/_static/llzo.svg" width="480" alt="LLZO garnet with ZrO6 polyhedra rendered with hofmann">
51
+ </p>
52
+
53
+ ## Features
54
+
55
+ - Static publication-quality output (SVG, PDF, PNG) via matplotlib
56
+ - XBS `.bs` and `.mv` (trajectory) file formats
57
+ - Optional pymatgen `Structure` interoperability
58
+ - Periodic boundary conditions with automatic image expansion
59
+ - Coordination polyhedra with configurable shading and slab clipping
60
+ - Unit cell wireframe rendering
61
+ - Interactive viewer with mouse rotation, zoom, and keyboard controls
62
+ - Orthographic and perspective projection
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install hofmann
68
+ ```
69
+
70
+ For pymatgen interoperability:
71
+
72
+ ```bash
73
+ pip install "hofmann[pymatgen]"
74
+ ```
75
+
76
+ ### Requirements
77
+
78
+ - Python 3.11+
79
+ - numpy >= 1.24
80
+ - matplotlib >= 3.7
81
+ - scipy >= 1.10
82
+ - pymatgen >= 2024.1.1 (optional)
83
+
84
+ ## Quick start
85
+
86
+ ### From an XBS file
87
+
88
+ ```python
89
+ from hofmann import StructureScene
90
+
91
+ scene = StructureScene.from_xbs("structure.bs")
92
+ scene.render_mpl("output.svg")
93
+ ```
94
+
95
+ ### From a pymatgen Structure
96
+
97
+ ```python
98
+ from pymatgen.core import Lattice, Structure
99
+ from hofmann import StructureScene, BondSpec
100
+
101
+ lattice = Lattice.cubic(5.43)
102
+ structure = Structure(
103
+ lattice, ["Si"] * 8,
104
+ [[0.0, 0.0, 0.0], [0.5, 0.5, 0.0],
105
+ [0.5, 0.0, 0.5], [0.0, 0.5, 0.5],
106
+ [0.25, 0.25, 0.25], [0.75, 0.75, 0.25],
107
+ [0.75, 0.25, 0.75], [0.25, 0.75, 0.75]],
108
+ )
109
+
110
+ bonds = [BondSpec(species=("Si", "Si"), min_length=0.0,
111
+ max_length=2.8, radius=0.1, colour=0.5)]
112
+ scene = StructureScene.from_pymatgen(structure, bonds, pbc=True)
113
+ scene.render_mpl("si.pdf")
114
+ ```
115
+
116
+ ### Controlling the view
117
+
118
+ ```python
119
+ scene.view.look_along([1, 1, 0]) # View along [110]
120
+ scene.view.zoom = 1.5 # Zoom in
121
+ scene.view.perspective = 0.3 # Mild perspective
122
+ scene.render_mpl("rotated.svg")
123
+ ```
124
+
125
+ ### Interactive viewer
126
+
127
+ ```python
128
+ view, style = scene.render_mpl_interactive()
129
+
130
+ # Reuse the adjusted view for static output:
131
+ scene.view = view
132
+ scene.render_mpl("final.svg", style=style)
133
+ ```
134
+
135
+ ## Documentation
136
+
137
+ Full documentation is available at [hofmann.readthedocs.io](https://hofmann.readthedocs.io/), covering:
138
+
139
+ - [Getting started](https://hofmann.readthedocs.io/en/latest/getting-started.html) -- installation and first renders
140
+ - [User guide](https://hofmann.readthedocs.io/en/latest/user-guide.html) -- views, render styles, bonds, polyhedra, unit cells
141
+ - [Interactive viewer](https://hofmann.readthedocs.io/en/latest/interactive.html) -- mouse and keyboard controls
142
+ - [XBS file format](https://hofmann.readthedocs.io/en/latest/xbs-format.html) -- `.bs` and `.mv` format reference
143
+ - [API reference](https://hofmann.readthedocs.io/en/latest/api.html) -- full autodoc API
144
+
145
+ ## Licence
146
+
147
+ MIT. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,108 @@
1
+ # hofmann
2
+
3
+ [![CI](https://github.com/bjmorgan/hofmann/actions/workflows/ci.yml/badge.svg)](https://github.com/bjmorgan/hofmann/actions/workflows/ci.yml)
4
+ [![Documentation](https://readthedocs.org/projects/hofmann/badge/?version=latest)](https://hofmann.readthedocs.io/en/latest/)
5
+
6
+ A modern Python reimagining of Methfessel's [XBS](https://www.ccl.net/cca/software/X-WINDOW/xbs/) ball-and-stick viewer (1995), named after [August Wilhelm von Hofmann](https://en.wikipedia.org/wiki/August_Wilhelm_von_Hofmann) who built the first ball-and-stick molecular models in 1865.
7
+
8
+ hofmann renders crystal and molecular structures as depth-sorted ball-and-stick images with static, publication-quality vector output (SVG, PDF) via matplotlib.
9
+
10
+ <p align="center">
11
+ <img src="docs/_static/llzo.svg" width="480" alt="LLZO garnet with ZrO6 polyhedra rendered with hofmann">
12
+ </p>
13
+
14
+ ## Features
15
+
16
+ - Static publication-quality output (SVG, PDF, PNG) via matplotlib
17
+ - XBS `.bs` and `.mv` (trajectory) file formats
18
+ - Optional pymatgen `Structure` interoperability
19
+ - Periodic boundary conditions with automatic image expansion
20
+ - Coordination polyhedra with configurable shading and slab clipping
21
+ - Unit cell wireframe rendering
22
+ - Interactive viewer with mouse rotation, zoom, and keyboard controls
23
+ - Orthographic and perspective projection
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install hofmann
29
+ ```
30
+
31
+ For pymatgen interoperability:
32
+
33
+ ```bash
34
+ pip install "hofmann[pymatgen]"
35
+ ```
36
+
37
+ ### Requirements
38
+
39
+ - Python 3.11+
40
+ - numpy >= 1.24
41
+ - matplotlib >= 3.7
42
+ - scipy >= 1.10
43
+ - pymatgen >= 2024.1.1 (optional)
44
+
45
+ ## Quick start
46
+
47
+ ### From an XBS file
48
+
49
+ ```python
50
+ from hofmann import StructureScene
51
+
52
+ scene = StructureScene.from_xbs("structure.bs")
53
+ scene.render_mpl("output.svg")
54
+ ```
55
+
56
+ ### From a pymatgen Structure
57
+
58
+ ```python
59
+ from pymatgen.core import Lattice, Structure
60
+ from hofmann import StructureScene, BondSpec
61
+
62
+ lattice = Lattice.cubic(5.43)
63
+ structure = Structure(
64
+ lattice, ["Si"] * 8,
65
+ [[0.0, 0.0, 0.0], [0.5, 0.5, 0.0],
66
+ [0.5, 0.0, 0.5], [0.0, 0.5, 0.5],
67
+ [0.25, 0.25, 0.25], [0.75, 0.75, 0.25],
68
+ [0.75, 0.25, 0.75], [0.25, 0.75, 0.75]],
69
+ )
70
+
71
+ bonds = [BondSpec(species=("Si", "Si"), min_length=0.0,
72
+ max_length=2.8, radius=0.1, colour=0.5)]
73
+ scene = StructureScene.from_pymatgen(structure, bonds, pbc=True)
74
+ scene.render_mpl("si.pdf")
75
+ ```
76
+
77
+ ### Controlling the view
78
+
79
+ ```python
80
+ scene.view.look_along([1, 1, 0]) # View along [110]
81
+ scene.view.zoom = 1.5 # Zoom in
82
+ scene.view.perspective = 0.3 # Mild perspective
83
+ scene.render_mpl("rotated.svg")
84
+ ```
85
+
86
+ ### Interactive viewer
87
+
88
+ ```python
89
+ view, style = scene.render_mpl_interactive()
90
+
91
+ # Reuse the adjusted view for static output:
92
+ scene.view = view
93
+ scene.render_mpl("final.svg", style=style)
94
+ ```
95
+
96
+ ## Documentation
97
+
98
+ Full documentation is available at [hofmann.readthedocs.io](https://hofmann.readthedocs.io/), covering:
99
+
100
+ - [Getting started](https://hofmann.readthedocs.io/en/latest/getting-started.html) -- installation and first renders
101
+ - [User guide](https://hofmann.readthedocs.io/en/latest/user-guide.html) -- views, render styles, bonds, polyhedra, unit cells
102
+ - [Interactive viewer](https://hofmann.readthedocs.io/en/latest/interactive.html) -- mouse and keyboard controls
103
+ - [XBS file format](https://hofmann.readthedocs.io/en/latest/xbs-format.html) -- `.bs` and `.mv` format reference
104
+ - [API reference](https://hofmann.readthedocs.io/en/latest/api.html) -- full autodoc API
105
+
106
+ ## Licence
107
+
108
+ MIT. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hofmann"
7
+ version = "0.1.0"
8
+ description = "A modern Python reimagining of the XBS ball-and-stick crystal structure viewer"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Benjamin J. Morgan", email = "b.j.morgan@bath.ac.uk"},
14
+ ]
15
+ keywords = ["crystallography", "molecular-visualisation", "ball-and-stick", "xbs", "matplotlib"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering :: Chemistry",
25
+ "Topic :: Scientific/Engineering :: Visualization",
26
+ ]
27
+ dependencies = [
28
+ "numpy>=1.24",
29
+ "matplotlib>=3.7",
30
+ "scipy>=1.10",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/bjmorgan/hofmann"
35
+ Documentation = "https://hofmann.readthedocs.io"
36
+ Repository = "https://github.com/bjmorgan/hofmann"
37
+ Issues = "https://github.com/bjmorgan/hofmann/issues"
38
+
39
+ [project.optional-dependencies]
40
+ pymatgen = ["pymatgen>=2024.1.1"]
41
+ dev = ["pytest>=7.0", "pytest-cov>=4.0"]
42
+ docs = [
43
+ "sphinx>=7.0",
44
+ "sphinx-rtd-theme>=2.0",
45
+ "sphinx-autodoc-typehints>=2.0",
46
+ ]
47
+ all = ["hofmann[pymatgen,dev,docs]"]
48
+
49
+ [tool.setuptools.package-data]
50
+ hofmann = ["py.typed"]
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,65 @@
1
+ """Hofmann: a modern Python reimagining of the XBS ball-and-stick viewer.
2
+
3
+ Hofmann renders crystal and molecular structures as ball-and-stick images,
4
+ with static publication-quality output via matplotlib.
5
+
6
+ Example usage::
7
+
8
+ from hofmann import StructureScene
9
+
10
+ scene = StructureScene.from_xbs("structure.bs")
11
+ scene.render_mpl("output.svg")
12
+ """
13
+
14
+ from hofmann.bonds import compute_bonds
15
+ from hofmann.polyhedra import compute_polyhedra
16
+ from hofmann.defaults import (
17
+ COVALENT_RADII,
18
+ ELEMENT_COLOURS,
19
+ default_atom_style,
20
+ default_bond_specs,
21
+ )
22
+ from hofmann.model import (
23
+ AtomStyle,
24
+ AxesStyle,
25
+ Bond,
26
+ BondSpec,
27
+ CellEdgeStyle,
28
+ Colour,
29
+ Frame,
30
+ Polyhedron,
31
+ PolyhedronSpec,
32
+ RenderStyle,
33
+ SlabClipMode,
34
+ StructureScene,
35
+ ViewState,
36
+ WidgetCorner,
37
+ normalise_colour,
38
+ )
39
+ from hofmann.scene import from_pymatgen, from_xbs
40
+
41
+ __all__ = [
42
+ "AtomStyle",
43
+ "AxesStyle",
44
+ "Bond",
45
+ "BondSpec",
46
+ "CellEdgeStyle",
47
+ "COVALENT_RADII",
48
+ "Colour",
49
+ "ELEMENT_COLOURS",
50
+ "Frame",
51
+ "Polyhedron",
52
+ "PolyhedronSpec",
53
+ "RenderStyle",
54
+ "SlabClipMode",
55
+ "StructureScene",
56
+ "ViewState",
57
+ "WidgetCorner",
58
+ "compute_bonds",
59
+ "compute_polyhedra",
60
+ "default_atom_style",
61
+ "default_bond_specs",
62
+ "from_pymatgen",
63
+ "from_xbs",
64
+ "normalise_colour",
65
+ ]
@@ -0,0 +1,89 @@
1
+ """Bond computation from declarative BondSpec rules."""
2
+
3
+ from fnmatch import fnmatch
4
+
5
+ import numpy as np
6
+
7
+ from hofmann.model import Bond, BondSpec
8
+
9
+
10
+ def compute_bonds(
11
+ species: list[str],
12
+ coords: np.ndarray,
13
+ bond_specs: list[BondSpec],
14
+ ) -> list[Bond]:
15
+ """Compute bonds for a single frame based on bond specification rules.
16
+
17
+ For each pair of atoms (i < j), checks all bond specs in order to
18
+ find the first matching rule where the interatomic distance falls
19
+ within ``[min_length, max_length]``.
20
+
21
+ Species matching is pre-computed per spec so that the inner loop
22
+ over atom pairs is a vectorised numpy operation rather than
23
+ per-pair Python calls.
24
+
25
+ Args:
26
+ species: List of species labels, length ``n_atoms``.
27
+ coords: Coordinates array of shape ``(n_atoms, 3)``.
28
+ bond_specs: List of BondSpec rules to apply.
29
+
30
+ Returns:
31
+ List of Bond objects for all detected bonds.
32
+ """
33
+ if len(species) == 0 or len(bond_specs) == 0:
34
+ return []
35
+
36
+ coords = np.asarray(coords, dtype=float)
37
+ n_atoms = len(species)
38
+
39
+ # Vectorised pairwise distance matrix.
40
+ diff = coords[:, np.newaxis, :] - coords[np.newaxis, :, :]
41
+ dist_matrix = np.linalg.norm(diff, axis=2)
42
+
43
+ # Upper-triangle mask: only consider pairs (i < j).
44
+ upper = np.triu(np.ones((n_atoms, n_atoms), dtype=bool), k=1)
45
+
46
+ # Track which pairs have been claimed by an earlier spec.
47
+ claimed = np.zeros((n_atoms, n_atoms), dtype=bool)
48
+
49
+ # Pre-compute unique species for efficient matching.
50
+ unique_species = list(set(species))
51
+
52
+ bonds: list[Bond] = []
53
+
54
+ for spec in bond_specs:
55
+ # Determine which unique species match each side of the spec.
56
+ sp_a, sp_b = spec.species
57
+ match_a = {s for s in unique_species
58
+ if fnmatch(s, sp_a)}
59
+ match_b = {s for s in unique_species
60
+ if fnmatch(s, sp_b)}
61
+
62
+ # Boolean masks: which atoms match side a / side b.
63
+ mask_a = np.array([s in match_a for s in species])
64
+ mask_b = np.array([s in match_b for s in species])
65
+
66
+ # Species pair mask (symmetric matching): (a[i] & b[j]) | (b[i] & a[j]).
67
+ pair_mask = (
68
+ (mask_a[:, np.newaxis] & mask_b[np.newaxis, :])
69
+ | (mask_b[:, np.newaxis] & mask_a[np.newaxis, :])
70
+ )
71
+
72
+ # Distance filter.
73
+ dist_ok = (
74
+ (dist_matrix >= spec.min_length)
75
+ & (dist_matrix <= spec.max_length)
76
+ )
77
+
78
+ # Combine: upper triangle, species match, distance match, not claimed.
79
+ hits = upper & pair_mask & dist_ok & ~claimed
80
+
81
+ # Extract matching pairs.
82
+ ii, jj = np.nonzero(hits)
83
+ if len(ii) > 0:
84
+ claimed[ii, jj] = True
85
+ for idx in range(len(ii)):
86
+ i, j = int(ii[idx]), int(jj[idx])
87
+ bonds.append(Bond(i, j, float(dist_matrix[i, j]), spec))
88
+
89
+ return bonds