Changelog
0.20.0
Sites can now be partially occupied or shared between multiple species. Pass a
Compositioninspeciesto declare a mixed site; the renderer draws it as a pie of wedges, one per constituent species, with vacancy fractions filled opaquely with the canvas background colour.from_pymatgen()propagates partial occupancies from the source structure.New render style fields
wedge_start_angle,show_wedge_edges, andvacancy_colourcontrol the appearance of mixed-site wedges.visiblenow applies only to pure-string site rows. Constituents of aCompositionare always rendered, regardless of theirvisibleflag, and the legend follows suit. This keeps wedge rendering consistent with bond detection and rule lookups (which already operated on every constituent of a mixed site).
0.19.0
StructureScene.speciesis now stored as a tuple. The sequence is fixed at construction.New
select_by_species()method filters a full-length per-atom array to keep only selected species, filling the rest with the appropriate missing sentinel (NaNfor numeric,Nonefor categorical).set_atom_data()gainsby_speciesandby_indexkeyword arguments for sparse per-atom metadata assignment.by_speciesmaps species labels to values;by_indexmaps atom indices. Both can be combined in one call, withby_indextaking precedence at overlapping atoms.set_atom_data()no longer accepts a dict as its positionalvaluesargument. Useby_index=instead.Missing entries in sparse categorical atom data are now filled with
None(object-dtype) instead of empty strings. Both are treated as missing by the rendering pipeline;Nonecannot collide with a real label.The
AtomDatacontainer is no longer re-exported fromhofmannorhofmann.model. The only supported way to obtain an instance is to read theatom_dataproperty of a scene; direct construction is considered an internal implementation detail. Users who importedAtomDatafromhofmannshould migrate to theStructureScenewrite methods:set_atom_data()for assignment,del_atom_data()for targeted removal, andclear_2d_atom_data()for dropping all per-frame entries at once. The class itself is documented in the API reference for inspection of instances obtained viascene.atom_data.The per-atom metadata container no longer takes a
framesargument at construction and no longer exposes ann_framesproperty. Each write declares its expected frame count at the call site; the container no longer stores any reference to the scene’s trajectory list.StructureScenegainsdel_atom_data()for targeted removal of a single entry andclear_2d_atom_data()for dropping every per-frame (2-D) entry in one go while preserving static per-atom (1-D) entries.set_atom_data()now validates the prospective post-write state of the container in a single walk. A 2-D input’sshape[0]must equallen(scene.frames), and any already-stored 2-D entry not being overridden by this write must agree with the same frame count. Mismatches raiseValueErrornaming the offending key and, for the stale-stored case, pointing atclear_2d_atom_data()for recovery.A single 2-D per-atom metadata entry can now be reassigned in place at a new shape after extending the trajectory:
scene.frames.append(new_frame) scene.set_atom_data("energy", new_energy_at_new_shape)
The stored version of the key is treated as overridden by the pending write and replaced atomically. For scenes holding two or more 2-D entries,
clear_2d_atom_data()is still required before the first reassignment because the other 2-D entries are now stale. The resulting error names the stale key so the user knows what to drop.Appending to
scene.framesafter assigning 2-D per-atom metadata used to silently leave the container holding a stale array. Rendering now raises a clear error at the start of everyrender_*call, identifying the mismatch rather than failing later with a confusing numpyIndexError.The
atom_dataproperty now returns a read-only mapping view. The underlying container inherits fromcollections.abc.Mapping, so every mutation entry point raises at the Python protocol level:scene.atom_data[key] = arranddel scene.atom_data[key]raiseTypeError, whilescene.atom_data.pop(...),.popitem(),.setdefault(),.update(...), and.clear()raiseAttributeError(none of those methods exist on aMapping). Use the scene’s write methods instead.The
atom_datasetter has been removed.scene.atom_data = ...raisesAttributeError.The
reprofscene.atom_datanow includes array shape information:>>> scene.atom_data AtomData({'charge': (3,), 'energy': (5, 3)})
Previously only the keys were shown.
0.18.0
The
AtomDatacontainer now exposes derived per-key metadata via read-only mapping attributesrangesandlabels, replacing the previousglobal_range()andglobal_labels()methods. Callers migrate with a direct substitution:ad.global_range(key)becomesad.ranges[key], andad.global_labels(key)becomesad.labels[key]. The results are computed eagerly on assignment, so every access is a simple dictionary lookup.The
AtomDatacontainer rejects unsupported dtypes at assignment time with a clear error message. Supported dtypes are bool, integer, float, string, and object; complex, datetime, bytes, and other dtypes now raiseValueErrorat assignment rather than failing later in the rendering pipeline.resolve_atom_coloursis no longer part of the public API. Colour resolution goes through theStructureScenerendering methods.
0.17.0
StructureSceneatom_dataarrays are now stored read-only. In-place mutation of a returned array raisesValueError: assignment destination is read-onlyinstead of silently bypassing shape validation and cache invalidation. Update values by building a new array and passing it throughset_atom_data().
0.16.0
New
light_directionparameter onRenderStylecontrols the direction of the virtual light source for polyhedra face shading, specified in screen space (x = right, y = up, z = towards viewer). The default(0, 0, 1)preserves the existing behaviour; set an off-axis direction such as(-0.3, 0.5, 1.0)for visible face shading from top-down viewing angles.
0.15.1
Rendering now warns when a species has no
AtomStyle. The warning lists all affected species.
0.15.0
Interactive viewer: frame indicator (
f), go-to-frame (g), set-step (s), and step-aware frame navigation.New
render_animation()method for exporting trajectories as GIF or MP4 animations.atom_datais now a validatedAtomDatacontainer that checks array shapes on assignment. It also accepts 2-D arrays of shape(n_frames, n_atoms)so that colourmap-based colouring can vary per frame in animations and the interactive viewer.
0.14.2
Fixed bond completion missing padding image atoms near cell boundaries. Padding atoms now get their full coordination shell, matching physical atoms.
0.14.1
Fixed cell edge lines being clipped at atoms that are not drawn. Atoms hidden by
hide_centre,visible, or slab clipping no longer produce spurious gaps in cell outline edges.
0.14.0
Breaking:
Framenow carries alatticefield (shape(3, 3)orNone). Thelatticefield onStructureScenehas been replaced by a read-only property that delegates toframes[0].lattice. Code that constructed aStructureScenewithlattice=...should move the lattice onto eachFrameinstead.This correctly supports NPT (variable-cell) trajectories where the unit cell changes between frames. Rendering functions now resolve the lattice from the current frame, so cell edges, periodic bonds, and axes widgets update per frame.
New
from_ase()constructor andfrom_ase()classmethod for building scenes directly from ASEAtomsobjects, without requiring pymatgen. Supports both periodic and non-periodic systems, single structures and trajectories (list[Atoms]orase.io.Trajectory), and the same style, bond, polyhedra, and view options asfrom_pymatgen(). ASE is available as an optional dependency:pip install "hofmann[ase]".from_ase()stores the lattice per frame, correctly supporting NPT trajectories where the cell changes between frames.
0.13.1
Added widget positioning documentation covering the
corner,margin, and custom coordinate parameters shared byAxesStyleandLegendStyle.Documentation figures are now generated at Sphinx build time via a
builder-initedhook, rather than being pre-generated and committed to the repository. This ensures figures always reflect the current rendering code.pymatgenis now included in thedocsoptional extra. SetSKIP_IMAGE_GEN=1to skip figure generation during rapid local iteration.
0.13.0
Breaking:
LegendItemis now an abstract base class. Use the concrete subclassesAtomLegendItem(circle markers),PolygonLegendItem(regular-polygon markers withsidesandrotation), orPolyhedronLegendItem(miniature 3D icons withshapeand optionalrotation).Migration:
LegendItem(key=..., colour=...)becomesAtomLegendItem(key=..., colour=...).LegendItem(key=..., colour=..., sides=6)becomesPolygonLegendItem(key=..., colour=..., sides=6).LegendItem(key=..., colour=..., polyhedron="octahedron")becomesPolyhedronLegendItem(key=..., colour=..., shape="octahedron").LegendItem.from_polyhedron_spec(...)becomesPolyhedronLegendItem.from_polyhedron_spec(...).
Serialisation:
to_dict()now includes a"type"discriminator;LegendItem.from_dict()dispatches to the correct subclass. Saved style files from 0.12.x that contain polygon or polyhedron legend items must be re-saved; only plain atom-style dicts (nosides/polyhedronfields) are handled without a"type"key.PolyhedronLegendItemgains arotationparameter accepting a(3, 3)rotation matrix or an(Rx, Ry)tuple of angles in degrees. WhenNone(the default), the standard oblique legend viewing angle is used.Fix legend edge width scaling: legend marker outlines were incorrectly multiplied by the widget display-space scaling factor, making them appear thicker than the corresponding edges in the scene.
0.12.0
LegendItemgains apolyhedronfield. Set it to"octahedron","tetrahedron", or"cuboctahedron"to render a miniature 3D-shaded polyhedron icon in the legend instead of a flat circle or polygon marker. Polyhedron icons default to twice the flat-marker radius so that the 3D shading is legible at typical figure sizes.LegendItemgains per-itemedge_colourandedge_widthfields. When set, these override the scene-level outline settings; when unset, items fall back to the scene’soutline_colourandoutline_width. Settingshow_outlines=Falsedisables edges only for items that do not define their own edge styling.New
from_polyhedron_spec()classmethod creates a legend item from aPolyhedronSpec, inheriting colour, alpha, and edge settings without duplication.render_legend()gains apolyhedra_shadingparameter controlling the shading strength of 3D polyhedron icons (0 = flat, 1 = full).Fix bounding box computation in
render_legend()to account for edge linewidth, preventing clipped outlines on tightly-cropped legend images.
0.11.1
Internal: extract legend rendering from
painter.pyinto a dedicatedlegend.pymodule, and move the shared widget scaling constant into_widget_scale.py.
0.11.0
Internal: legend drawing now runs through
LegendItemobjects. A new_build_legend_itemshelper assembles items from the scene’s species and atom styles, and_draw_legend_widgetconsumes the resulting list.New
LegendItemclass bundles per-entry legend data (key, colour, optional label, optional radius) with validated property setters following theBondSpecpattern.LegendStylegains anitemsparameter. Pass a tuple ofLegendIteminstances to display a fully custom legend (e.g. forcolour_bydata) instead of the default species-based entries.LegendItemsupports regular-polygon markers viasidesandrotationfields. Setsides(>= 3) to draw a polygon instead of a circle, androtationto rotate it in degrees. Useful for indicating polyhedra types in the legend.LegendItemgains agap_afterfield for non-uniform vertical spacing. Each item can override the style-levelspacingfor the gap below it;Nonefalls back toLegendStyle.spacing.LegendItemgains analphafield (0.0–1.0, default 1.0) for semi-transparent marker faces. Marker outlines remain fully opaque, matching the visual style of polyhedra.
0.10.2
Internal: replaced four parallel per-polyhedron lists in
_PrecomputedScene(poly_base_colours,poly_alphas,poly_edge_colours,poly_edge_widths) with a single list of frozen_PolyhedronRenderDatadataclass instances, making the coupling between colour, alpha, and edge style explicit.
0.10.1
BondSpecvalidation is now extracted into per-field private methods, removing duplication between__init__and property setters. Thecoloursetter now validates its input vianormalise_colour(), closing a gap where invalid colours were silently accepted post-construction.
0.10.0
New
show_legendoption onRenderStyledraws a vertical column of coloured circles with species labels. Customise placement, font size, circle sizing, spacing, and label gap viaLegendStyle.LegendStyle.circle_radiusaccepts three forms: a uniform float, a(min, max)tuple for proportional sizing based onAtomStyle.radius, or a per-species dict for explicit control.render_legend()produces a tightly-cropped legend image without any structure, useful for composing figures in external tools. Supportsfigsizefor fixed output dimensions andtransparentbackgrounds.LegendStyle.label_gapcontrols the horizontal gap between legend circles and species labels (default 5.0 points).LegendStyle.labelsaccepts a dict mapping species names to display strings. Common chemical notation is auto-formatted: trailing charges become superscripts with tight kerning ("Sr2+"renders as Sr^2+), embedded digits become subscripts ("TiO6"renders as TiO_6), and strings containing$are passed through as explicit matplotlib mathtext.Legend entry spacing is automatically widened when labels contain super/subscripts, unless the user has explicitly set a custom
spacingvalue.
0.9.0
Periodic boundary handling has moved from scene construction to render time.
from_pymatgen()no longer expands image atoms at construction; instead,Bondcarries animagefield recording which lattice translation the bond crosses, and a newRenderingSetpipeline materialises image atoms on demand during rendering. This means the same scene can be rendered with different PBC settings without reconstruction.The pipeline applies five stages in order:
Single-pass completion (
completeonBondSpec)Recursive expansion (
recursiveonBondSpec)Geometric padding (
pbc_paddingonRenderStyle)Polyhedra vertex completion
Molecule deduplication (
deduplicate_moleculesonRenderStyle)
See Scenes and structures for details.
pbc,pbc_padding,max_recursive_depth, anddeduplicate_moleculesare now fields onRenderStylerather thanStructureScene, so rendering style is fully separated from structure data.Bond detection for periodic structures dispatches based on the inscribed sphere radius of the unit cell. When all bond lengths are shorter than the inscribed sphere radius (the common case), the minimum image convention (MIC) gives an efficient single-pass computation. When bond lengths are comparable to cell dimensions, all 27 image offsets are checked iteratively. Both paths use O(n^2) peak memory.
Recursive expansion can produce duplicate molecular fragments. The deduplication stage detects extended structures (slabs, frameworks) that wrap the unit cell and protects them from removal, removes non-wrapped components whose source atoms are a subset of a wrapped component, and selects a canonical copy among remaining duplicates using an unwrapped fractional centre-of-mass tie-breaker.
StructureScenenow validates thatviewis aViewStateon assignment, with a helpful hint when a tuple fromrender_mpl_interactive()is accidentally assigned without unpacking.Passing
Nonefor a nullable style keyword argument (e.g.pbc_padding=None) inrender_mpl()now correctly passes through as an explicit override rather than being silently dropped.
0.8.0
The monolithic
model.py(1888 lines) andrender_mpl.py(2517 lines) have been split into three sub-packages organised by architectural layer:hofmann.model– data types (colour utilities, atom/bond/polyhedron specs, rendering styles, view state, and the scene container).hofmann.construction– scene building (file parsing, bond and polyhedra computation, element defaults, style I/O, and pymatgen/XBS scene constructors).hofmann.rendering– display (projection, bond geometry, cell edge rendering, the painter’s algorithm, static output, and the interactive viewer).
All public import paths are preserved:
from hofmann import BondSpecandfrom hofmann.model import BondSpeccontinue to work.
0.7.1
Avoid quadratic array growth in
_merge_expansionsduring periodic boundary expansion. Accepted image coordinates are now collected in a list with O(1) hash-based deduplication and concatenated once at the end, matching the approach already used by_expand_neighbour_shells.StructureScenenow validates that every frame has the same number of atoms as thespecieslist at construction time, raisingValueErrorimmediately instead of failing with a confusing error during rendering.look_along()now returnsself, enabling one-liner construction such asViewState(centre=centroid).look_along([1, 1, 1]).Passing
Nonefor a style keyword argument inrender_mpl()now resets that field to theRenderStyleclass default instead of being silently ignored.BondSpec,AtomStyle,PolyhedronSpec, andViewStatenow validate their numeric fields at construction time, raisingValueErrorfor out-of-range values (e.g. negative radii,min_length > max_length,alphaoutside[0, 1], non-positivezoomorview_distance).render_mpl(),render_mpl_interactive(),centre_on(), andfrom_pymatgen()now raise descriptiveValueErrormessages for out-of-range index arguments (frame_index,atom_index,centre_atom) instead of leaking bareIndexErrorexceptions.
0.7.0
from_pymatgen()(and thefrom_pymatgen()classmethod) now acceptatom_styles,title,view, andatom_datakeyword arguments, allowing styles to be configured at construction time rather than requiring post-hoc mutation.New JSON-based style persistence. All style classes gain
to_dict()/from_dict()methods for serialisation, and module-levelsave_styles()/load_styles()functions write and read style files containing any combination ofatom_styles,bond_specs,polyhedra, andrender_stylesections.save_styles()andload_styles()provide convenience methods on the scene itself. See Styles and presets for details.New
StyleSetdataclass returned byload_styles().
0.6.0
BondSpecnow only requiresspeciesandmax_length.min_lengthdefaults to0.0;radiusandcolourdefault to class-level values (BondSpec.default_radius = 0.1,BondSpec.default_colour = 0.5) which can be changed to set project-wide defaults. Therepr()shows<default ...>for values that have not been explicitly set.
0.5.0
render_mpl()now accepts anaxparameter to render into an existing matplotlib axes, enabling multi-panel figures and composition with other plots.
0.4.0
Default bond detection now uses VESTA bond length cutoffs (bundled as JSON, sourced from pymatgen) instead of the covalent-radii-sum heuristic. Self-bonds (e.g. C-C) are included automatically when present in the VESTA data. The
toleranceandself_bondsparameters ondefault_bond_specs()have been removed.
0.3.0
Bond completion across periodic boundaries.
BondSpecgains acompleteflag for single-pass completion of bonds at cell boundaries, and arecursiveflag for iterative search that follows chains of bonds across periodic images. See Scenes and structures for details.AtomStylegains avisibleflag (defaultTrue). Setting it toFalsehides atoms of that species and suppresses their bonds without removing them from the scene.BondSpec.completenow validates the species name against the bond spec’s species pair, catching typos that previously resulted in a silent no-op.Removed
PolyhedraVertexModeenum and thepolyhedra_vertex_modefield onRenderStyle. Vertex atoms are now always drawn in front of their connected polyhedral faces (the previous default behaviour).
0.2.2
Add dodecahedron project logo to README and repository.
0.2.1
Static renderer now uses per-axis viewport extents, producing tightly cropped output for non-square scenes.
0.2.0
Per-atom metadata colouring via colourmaps. Use
set_atom_data()and thecolour_byparameter onrender_mpl()to map numerical or categorical data to atom colours.Multiple
colour_bylayers with priority merging. Pass a list of keys to apply different colouring rules to different atom subsets; the first non-missing value wins for each atom.Polyhedra without an explicit colour now inherit the resolved
colour_bycolour of their centre atom.New public API:
resolve_atom_coloursfor programmatic colour resolution, andCmapSpectype alias for colourmap specifications.
0.1.0
Initial release.
Ball-and-stick rendering via matplotlib with depth-sorted painter’s algorithm.
Publication-quality vector output (SVG, PDF).
XBS
.bsand.mvfile format support.pymatgen
Structureinteroperability (optional dependency).Periodic boundary condition expansion with bond-aware and polyhedra-vertex-aware image generation.
Coordination polyhedra with configurable slab clipping modes.
Interactive matplotlib viewer with mouse rotation and zoom.