Core Crates

A selective deep-dive: of the 21 crates under core/, this section covers the seven a contributor touches most (backbone, query, selection-engine, render, edit, forcefield, periodic), plus short notes on symmetry and diagram, which are large and the least obvious from their names, and on plots. Repository Layout lists them all in one line each, and Services Layer covers core/services.

Size is a poor guide to which of these you will need, and the selection above does not track it: core/periodic is 900 lines and has a full section because its output is on screen in four frontends, while core/symmetry (19,000 lines), core/diagram (15,800) and core/ribbon (9,700) are each mostly self-contained behind a small entry point.

5.1 Backbone (core/backbone/src/lib.rs)

  • Defines newtype identifiers (AtomId, BondId, FrameId, PropertyId) and element metadata (periodic module) used across the workspace.
  • SceneBuilder constructs snapshots with atoms, bonds, metadata, editable MolecularElectronicState, and derived data (electronic structure, vibrational modes, thermochemistry). The molecular state belongs to the current structure; parsed ElectronicStructure remains evidence from the source calculation. SceneGraph wraps the snapshot with provenance hashing (SceneDigest) for change detection.
  • PropertyRegistry stores globally registered per-atom or per-scene properties, enabling dynamic queries and render overlays.
  • Exports helpers such as infer_covalent_bonds, AtomPopulation, FrequencyData, Trajectory, and UnitCell. Loader and renderer crates consume these types.

5.2 Query & Selection (core/query, core/selection-engine)

  • core/query provides the selection expression enum, parser, and evaluator. Supported primitives include element filters (element O, bare symbols, atomic_number == Z), distance shells, angle windows, nearest-neighbour queries, property comparisons, and residue terms (chain A, resname HIS, resid 42), and retained alternate locations (altloc B).
  • Residue terms read the same accessors as the grouping. There is no residue entity in the scene graph — a residue is a view over per-atom PDB/mmCIF annotation — and orbitron_backbone::residues is the one definition of (chain, seq, insertion code). Both the selection evaluator and any panel that reports a residue go through ResidueProperties, so they cannot come to disagree about what HIS 42 means. The insertion code is part of the identity: an antibody numbered 100, 100A, 100B is three residues, and dropping it merges them. The Scene panel’s Coordination readout is the other consumer: it names a metal’s site (Zn C1) and its donors (via HIS A1 NE, …) through the same accessors, and caches the formatted strings on the scene digest — resolving a donor id costs a lookup, and the panel redraws sixty times a second for text that only changes when the structure does.
  • The residue view also retains mmCIF label-chain and label-sequence identifiers, label-component identifiers, named alternate locations, and source atom IDs. _chem_comp definitions supply controlled component type, name, formula, mass, and standard parent identity. ResidueKind and Residue::one_letter_code supply the compact protein/nucleic-acid labels used by the desktop navigator. Modified peptide and nucleic-acid monomers use their source type and parent, while unknown, peptide-like, and non-polymer components remain Other rather than being guessed into a polymer sequence. Parsers store coordinate-independent DeclaredResidue rows under pdb:polymer_sequence. residue_sequence joins them to observed residues by mmCIF label identity first and author identity second, then emits atom-free unresolved entries for unmatched source rows. This also keeps non-standard declared monomers in their polymer chain. The navigator caches the joined model by scene digest and emits SelectionPanelAction::ApplyAtomSet, so it shares the canonical viewport selection. Per-digest egui memory retains the residue query and focused identity. The latest still-selected atom in selection_history supplies a one-shot scroll target; recording the followed atom prevents the panel from snapping back on every frame. A second digest-keyed cache joins pdb:atom_name values to residue ids once per scene for live filtering. Search terms are ANDed; qualified author sites are exact, while plain residue and atom terms support partial matching.
  • ResidueProperties also owns alternate-location access. Bond inference uses it to reject contacts between different named locations in the same residue, while blank-location atoms remain compatible with every conformer.
  • alternate_location_choices and preview_alternate_location_edit in orbitron-edit keep alternate-location mutation residue-local. Keep-only removes other named conformers and clears the retained pdb:alt_loc values; delete removes only the selected conformer. Both preserve shared blank atoms, report incident and cross-residue bonds, reject stale previews, and restore deleted atoms, bonds, and property values exactly on Undo.
  • water_ion_cleanup_selection groups candidates through the shared residue model and uses the query crate’s whole-residue, minimum-image contact search for Within/Beyond filters. Classification is conservative: named water must have water-site composition, while an ion must be monatomic and its component identity must agree with its metal or halide element. The preview pins exact residue, atom, charge, and bond removal before one undoable command.
  • Shared structure analyses live beside the selection evaluator in core/query: residue contacts, ligand and chain interfaces, explicit-H hydrogen bonds, named salt bridges, disulfides, and steric clashes. steric_clashes_excluding returns deterministic worst-first pair records with distance, the limit at 80% of the summed van der Waals radii, and positive overlap. It excludes bonded and 1-3 pairs, applies caller-supplied alternate-location exclusions, and uses Cartesian or periodic candidate bins before the exact distance test. The residue-mutation preview consumes these records rather than carrying a second clash implementation.
  • ramachandran_residues_excluding builds complete protein triplets from the same residue model, takes phi and psi from the shared math crate’s dihedral, which is the IUPAC sign PDB and CCTBX use, and unwraps periodic coordinates one backbone leg at a time. It bilinearly interpolates six Richardson Lab Top8000 grids bundled under core/query/data/ramachandran/. The data directory pins the upstream commit, CC BY 4.0 license, binary layout, and regeneration command. Callers supply hidden alternate-location atoms; the result records residue identity, visible residue atoms, the five defining backbone atoms, reference class, angles, score, and favored/allowed/outlier category.
  • missing_side_chain_residues_excluding compares each retained amino-acid conformer with the expected heavy side-chain atom names from Orbitron’s standard residue templates. A retained CA atom distinguishes a coordinate-bearing residue from an unresolved entry or an isolated named donor. The result records residue and conformer identity, visible atom IDs, and missing atom names. It ignores backbone atoms and hydrogens, maps common protonation variants to their parent template, supports MSE and SEC, and omits residues without a known complete template. Caller-supplied exclusions keep the result aligned with the active alternate-location view.
  • rotamer_residues_excluding measures CCTBX-defined chi angles for complete rotamer-bearing amino-acid side chains and unwraps each path in periodic structures. It interpolates 17 sparse Richardson Lab Top8000 grids bundled under core/query/data/rotamer/; the data directory pins the upstream commit, CC BY 4.0 license, binary layout, and regeneration command. The result records residue and conformer identity, visible residue atoms, every chi-defining atom quartet, angles, score, and favored/allowed/outlier category. Callers supply alternate-location exclusions through the same interface used by the other structure analyses.
  • Primitives combine through the And / Or / Not AST variants, plus WithinSelection (the within <R> of <selection> operator, evaluated as the set of atoms within the radius of any anchor matched by the sub-selection). parse_expression (core/query/src/query/parsing.rs) is a two-layer parser: a lexer keeps function-call parens and quoted property names inside one primitive token, and a recursive-descent layer handles the boolean operators, the within … of prefix, and grouping (not > and > or). Element symbols resolve to AtomicNumber via orbitron_backbone::atomic_number_for_symbol, so evaluation and formatting reuse the existing atomic-number path. PropertyCompare carries a CompareOp (>, >=, <, <=, ==, !=).
  • parse_expression returns SelectionExpr ASTs; the UI’s command palette and CLI subcommands rely on it to turn input strings into programs.
  • evaluate_selection resolves expressions against a SceneGraph. Errors are surfaced via QueryError (UnknownProperty, NonNumericProperty, Evaluation).
  • core/selection-engine::SelectionProgram wraps expressions with digest-aware caching and optional scoring metadata (SelectionScore). This crate is responsible for keeping UI selection interactions performant.

5.3 Rendering Core (core/render)

  • Renderer::new initialises a WGPU surface for a winit::window::Window. Headless rendering (headless module) enables CLI exports without a window.
  • RenderSceneData aggregates instanced atoms, bonds, and labels ready for GPU upload. RenderSceneData::pick_atom (core/render/src/data_builder/types.rs) is the ray/sphere hit-test the viewer picks atoms with. It skips instances an overlay invented rather than the scene contains: periodic_image_id mints the id a periodic ghost carries and AtomInstance::is_periodic_image recognises one, so a click on an image cannot return an id that names no atom. AtomPicker’s spatial index leaves them out at build time, which is the same rule by the cheaper route; the two paths must agree, and spatial_picker_matches_linear_picker_across_a_large_scene is what holds them to it.
  • Lighting presets live in lighting.rs; set_colorblind_safe (in core/render/src/lib.rs) toggles the Okabe–Ito palette globally.
  • Bonds are rendered as instanced quads with per-instance thickness, dash, and glossiness. BondInstance::order plus orbitron_viewer_core::apply_theme_styles (defined in core/appearance/src/render_styles.rs and re-exported from the viewer-core crate root; ui/shell/src/util/render_styles.rs is a further re-export shim) expand double/triple/quad bonds into multiple lanes using theme-provided spacing and highlight scaling.
  • Mesh pipelines (mesh::MeshRenderPipeline) handle GPU buffer management. core/render is the only crate that declares a wgpu dependency, so the rest of the workspace stays backend-agnostic; the desktop shell is the one exception, reaching wgpu types through egui-wgpu to build the device stack for its detached export window (ui/shell/src/export/host.rs).

5.3.1 Embedding Orbitron Renderer (EmbeddedRenderer)

You can host Orbitron’s GPU renderer inside your own application window without using the egui UI shell. This is useful for Rust hosts (Tauri, winit, native desktop apps) that want to reuse Orbitron’s rendering pipeline.

Prerequisites

  • Rust toolchain from rust-toolchain.toml (or newer).
  • Access to a SceneGraph (load via orbitron-io-pipelines::load_scene, or via OrbitronServices, or construct using SceneBuilder).

Minimal integration

orbitron_render::EmbeddedRenderer wraps the renderer with a synchronous API. For a full runnable reference see examples/web-embedding/ (WASM viewer).

The core steps are:

  1. Create an EmbeddedRenderer from your window. The constructors borrow the window (EmbeddedRenderer::new(&window) / new_async(&window) / with_mode_async(&window, mode)), so the window must outlive the renderer.
  2. Call prepare_scene whenever the scene or highlight selection changes.
  3. Call render_prepared in your redraw loop with your Camera.

Under winit 0.30 the event loop is driven by ApplicationHandler + run_app (there is no EventLoop::run closure, WindowBuilder, or Event::RedrawRequested anymore), and a window created in resumed() has to outlive a borrowed renderer stored beside it. The maintained reference for that pattern is the desktop shell (ui/shell/src/runner.rs), which owns the window as an Arc<Window> and builds the lower-level renderer via Renderer::new_owned for a 'static surface.

5.4 Editing Engine (core/edit)

  • Provides the edit-mode foundation: EditableScene, EditCommand implementations, ID helpers, and the geometry engine behind the molecular builder.
  • command/ holds the high-level actions, grouped by target (atom_commands.rs, bond_commands.rs, fragment_commands.rs, geometry_commands.rs, hydrogen_commands.rs, electronic_state_commands.rs, residue_commands.rs, residue_state_commands.rs, residue_state_definitions.rs, disulfide_commands.rs, disulfide_annotations.rs, terminal_cap_commands.rs, conformer_commands.rs, perception_commands.rs, wrap_commands.rs, cluster_commands.rs, alternate_location_commands.rs, solvent_cleanup_commands.rs, cell_commands/), with the EditCommand trait itself in command/edit_command.rs.
  • ops.rs executes mutations against editable scenes, ensuring invariants such as bond symmetry and selection history updates.
  • The viewer’s Edit Mode panels (ui/shell/src/edit) invoke these commands via run_edit_command_with_feedback, emitting undo/redo stacks and error toasts.

geometry/ — the builder. The pattern throughout is a pure planner plus a thin command: the planner takes coordinates and returns what should change, so it is testable without a scene, and the command applies the plan and reports it. plan_clean_geometry / CleanGeometryCommand is the reference example.

Module Role
build_template.rs The nine BuildGeometry shapes, their substituent directions, AttachmentSites (non-empty only for the two five-coordinate shapes), and the staggering twist used when growing onto an existing atom
ring.rs carbocycle(size) for MIN_RING..=MAX_RING (3–8): polygon, pucker, cap, then idealize_in_place, so a placed ring and a cleaned one agree by construction
clean.rs plan_clean_geometry — angles, then coordination spheres, then the point-group fit, each gated by CleanGeometryOptions
idealize.rs The Gauss–Seidel distance-constraint solver seeded at current coordinates. ANGLE_STIFFNESS = 0.1 makes a bond about ten times stiffer than an angle, which is why PASSES is 2000
cluster.rs plan_cluster_model — cutting a metal site out as a cluster model for a QM calculation. One rule does both cuts: grow outward from each donor and stop at TRUNCATION_STOP_ATOMS, so a histidine keeps its imidazole plus CB and a backbone carbonyl keeps the peptide unit, with no table of twenty residues. Takes a distance shell as well as the bond list, because a metal–ligand contact is longer than covalent radii reach and a catalytic water would otherwise be dropped
sphere.rs, sphere_assessment.rs, shape_measure.rs Continuous shape measures against reference polyhedra, and the SphereVerdict the Scene panel reports
coordination_template.rs Coordination geometry and Shannon-radius reach per element and oxidation state
embed.rs, graph.rs, stereo.rs 3D coordinates from a flat connection table, and handedness preservation / inverted-stereocentre repair
electron_count.rs, ligand_field.rs, site_trust.rs, non_innocent.rs, agostic.rs Metal-site electron counts, the ligand-field picture and the trust grade on an oxidation state; see §5.10
conformer.rs Deterministic bounded torsion sampling, UFF relaxation/ranking, stereochemistry checks, and heavy-atom aligned-RMSD deduplication
attach/, donors.rs, ops.rs, math.rs Ligand attachment, donor suggestion, and shared helpers
Two backbone helpers that behave the opposite of how they read

Both live in core/backbone rather than here, and both cost real time to rediscover from the builder side.

missing_hydrogens returns zero for an over-valent atom. It searches for the smallest valence at or above what the atom is already using, and for an over-valent atom there is none — so “too many hydrogens” and “exactly right” come back identically. Reading that as “nothing to do” is what let a promoted bond leave its hydrogens in place. Use hydrogen_balance (core/backbone/src/scene/utils/hydrogens.rs), which is the same computation signed: positive means hydrogens are missing, negative means that many too many. missing_hydrogens is literally that clamped at zero.

Bond-order perception on a drawn structure needs GeometryEvidence::Ignore. Perception treats interatomic distance as evidence and refuses to promote a bond longer than 0.98 of the covalent-radii sum, which is right for coordinates that came from a calculation or a database: there, a 1.33 Å C–C is the evidence that it is a double bond. A builder places every bond at the single-bond length whatever the bond means, so the ratio sits at 1.0 and nothing is ever promoted — measured, the same six-atom ethene comes back all-single at 1.52 Å and correctly doubled at 1.33 Å. Pass GeometryEvidence::Ignore (core/backbone/src/scene/bond_perception/mod.rs) to assign from valence alone. Note also that a failed assignment leaves the partial result in place; there is no rollback, and that is the design.

smiles.rs sits beside geometry/. It converts Yowl’s parsed graph into an explicit-H SceneGraph or SmilesFragment, assigns molecular charge, and invokes the same 3D embedding path when requested. Unsupported stereochemistry, query syntax, maps, and metals are boundary errors. command/residue_commands.rs consumes the pinned CCD-derived templates in core/edit/data/standard-amino-acids.json; backbone identity and coordinates remain stable while side-chain atoms are replaced. command/residue_rotamers.rs reads the CC BY 4.0 Top8000 derivative under core/edit/data/rotamer/. It selects a smoothed 20° phi/psi bin, or a declared backbone-independent fallback when phi/psi cannot be measured, and supplies the named chi-angle candidates used by residue_preview.rs. The preview keeps the existing exact candidate application and undo transaction. Clash-free rows follow local library probability; clashing rows follow summed squared overlap and then probability. command/side_chain_repair.rs wraps that mutation path for a same-component repair. It requires a positive missing-side-chain finding, refuses alternate locations, ambiguous protonation aliases, active SSBOND endpoints, and any side-chain bond crossing the residue boundary, then ranks complete candidates by clash-free status and minimum-image RMS displacement from existing heavy side-chain atoms. The detached validation reports missing atoms, source atoms replaced, explicit hydrogens removed, formal charge, and clash scores before and after. RepairSideChainCommand retains the preview’s materialized source digest and delegates exact candidate application and undo to the existing mutation transaction. command/terminal_cap_commands.rs builds undoable heavy-atom ACE/NME caps from the shared protein_termini query. Its preview digest uses the materialized EditableScene representation because entering edit mode turns lazy inferred bonds into explicit session topology; comparing against the pre-session graph would reject an unchanged PDB preview as stale. command/residue_state_definitions.rs defines the supported ASP/ASH, GLU/GLH, CYS/CYM, and LYS/LYN hydrogen, charge, and bond-order patterns. The planner in residue_state_commands.rs materializes inferred bonds before recording its source digest and returns a detached scene plus residue and molecular charge changes. SetResidueProtonationCommand stores only the affected residue atoms and incident bonds for exact Apply, Undo, and Redo. Retained heavy-atom IDs and coordinates do not change; generated hydrogens use the shared approximate hydrogen placement code and stripped experimental/computed properties. command/disulfide_commands.rs plans explicit bridge creation and cleavage from two annotated CYS/CYX residues. Creation is bounded to a 1.7–2.5 Å minimum-image SG-SG distance and never moves heavy atoms. Cleavage creates two approximate thiol hydrogens. The command stores residue-local atoms, incident bonds, and the active SSBOND metadata string for exact Apply, Undo, and Redo. disulfide_annotations.rs is the one writer for pdb:ssbond_pairs; residue mutation uses the same helper when it removes a cysteine endpoint. Edited bond properties identify the live connection as disulf, while imported raw pdb:struct_conn data remains unchanged source provenance.

5.5 Force Field (core/forcefield)

A self-contained UFF implementation, split out of core/edit so the geometry planner and the minimiser stay independently testable. It is what Relax Geometry runs.

  • params.rs holds the UFF parameter table; typing.rs assigns atom types from geometry.
  • All five terms are present, each in its own module: bond.rs (stretch), angle.rs (bend, including the periodic form metals need), torsion.rs (dispatching on hybridisation across all six cases), inversion.rs, and vdw.rs.
  • topology.rs builds molecular or explicit-image periodic interaction graphs; system.rs evaluates them; minimise.rs runs the minimisation. Every analytic gradient is checked against a numerical one in the crate’s tests.

Van der Waals has two entry points, and the difference matters. vdw_energy is the bare Lennard-Jones with no cutoff. vdw_energy_switched tapers it to zero between VDW_TAPER_START (10 Å) and VDW_CUTOFF (12 Å) using the quintic S = 1 − 6x⁵ + 15x⁴ − 10x³, which has zero first and second derivative at both ends — a C² energy and a C¹ gradient, which is what a line search needs. A bare truncation steps the potential and a shifted-force form distorts it everywhere; both were rejected for those reasons. Below 10 Å the two functions agree exactly.

The 12 Å radius comes from the table rather than convention: UFF’s deepest pair is Fr–Fr at x1 = 4.9 Å, retaining 2.75% of its well depth at 10 Å and 0.92% at 12 Å, with ordinary organics at 0.37% and 0.12%. A test pins both figures.

The non-bonded pair list is bounded by distance as well as by connectivity. It used to hold every pair more than two bonds apart, which was quadratic and eagerly allocated — 800 MB at ten thousand atoms. A uniform grid now bounds it by VDW_CUTOFF + VDW_SKIN, so the count follows density. The topological exclusion is unchanged and still comes first: a 1-3 pair stays excluded however far its angle opens.

Fixed-cell periodic minimisation uses explicit lattice images in all five terms. Each interaction participant is an atom index plus an integer (a, b, c) image. Directed bond images compose into angles, torsions and inversions; gradients from every occurrence return to the one base-atom coordinate. This represents self-image bonds and several bonded images of one atom pair, which a minimum-image shortcut cannot.

Nonbonded terms enumerate one half-space of image shells to the 12 Å cutoff plus skin. Their 1-2 and 1-3 exclusions are a depth-two walk over (atom, accumulated image) states, so only the bonded image is excluded. A Cartesian grid bounds candidate work by density. Slab axes marked non-periodic are neither folded nor replicated, and singular cells fail explicitly.

System::new remains the molecular API; System::new_periodic is the fallible fixed-cell entry point. BondRecord is unchanged because it cannot distinguish two images of the same atom pair. Cross-face contacts are inferred internally and have single order. Preserving an aromatic or multiple bond through a face would require parser-supplied image-bearing topology. The implementation contract and validation evidence are in notes/archive/periodic-minimisation-plan.md.

Relax entrenches the geometry it is given — it finds the nearest local minimum. Clean (core/edit/src/geometry/clean.rs) moves a structure onto what its connectivity implies instead. Keeping those two distinct is the load-bearing design decision in the builder; see notes/archive/PROJECT_PLAN-edit-builder.md for the reasoning.

5.5.1 Coordination geometry: where it lives and what it cites

The metal-complex machinery is spread over three crates by concern, and the reference data each part rests on is worth knowing before changing any of it.

File Holds
core/backbone/src/ionic_radii.rs Shannon effective ionic radii by charge and coordination number, CSD-derived radii (DERIVED_RADII) at the low coordination numbers and the oxidation states Shannon lacks, the measured donor radii (O and S from GFN2-xTB complexes, N and the halides from the CSD’s terminal contacts), tabulated_metal_donor_distance combining them (with mercury(II)’s halide rule), and ACTINYL_EQUATORIAL_OFFSET
core/backbone/src/metal_oxo.rs Terminal M=O bonds: each element’s (terminal_oxo), each oxo core’s (oxo_core_bond) and the length for a given count of oxo groups (metal_oxo_bond); which oxygen is an oxo group (reads_as_oxo, OXO_WINDOW) and which ion an actinyl (is_actinyl). The coordination templates, analyze metal-sites, Relax and Clean all read these
core/edit/src/geometry/coordination_template.rs Element and oxidation state → preferred coordination number, reference shape, ceiling, and any oxo core; slot positions and expansion
core/edit/src/geometry/shape_measure.rs The reference polyhedra and the continuous shape measure against them
core/edit/src/geometry/sphere_assessment.rs Verdicts (clean, drifted, no clear match) and snapped_sphere_positions
core/edit/src/geometry/attach/ build_complex: slot assignment, ligand placement, aqua fill, the two-pass relaxation and the refusals, each a BuildRefusal variant with a stable code() and depends_on_the_donors() for a caller trying several donor sets. build_polynuclear is the same build over several metal sites: each donor names its site, a later site’s template is fitted onto the donors a ligand placed with an earlier one brings to it, and one relaxation settles every site; a donor shared by two sites (a bridge) and a site no ligand reaches are refused. Waters are placed after every site, and a slot whose water would sit within reach of another metal or on an atom another site holds is left empty and counted (Complex::omitted_aqua). build_complex is its one-site case
core/forcefield/src/topology.rs The metal–donor bond override and the Kepert donor–donor repulsion
core/forcefield/src/uff4mof.rs The UFF4MOF atom-type extension; only copper’s square-planar Cu4+2 is assigned

References, and what each is actually used for.

  • Shannon, effective ionic radii — the reach of every metal–donor bond. Indexed by charge and coordination number, which is why the coordination number has to be settled before a distance can be looked up. Validated within 0.033 Å of experimental aqua-ion distances over fourteen ions.
  • Casanova, Llunell, Alemany and Alvarez, Chem. Eur. J. 2005, 11, 1479 — Table 2 tabulates the continuous shape measure of every ideal eight-vertex polyhedron against every other. Those numbers depend only on the geometries, so they check our reference coordinates against the literature rather than against ourselves. It earned its place immediately: every pair involving the square antiprism came out about 1.7 high, identifying the antiprism’s coordinates rather than the algorithm as wrong. The table quotes the measure with the metal as a vertex (ML_n), which for a capped shape is not the donors’ number: read donors-only, its Johnson bicapped trigonal prism looked irreproducible until 2026-10-14. The spherical bicapped prism Orbitron uses is anchored through the Johnson one, 1.439 apart in the paper, and the muffin and capped cube through the nine-vertex table of Chem. Eur. J. 2008, 14, 1291.
  • Rappé, Casewit, Colwell, Goddard and Skiff, J. Am. Chem. Soc. 1992, 114, 10024 — UFF, the whole of core/forcefield. Its parameter table was transcribed from RDKit’s Params.cpp and cross-checked against a second independent source.
  • Addicoat, Vankova, Akter and Heine, J. Chem. Theory Comput. 2014, 10, 880, and Coupry, Addicoat and Heine, ibid. 2016, 12, 5215 — UFF4MOF and UFF4MOF-II. The 2016 extension supplies eight-coordinate types for every lanthanide and for uranium, which UFF has for no element. Carried, and apart from copper’s square-planar type not assigned; see below.
  • libmsym (Marcus Johansson, MIT) — the point-group detection ported into core/symmetry and used by Symmetrize.
  • The Kepert model — donors repelling each other on a sphere. UFF has almost no angular preference at a metal, so for sterically controlled centres the angle terms are dropped and this stands in their place.

Validation corpora, both outside this repository and neither derived from Orbitron: tmQM (108,541 CSD-derived complexes at TPSSh) and the reackt staging set. The harnesses that use them are in Testing §11.9.

UFF4MOF is present and, apart from copper, deliberately unused

uff4mof_for is public and assign_type never calls it. Assigning the square-antiprismatic types to centres above six donors is the obvious thing to do and was measured making relaxed geometries slightly worse — over 1,387 high-coordinate crystal structures, metal–donor drift 0.144 Å against 0.134 and sphere angles 0.203 rad against 0.197, the same direction in every statistic.

Why is not established, and the likeliest reason is not that the parameters are wrong: they were fitted under a bond-order convention this code does not reproduce (both papers fit every metal–ligand bond at order ½), and UFF’s natural bond length multiplies the radius by a bond-order correction. Settling that means reproducing the convention, not retuning anything.

The exception is copper. Rappé’s table has one copper type, tetrahedral Cu3+1, and Cu(II) is square planar or octahedral, so assign_type gives a copper in either of those spheres the 2014 paper’s Cu4+2, whose radius is Rappé’s to 0.02 Å and whose angle is 90°. Relaxing 20,264 four- to six-coordinate tmQM spheres from their crystal geometry, the 28 four-coordinate coppers it retypes drift 0.072 rad in their angles rather than 0.531; the 59 five- and six-coordinate ones are level. The same change stops a square-planar iron taking tetrahedral Fe3+2: an element with no type for a square plane or an octahedron takes its type for the other, since UFF gives both one angular form.

The 2014 table also misprints three effective charges (Cu4+2, Co3+2/Co4+2, Sc6+3); every row takes its element’s own, as the module doc records.

Only the bond radius and angle are transcribed per type; the seven nonbonded and torsional columns are read off the element’s existing UFF entry, because both papers state that atom types of the same element share them. Do not transcribe them a second time — and do not take the table from a PDF extractor, which dropped Ag2f2 outright and shifted two radii up a row.

5.6 Reciprocal-Space Analysis (core/periodic)

orbitron-periodic converts a canonical BandStructure plus its calculation cell into one ReciprocalScene shared by every presentation surface.

  • orbitron-symmetry::first_brillouin_zone constructs the exact Wigner-Seitz cell of the physical reciprocal lattice, including the 2π factor.
  • ReciprocalScene::from_band folds every parsed sample into that zone while retaining its source index. A mesh remains independent points.
  • Declared paths are split at zone boundaries, so a renderer cannot draw a false chord across opposite faces.
  • projection.rs supplies the common orthographic projection used by the desktop painter and SVG/PNG exporters.

The crate owns geometry and selection state, not widgets. Desktop controls live in ui/shell/src/panels/analysis/bands_dos/brillouin.rs; the TUI summarizes the same scene; Python converts it to plain containers; the WASM custom element uses the same indexed zone and path records.

5.7 Symmetry (core/symmetry)

About 20,000 lines, and the crate whose name says the least about what is in it. Two unrelated jobs share it because they share the linear algebra: molecular point groups and crystallographic lattices.

The molecular half is a Rust port of libmsym by Marcus Johansson (MIT). The data structures and public API were redesigned for idiomatic Rust; the mathematics follows libmsym closely. detect_point_group(atoms, tolerance) returns a Schoenflies label, scan_candidate_groups reports what else was within reach at other tolerances — which is what the desktop offers when a structure is nearly symmetric — and symmetrize snaps coordinates onto a detected group. masses.rs supplies the weighting; candidates.rs, classification.rs, detection.rs and operations.rs are the private machinery.

The crystallographic half is what the periodic stack calls:

Module What it answers
niggli.rs The reduced cell for a lattice given in any setting
bravais.rs Which of the 14 Bravais lattices, plus the primitive→conventional transform
primitive.rs The primitive cell of a supercell, in fractional or Cartesian input
crystal.rs Crystal system from a metric, from lengths and angles, or from an ITA number
spacegroup/ Operations recovered from a symbol or ITA number. The 230 groups have 530 settings between them, and a Hermann-Mauguin symbol does not always determine one — so resolve_setting reports a SettingAmbiguity rather than choosing, because the wrong setting relocates atoms without failing. The table is generated from spglib.
brillouin.rs first_brillouin_zone: the exact Wigner-Seitz cell of the physical reciprocal lattice, 2π included. core/periodic folds every band sample into it
kpoint_labels.rs High-symmetry corner names
slab.rs Three lattice vectors re-orienting a cell for an arbitrary (hkl) cleave, behind the editor’s slab command
symop.rs Parsing and expanding x,y,z-style operator strings from a CIF

5.8 2D Diagrams (core/diagram)

Turns a SceneGraph into a DiagramSceneData — 2D atom and bond positions with stereo annotations — for the structure diagram panel and the SVG/PNG export. The public surface is four items (DiagramSceneData, DiagramSettings, DiagramStyle, depiction_quality); nearly all of the 15,800 lines are the private layout/ tree behind them.

layout/ is a pipeline, and its subdirectories are the stages: graph and rings find the structure, embedding and placement produce first coordinates, relax and optimize improve them, aromatic and coordination apply the conventions a chemist expects (planarised rings, fanned substituents, coordination compounds drawn the way they are drawn), crossings and metrics measure what came out, and normalize orients and scales it.

Two things worth knowing before touching it:

  • depiction_quality exists because “is this fit to publish” is a question about the drawing, not the molecule. The optimiser’s internal collision_score is an unbounded sum of inverse squared distances — the right shape to descend, the wrong shape to compare two structures or report to a person. The crowding threshold (CROWDING_FRACTION_OF_BOND = 0.55) is calibrated against a 420-complex sample: monodentate coordination compounds, which chemists read without complaint, score 2.1%, while hexadentate chelates — the visibly unreadable ones — score 47.8%.
  • Layout results differ by CPU architecture. The same file gives 9.8% crowding on arm64 and 12.2% on x86_64. The drawing agrees; the count either side of a threshold does not. A test that asserts a crowding number will be flaky across the fleet, and reads as a float flake rather than as what it is.

5.9 Data Figures (core/plots)

orbitron-plots draws headless data figures for orbitron plot and the desktop’s figure export. It knows no chemistry: a caller builds a Figure from prepared series and asks for bytes. What goes on a figure is decided in core/services/src/plot (see Services Layer).

It is one drawing in two formats. The SVG is the drawing, and PNG is that SVG rasterised by resvg, so a label cannot sit in one place in the PNG and another in the SVG. Layout measures text in the embedded Inter, and resvg is given only that font, so a figure does not change with the machine that draws it. Sizes are physical (Length), labels take a small markup (E_F, cm^-1), and the same figure gives the same bytes.

5.10 Oxidation states, charges and electron counts (core/backbone, core/edit)

The identifiers, the oxidation states, the electron count and the ligand-field picture all need one division of a complex’s charge between ligands and metal, and a coordinate file states neither. The machinery below makes them share it. What the numbers mean and how accurate they are is in the user guide’s Metal-site accuracy; this section is where the code lives.

Oxidation state (core/backbone/src/oxidation_state.rs). OxidationStates::resolve returns (state, OxidationStateSource) for a metal. The sources are Given, Stated (a CIF’s Fe3+), FormalCharge, IonicModel, BondValence, Element (the one state zinc, cadmium and most lanthanides take), Coordination and None. The ionic model answers first wherever the file declares a total charge, because it is an electron count over the ligands, and a bond-valence sum replaces it only in the cases the private StateEvidence::computed spells out (a state of 0 against a consistent sum, a state the element is almost never named at). Three corrections follow, each from a measured benchmark rather than a rule of chemistry:

  • A bond-valence state on a metal with a carbon bond stands only if some reading of the ligands reaches it (IonicReading::reconciled); otherwise the ionic state is restored, unless beyond_the_group says the ionic state exceeds the metal’s group number, which no d-block metal reaches.
  • coordination_state replaces a state the donor count cannot carry (Au in two or three bonds is Au(I), Pd and Pt in four are +2, Cu(I) over N and O donors alone is Cu(II)).
  • StateEvidence::decide with ReadingCheck::NotRequired is the decision without that reading check; the reading’s own search for a state uses it, since the check asks that search’s result.

ionic_reading returns the IonicReading the identifier writers and the count share: atoms and bonds with charges written, each metal’s state, the reconciled metals whose ligands were read again to give the reported state, and ionic_states (what trust is graded against). It is None when the file declares no total charge, states its own bond orders, or the model cannot divide the complex, and the last scene’s reading is cached per thread. Any change that can move an identifier for the same input bumps IDENTIFIER_REVISION in core/backbone/src/lib.rs; core/backbone/IDENTIFIER_REVISIONS.md logs what each revision changed. bond_valence.rs holds the Brown-Altermatt, Brese-O’Keeffe, Liu-Thorp and Shields tables and, for the f block, Trzesowska et al.’s, Gagné-Hawthorne’s and Zachariasen’s, each pair traced to a primary paper (the accumulated compilation is licensed non-commercially and deliberately not vendored).

Ligand charges (core/backbone/src/scene/). charge_inference/ionic.rs has resolve_ionic_complex, which perceives the ligands with the metals detached, gives each its closed-shell charge and the metals the rest, and resolve_ionic_complex_toward, which re-reads the ligands with charges allocated (allocate_to_targets, bounded in donors and ligands) so a metal comes out at a state chosen elsewhere. The special cases that moved identifiers are pins and conventions, not general rules: fixed_oxidation_state metals, an η5 cyclopentadienyl as an anion, cyclooctatetraenide as a dianion on group 3 and f-block metals, and apply_alkylidene_convention (a heteroatom-free alkylidene is neutral on group 8 and later or beside a CO, a dianion on earlier metals without one). Hydrides stay neutral in the resolver, and the count and state add one each.

  • polyhedral_cage.rs charges boron cages by the skeletal-electron count: the smallest class that holds the electrons, closo n + 1 pairs, nido n + 2, arachno n + 3. A metal contact is a sigma bond only on a vertex with no exo partner; a vertex that keeps its hydrogen is a face contact. A closo cage of 9 to 12 vertices bound over a face is a dianion. Closo versus nido is never read from the geometry, except that a metal fragment worth two skeletal electrons by the arithmetic (not a nickel, palladium or platinum one, whose d10 ML2 fragment the electron-pair count is known to miss) charges the cage from the cluster where the cluster’s edge count shows the same class.
  • redox_ligand.rs reads a catecholate or amidophenoxide’s redox level from its own bond lengths (Brown’s metrical oxidation state).
  • huckel.rs is an extended Hueckel ligand charge guess. It is a ranking key in the ligand search, below odd-electron and unsound fragments, never an override.

Count and trust (core/edit/src/geometry/). count_electrons returns, per metal, an ElectronCount or a Decline; the per-centre work is count_one. The count is d or f electrons at the ionic state plus each site’s SiteShare, and it declines where the ionic model does not decide it: no declared charge, periodic, file bond orders, unresolved, multinuclear, nitrosyl, a carbene with no heteroatom, an odd hapto site, a side-on or B-H or Si-H contact the bonds dropped, a state the report overrode (StateOverridden), and a cage class (CageClass). ElectronCount::unsettled carries a reading the geometry does not settle with the totals under the others (CountReading::Unsettled). The other modules:

Module Role
ligand_field.rs Sigma-only angular overlap picture: d-orbital pattern, both spin fillings where high and low spin differ, Jahn-Teller character. The stabilisation is a property of the model in e_sigma, not a computed energy
site_trust.rs assess_site_trust grades a state High, Moderate, Low or AsStated and lists Cautions (MethodsDisagree, BondValenceFallback, CoordinationPrior, OneMethod, OddElectrons, Unresolved, NonInnocent, UnusualState)
non_innocent.rs Five classes whose charge is a convention: dithiolene, dioxolene, alpha-diimine, nitroxide, nitrosyl. No flag does not mean innocent
agostic.rs Agostic and anagostic M…H-C windows; the caller decides from the electron count
shape_measure.rs, embed.rs, stereo.rs SHAPE-parity continuous shape measures, distance-geometry embedding, and stereocentre repair. Each has a <name>/tests.rs beside it; shape_measure/shape_parity.json is the SHAPE reference

Benchmarks that gate this work. The suite has never found a defect here by going red, so changes are scored against outside references. All data is local: the CSD is licensed and is never committed.

  • scripts/csd_werner_benchmark.py (extract, run, score) scores reported states against the Roman numeral in CSD compound names. extract needs the CSD Python API’s interpreter and writes XYZ files to a scratch directory.
  • scripts/metal_complex_benchmark.py (build, run, score, bos, bvs) scores identifiers against tmQM’s ionic SMILES, and the state against BOS-Lig with bos.
  • report_electron_counts_over_tmqm in automation/cli/tests/electron_count_tmqm.rs is #[ignore]d and reads tmQM, where every complex is closed-shell, so an odd count is a wrong division of charge. Set ORBITRON_TMQM to the .xyz files (colon-separated), ORBITRON_TMQM_LIMIT to cap records (0 for all) and ORBITRON_TMQM_OUT to write a row per metal. It reports and does not gate; run it with --run-ignored all --no-capture.

Compare whole rows before and after a change here, not totals. The reasoning and dated figures are in notes/archive/electron-counting.md, notes/archive/metal-complex-identifiers.md and notes/archive/open-items-closed-through-2026-10-12.md.