This page mirrors the project's root CHANGELOG.md
For the version currently on PyPI, see the
release history on PyPI. For
the very latest (possibly unreleased) changes, see the
CHANGELOG.md
on the main branch.
Changelog
All notable changes to this project will be documented here. Format based on Keep a Changelog.
[Unreleased]
[0.2.3] - 2026-06-16
Fixed
- 3-D visualizer: scrolling now actually zooms. The mouse-wheel handler updated
GLWidget._zoom_multiplier, but the camera's eye distance inStructureRenderer.get_view_matrixwas hard-coded to4 Γ spanand ignored it β so the wheel only nudged the near/far clip planes (invisible under a perspective projection) and the molecule never changed apparent size.get_view_matrixnow takes the zoom multiplier and folds it into the eye distance (4 Γ span Γ zoom), matching the near/far scaling already used in_projection_matrix. - "Supported point groups" table (
README.mdanddocs/algorithm.md): the "Dihedral with Οd" row rendered the d full-size while the Οβ/Οα΅₯ rows used proper subscripts, because there is no Unicode subscript letter "d". The d subscripts (Ο<sub>d</sub>,Dβ<sub>d</sub>,Dββ<sub>d</sub>) now use<sub>so they align with the rest of the table.
Documentation
README.mdanddocs/user-guide.md: clarified the difference betweenprint_character_table_for(fire-and-forget convenience that prints and returns nothing) andget_or_generate_point_group(returns thePointGroupobject for further use), noting that the former is shorthand forget_or_generate_point_group(name).print_character_table().- Documentation site: added a consistent "Next steps" card grid (matching the
Home page style) to the bottom of every content page β Getting Started, User
Guide, Examples Gallery, Example Script, Algorithm & Supported Groups, API
Reference, and Glossary β so each page routes onward to its logical neighbours
as prominent clickable cards rather than easily-missed footer links. On the
Example Script page the cards sit above the embedded
example_usage.pydump so they aren't buried below a long scroll. docs/user-guide.md: every feature that has a command-line equivalent now shows it via a consistent "Python / Command line" tabbed pair (point-group determination, character tables, HTML/LaTeX export, idealized-structure generation and preview, rotor classification, symmetry operations, basis functions, the 3-D visualizer, and the sample molecules). Purely-Python helpers (periodic_table, thedisplayprinters) now say so explicitly instead of leaving the CLI status ambiguous.docs/user-guide.md: expanded the prose for each function to spell out what inputs are required or optional (e.g. that the exporters take a list of group names, that the sample helpers take a sample name rather than a path, thatgenerate_idealized_structurereads the order from the group name) and why each one is useful.docs/index.md: renamed the "Try it without installing anything yet" tip to "Try it without .xyz files", and extended the "Why pyrrhotite?" note with the py(thon) + rhotite (β rotate) wordplay behind the name.
[0.2.2] - 2026-06-13
Fixed
- Documentation site footer: the "Contact the maintainer" envelope icon used a
relative link (
about/#contact) which, since MkDocs does not rewrite social links, resolved relative to the current page and produced a broken doubled URL (e.g..../pyrrhotite/about/about/#contact). It now uses the absolute site URL. Asite_urlwas also added tomkdocs.yml(previously missing) so Material can generate correct canonical/absolute links. docs/index.md: the "Why pyrrhotite?" admonition title escaped its inner double-quotes as\", which rendered the literal backslashes on the site. It now uses typographic curly quotes so it renders cleanly.README.md: the twoexample_usage.pylinks were repository-relative, so they worked on GitHub but 404'd on the rendered PyPI project page. They are now absolute GitHub URLs that resolve everywhere the README is shown. The Quick-start blockquote also now clarifies that the script ships with the repository rather than the installed wheel, and points to the new docs page below.
Documentation
- Added a new "Example Script" page (
docs/example.md) that embeds the fullexample_usage.pyinline viapymdownx.snippets(kept in sync with the real file on every build) alongside a view/download-on-GitHub link, so the showcase is readable directly on the docs site without leaving it. Added to the Guide navigation and cross-linked from the User Guide intro.
[0.2.1] - 2026-06-12
Fixed
Symmetry._find_point_groupno longer silently mis-identifies molecules whose detected proper-rotation order exceeds the hardcoded group range. Previously a genuine high-order axis (e.g. a detected C11) could still match a low-order hardcoded group such as D2h with a non-negative operation surplus and win, because the analytical fallback only ran when no hardcoded group matched at all. A hardcoded group is now accepted only if it can account for the highest-order proper axis actually detected (newSymmetry._group_accounts_for_axis/PointGroup.num_proper_rotations); otherwise the analytical generator is used.Symmetry._generate_point_group_from_ops(the analytical fallback) was reading reflection-plane Οh/Οv/Οd labels that are not assigned until after point-group determination, so its plane tests were always False β it could never produce a Dnd group and misclassified Cnh/Dnh/Dnd. It now derives horizontal-vs-vertical planes geometrically from each plane's normal relative to the principal axis (the same criterion the labeller uses), correctly distinguishing Dnh (has Οh) from Dnd (only Οd), and uses the improper-rotation order for the Sn label. Net effect: the Cn/Cnh/Cnv/Dn/Dnh/Dnd families now round-trip correctly through generate-then-detect for the full adaptive range n = 3β20 (previously only the n β€ 10 cases covered by the test suite were reliable).- Narrowed an overly broad
except (ValueError, Exception)in the same fallback toexcept ValueError, so unexpected errors are no longer silently swallowed. Symmetry._find_point_groupnow also requires a candidate hardcoded group to account for the highest-order improper (Sβ) axis detected, not just the highest proper axis (newSymmetry._max_finite_degree/_group_accounts_for_axes, plusPointGroup.num_improper_rotations). This fixes two high-order mis-detections: an Sβ molecule (which contains the proper rotation C_{n/2}) was matching a hardcoded C_{n/2} and hiding its Sβ axis (e.g. S12 β C6), and a genuine D8d β whose S16 class is under-counted by the operation search β was losing to C8v. Both now fall through to the analytical generator and resolve correctly. All seven axial families now round-trip through generate-then-detect for the full adaptive range (Cn/Cnh/Cnv/Dn/Dnh/Dnd n = 3β20, Sn n = 4β20).generate_idealized_structure: for the Dn/Dnh/Dnd and Sn families the ring radius is now capped so the central hub stays within bonding distance of every ring atom regardless of n. Previously_radius_for(n)grew ~linearly with n, so for larger structures (e.g. D20h) the rings drifted beyond the hub-ring bonding cutoff and the hub rendered as an unconnected atom floating between the two rings in the 3-D viewer (new_hub_bonded_ring_radiushelper). Hub-ring bonding is verified for all of Dn/Dnh/Dnd/Sn through n = 20.list_sample_molecules(and the other*_samplehelpers indisplay.py) raisedFileNotFoundError: .../site-packages/tests/fileswhen called from a pip-installed package, because the sample XYZ files lived only in the repo'stests/files/directory, which is not shipped in the wheel. The sample molecules now live inside the package atpyrrhotite/sample_molecules/(declared as package-data inpyproject.toml) as the single source of truth:display.pyresolves them there, and the test suite'sXYZ_DIRwas repointed to the same directory, so the oldtests/files/directory has been removed.generate_idealized_structure: ring atoms no longer visually crowd ("smush") together at high orders. The central hub now uses caesium (the largest covalent radius inperiodic_table.py, raised to its true Alvarez-2008 value of 2.44 Γ ) instead of iron; the wider hub-ring bonding cutoff lets the rings sit at a larger radius while the hub still bonds to every ring atom, so adjacent ring atoms stay comfortably separated through n = 20 (e.g. minimum atom spacing in D20h rose from ~0.87 Γ to ~1.28 Γ , versus a ~0.8 Γ rendered sphere diameter).generate_idealized_structure: the central hub element for the Dn/Dnh/Dnd and Sn families is now chosen dynamically by ring size instead of always being caesium. A fixed Cs hub (covalent radius 2.44 Γ ) is right for large n β its wide bonding cutoff lets far-out rings stay connected β but at small n the rings sit only ~2 Γ from the centre, so the 2.44 Γ hub sphere visually engulfed them in the 3-D viewer._hub_element_fornow picks the smallest hub that still bonds to the ring at its natural radius (Cl β Fe β Cs), so small structures get a small hub and only large orders escalate to caesium. The hub still bonds to every ring atom and all seven axial families round-trip through detection across n = 3β20 (Sn 4β20).
Documentation
- Simplified the
pyrrhotite-vs-schoenfliescomparison table inREADME.mdanddocs/about.md: folded the HTML/LaTeX export row into a single "Character tables" row and added an "Idealized structure generation" row reflecting that feature. README.mdpolish: added PyPI/Python/license/docs badges, a name-origin note, a linked table of contents, a documentation-site pointer, a 3-D viewer screenshot (docs/assets/visualizer-fullerene.png), a rendered LaTeX export example, and cross-links to the User Guide / Algorithm pages. Consolidated the "viewer does not draw symmetry overlays yet" caveat to a single canonical spot, and fixed the staletests/files/*.xyzCLI example tosrc/sample_molecules/*.xyz.- Added a disclaimer to the idealized-structure-generator section (
README.mdanddocs/user-guide.md) clarifying that it only illustrates a point group's geometry β element and bond choices are for visualization only and do not represent real chemical structures β and documenting the supported families (noDnv; onlyDnh/Dnd) and the high-nrendering/detection limits (n β€ 20). - Added docstrings to previously undocumented helpers across the codebase: the
10 internal row-builders in
character_tables/generator.py, the visualizer's Qt/OpenGL override methods (gl_widget,shader_program,model_manager,structure_renderer,window,obj_loader), nested helpers insymmetry.py,display.py,operation_manager.py, andbasis_functions.py, and module docstrings for thevisualizer/modelsandvisualizer/shaderspackages. - Added teaching notes in
symmetry.pyexplaining the deliberately permissive asymmetric-top inertial filter and the scale-dependent absolute thresholds used for linearity/planarity. - Reworded stale "Direct translation of reference/β¦" / "Mirrors reference/β¦"
module headers to note that the vendored
reference/tree was removed in 0.2.0. README.md: documented that improper-axis families (Sβ, and the Sββ axis of Dβd) detect reliably only to ~n = 10, while the proper-axis families now detect across the full n = 3β20 adaptive range.- Added a "Detecting high-order axes (n > 10)" explainer to both
README.mdanddocs/algorithm.mdcovering the three mechanisms that lift the cap from n β€ 8 to n = 20 (geometry-bounded search order via_max_plausible_order, the order-dependent validation tolerancemin(0.1, Ο/(degreeΒ·(degree+1))), and matching that respects the highest detected proper/improper axis), plus an assessment of whether the fixed 10% tolerance is problematic. docs/stylesheets/extra.css: the Mermaid flowcharts on the User Guide and Algorithm pages now fill their nodes with the brand amber on navy (matching the "Get started" button) instead of a faint 10%-opacity wash; and the home-page navigation cards whose title is a link are now clickable across the whole card (a stretched-link::afteroverlay), scoped so multi-link cards like the About page's Contact cards are unaffected.docs/api.md: addedstructure_generator.format_xyz, plus a new "Display helpers" section documenting the fourpyrrhotite.displaypretty-printers (print_bond_pairs,print_ops_with_atoms,print_basis_functions,print_char_table_programmatic), which were previously undocumented.- Documented the
pyrrhotite.displaypretty-printers inREADME.mdanddocs/user-guide.mdas well, and surfaced the runnableexample_usage.pyfeature tour (previously only referenced in passing) in the README Quick start and the User Guide intro. example_usage.py: the showcase now loads its demo molecules via the bundledload_sample(...)helper instead of hard-codedtests\files\*.xyzpaths, which no longer exist (samples moved into the package in 0.2.1) and were Windows-only backslash paths β so the script now runs as-is on any platform straight afterpip install pyrrhotite..claude/CLAUDE.md: rewritten to describe the actualsrc/package layout (the file still referenced the obsoleteschoenflies/+reference/layout).- Source files normalised to plain UTF-8 (BOM removed).
[0.2.0] - 2026-06-10
Added
src/visualizer/β interactive 3-D molecule viewer built on PyQt6 and OpenGL. Atoms are drawn as spheres (coloured per element fromperiodic_table.py), bonds as cylinders, with an orientation gizmo (red/green/blue arrows for x/y/z) and an optional element-symbol overlay. Controls: left-drag to rotate (arcball), scroll to zoom. Exposed aspyrrhotite.visualize(structure, show_labels=False).- New optional install extra
pip install 'pyrrhotite[vis]'(PyQt6, PyOpenGL, pyrr, matplotlib) for the visualizer. - New CLI flags
--visualize/-vis(open the 3-D viewer after analysis) and--labels/-l(show element labels in the viewer; implies--visualize). src/display.pyβ pretty-printing helpers (print_bond_pairs,print_ops_with_atoms,print_basis_functions,print_char_table_programmatic) and sample-molecule convenience functions (list_sample_molecules,load_sample,analyse_sample,visualize_sample,show_character_table_sample) built on the bundledtests/files/molecules. All re-exported from the top-levelpyrrhotitepackage.src/character_tables/β character table generation split out into its own subpackage:generator.py(moved fromsrc/point_groups/character_table_generator.py)html_formatter.pyβformat_html()/save_html(), render character tables as standalone HTMLlatex_formatter.pyβformat_latex()/save_latex(), render character tables as LaTeX (requires thebooktabsandamsmathpackages)src/structure_generator.pyβgenerate_idealized_structure(point_group, ...)builds an idealizedStructure(rings of placeholder atoms) for any of the seven axial point groups (Cn, Cnh, Cnv, Sn, Dn, Dnh, Dnd) and order n, round-tripping throughSymmetryto recover the same label. Also addsformat_xyz()/write_xyz()for serialising the result to XYZ. Exposed viapyrrhotite.generate_idealized_structure/pyrrhotite.write_xyz, and via the CLI aspyrrhotite -g <NAME> --xyz [PATH].pyrrhotite.visualize_idealized_structure(point_group, ...)β generates an idealized structure and opens the 3-D viewer directly, without writing it to an.xyzfile first (requirespip install 'pyrrhotite[vis]'). Also available from the CLI aspyrrhotite -g <NAME> --visualize(or--labels/-l), when not combined with--xyz.tests/test_structure_generator.pyβ round-trip tests forgenerate_idealized_structureacross all seven axial families and orders n=3..10 (including n>8 to exercise the adaptive axis-order search), plus error-path tests for unsupported families/orders.example_usage.pyβ new section 14 demonstratinggenerate_idealized_structure,format_xyz, andwrite_xyz: basic generation and round-trip detection, saving/reloading from disk, round-tripping all seven axial families at n=6, customising radius/height/element, and the error cases for unsupported groups/orders.
Changed
- Character table generation now lives under
src/character_tables/instead ofsrc/point_groups/character_table_generator.py;parse_point_group_name,generate_point_group,get_or_generate_point_group, andprint_character_table_forare imported fromsrc.character_tables. - Symmetry detection (
src/symmetry.py) no longer caps proper-rotation search at Cn, n<=8. The three candidate-axis search functions (_find_proper_rotational_axes_along_principal_axes,_find_proper_rotational_axes_through_atoms,_find_proper_rotational_axes_between_atoms) now use a new_max_plausible_orderhelper, which derives a per-axis upper bound (capped at n=20) from the size of the largest ring of symmetry-equivalent atoms (same element, distance from axis, and position along axis) found around that axis. src/operations/operation_manager.py: the operation-validity tolerance is now tightened for high-order Cn/Sn candidates (degree >= 8), tomin(0.1, pi / (degree * (degree + 1))). This prevents a high-order axis (e.g. genuine C9) from also spuriously validating a neighbouring wrong-order candidate (e.g. C8), whose angular spacing would otherwise fall within the original fixed 0.1 tolerance.find_point_group,get_or_generate_point_group, andgenerate_point_groupnow accept either aPointGroupLabelor a Schoenflies name string (e.g."D6h"), parsing the string internally viaparse_point_group_name. Callers no longer need to writefind_point_group(parse_point_group_name("D6h"))βfind_point_group("D6h")is now sufficient.
Fixed
generate_idealized_structure's defaultradius/height(now 1.0 A / 0.6 A, previously 1.5 A / 1.0 A) were too large relative toStructure.calculate_bond_pairs's bonding cutoff for the placeholder element, leaving e.g. the two rings of a generated D9d structure as disconnected components with no bonds between them. The new defaults keep both within-ring and (for Dn/Dnd) cross-ring atoms within bonding distance, so generated structures render as connected molecules in the 3-D viewer, while still round-tripping correctly throughSymmetry.generate_idealized_structure's geometry has been redesigned per family so thatcalculate_bond_pairsproduces realistic bonding patterns modelled on real molecules of the same point group, rather than over-connected uniform rings (previously every ring atom could end up bonded to ~4 neighbours, including spurious cross-ring bonds between otherwise unrelated rings):- Cnv: the ring is now sized so its atoms stay outside bonding distance of each other (like ammonia's H...H, which doesn't bond), while the apex atom remains within bonding distance of every ring atom. Each ring atom bonds only to the apex (degree 1); the apex bonds to all n ring atoms (degree n).
- Cn / Cnh: the previous offset second ring (which could bond to multiple ring1 atoms, or to both an above and below decoration ring) is replaced by a single terminal-substituent atom per ring1 atom (like benzene's ring + H), giving each main-ring atom degree 3 (2 ring neighbours + 1 terminal atom) and each terminal atom degree 1.
- Dn / Dnh / Dnd: now built around a central "hub" atom (a larger-radius placeholder element, e.g. Fe), mirroring ferrocene's metal-sandwich structure (
ferrocene-eclipsed.xyz/ferrocene-staggered.xyz). The two rings are separated far enough that they no longer bond directly to each other; each is connected to the structure only via the hub. Each ring atom now has degree 3 (2 ring neighbours + hub) instead of 4. - Sn: also gains a central hub atom connecting the two halves of the antiprism (no more direct cross-ring bonds), with the existing Sn-symmetry-breaking "marker" atoms repositioned to bond as terminal substituents.
- All seven families, n=3..10 (Sn even n=4..10), were verified to produce a single connected component, no near-zero-length bonds, and correct round-trip detection through
Symmetry. generate_idealized_structure'sCnfamily placed its terminal substituent atoms too close to their parent ring atom (bond length ~0.47 A, less than the sum of the two atoms' covalent radii), so the bond cylinder was completely hidden inside the overlapping atom spheres in the 3-D viewer. Each terminal atom is now placed at a fixed offset (~1 A) from its parent ring atom in that atom's local radial/tangential/z frame, giving a bond length comparable to the other families (and to a real C-H/N-H bond) regardless ofn.generate_idealized_structure's placeholder elements are now chosen to look like more plausible molecules: a new_element_for_degreehelper maps each atom's designed bonding degree to an element with a roughly matching valence (H for degree 1, O for degree 2, N for degree 3, C for degree 4, S for degree 5-6, and a metal-like "Fe" hub for degree >= 7, consistent with the existing ferrocene-hub motif). At the defaultelement="F", the primary ring element for each family is substituted accordingly via_ring_element(e.g. "N" for the degree-3 ring atoms in Cn/Cnh/Dn/Dnh/Dnd, "C" for the degree-4 ring atoms in Sn, "H" for the now degree-1 ring atoms in Cnv, with the Cnv apex element chosen by its degree n). Since F, N, O, and C all share the same covalent radius (0.4 A), this is purely a cosmetic relabelling for those cases and does not change any bonding distances.generate_idealized_structure'sCnvring radius (previously sized purely from the ring-ring unbonding distance with a fixed margin) could, forn >= 12, end up larger than the apex-ring bonding cutoff itself, leaving the apex bonded to no ring atoms at all (an isolated apex floating in the centre of an unbonded ring, with zero visible bonds in the 3-D viewer). The ring radius is now sized as a fixed fraction of the apex-ring bonding cutoff instead, so the apex always bonds to every ring atom regardless ofn; forn <= 12this still leaves ring atoms unbonded to each other (ammonia-like, degree 1), while forn >= 13neighbouring ring atoms also end up bonded (degree 3: 2 ring neighbours + apex) -- both patterns are connected and render with visible bonds.generate_idealized_structure'sCnterminal-substituent offset's tangential component (0.4 A) could, forn >= 12, place the substituent within bonding distance of the neighbouring ring1 atom as well as its own (since ring1's nearest-neighbour spacing shrinks towards ~1.35 A asngrows), giving ring1 atoms degree 4 and substituents degree 2 instead of the intended benzene-like degree 3 / degree 1. The tangential component is now 0.2 A, which keeps the substituent bonded only to its own ring1 atom for allnup to 20.
Removed
- The vendored C++ reference implementation (
reference/) has been removed from the repository. The original project remains available at https://gitlab.com/lkkmpn/schoenflies.
Documentation
src/symmetry.py(Symmetry._MAX_AXIS_ORDER) and the README "Known limitations" section now explain why the n=20 adaptive-search cap can't simply be raised further: the per-degree validation tolerance shrinks roughly as 1/n^2 and approaches typical.xyzcoordinate precision beyond n~20, detecting Cn still requires an actual n-fold ring of equivalent atoms in the molecule, and the ring-grouping search is O(atoms^2) per candidate axis.
[0.1.2] - 2026-06-03
Added
- PyPI long description:
README.mdrewritten for the project page βpip install pyrrhotiteinstall instructions, quick-start example, full CLI flag reference, and example output
[0.1.1] - 2026-06-03
Added
src/point_groups/character_table_generator.pyβ automatic character table generator for all seven axial point group families (Cn, Cnh, Cnv, Sn, Dn, Dnh, Dnd) for arbitrary order n β₯ 2, implementing the analytical formulas from Johansson & Veryazov (2017). Exposesgenerate_point_group(label)andget_or_generate_point_group(label). (Moved tosrc/character_tables/generator.pyin 0.2.0.)Symmetry._generate_point_group_from_opsfallback in_find_point_group: when no hardcoded group matches the detected operations (e.g. n > 10), the family and order are inferred and the table is generated on-the-fly.tests/test_character_table_generator.pyβ 120 tests: consistency against all hardcoded axial tables, off-diagonal row orthogonality for n > 10, structural sanity checks, and spot-checks of known analytical values.
Fixed
testversion.pywas readingpyrrhotite/_version.pyinstead ofsrc/_version.py, causing the CI version-check job to fail (FileNotFoundError)testversion.pywas also attempting to openmeta.yaml, which does not exist in the repository; themeta.yamlcheck has been removed
[0.1.0] - 2026-05-11
Added
- Full Python translation of the Schoenflies point group determination algorithm from the C++ reference implementation (originally vendored under
reference/src/; removed from the repository in 0.2.0 β see https://gitlab.com/lkkmpn/schoenflies for the original) src/periodic_table.pyβ hardcoded atomic data (symbol, mass, covalent radius, colour) for all 118 elements, translated from the C++ reference'speriodic_table/periodic_table.cppsrc/rotor_class.pyβRotorClassenum (AsymmetricTop, OblateSymmetricTop, ProlateSymmetricTop, Linear, SphericalTop)src/structure.pyβStructureclass: XYZ file loading, centre-of-mass centering, closest-atom lookup, bond-pair detectionsrc/operations/operation_label.pyβOperationLabelwith innerElement,Plane, andPrimeenums; factory classmethods mirroring C++ overloaded constructorssrc/operations/operation_label_count.pyβOperationLabelCountpairing a label with a multiplicity countsrc/operations/operation_group.pyβOperationGroupgrouping operation IDs under a shared labelsrc/operations/operation.pyβOperationclass: Rodrigues rotation matrices, Householder reflection matrices, inversion, improper rotation;do_operationatom-mapping with normalised error metricsrc/operations/operation_manager.pyβOperationManager: validates, deduplicates, and stores found operations; generates the final labelled point-group operation setsrc/point_groups/irrep_label.pyβIrrepLabelwithMulliken,Parity, andPrimeenums for Mulliken notationsrc/point_groups/point_group_label.pyβPointGroupLabelwithClassenum covering all 18 point-group families including Cβv and Dβhsrc/point_groups/point_group.pyβPointGroupclass withcompare_to_symmetry_operationsused by the matching algorithmsrc/point_groups/point_groups.pyβ hardcoded definitions for all 54+ point groups with operation counts, irreducible representations, and character tablessrc/symmetry.pyβSymmetryclass: full 7-step pipeline (principal axes via inertia tensor, rotor classification, symmetry-operation search, point-group matching, Cartesian axis assignment, operation labelling, point-group operation generation)tests/files/β 32 XYZ test molecules copied fromreference/test/files/covering all major point-group familiestests/conftest.pyβ pytest fixtures and expected point-group label mapping for all 32 moleculestests/test_structure.pyβ unit tests for XYZ loading, COM centering,find_closest_index, andcalculate_bond_pairs