Tal — Crystallographic Application

Author: Daniel M. Kaminski · Lublin, Poland

https://dkami.umcs.pl/tal/ · https://dkami.umcs.pl/cavity-detector-in-cif-v1-82/

Version 3.2 beta · Last updated: 2026 · Help aligned with source

1. Introduction

Tal is a sophisticated desktop application for crystallographic visualization and analysis. Built with .NET 8 and MonoGame, it offers a high-performance 3D rendering environment for exploring molecular structures, analyzing intermolecular interactions, and performing advanced computational tasks like cavity detection and Hirshfeld surface analysis.

The application uses a flexible docking interface, allowing you to customize the layout of various tool windows around the central 3D viewport.

Key Features:

2. File Management

Operations related to opening, saving, and exporting data are found in the File menu (MainForm.Menu.cs / MainForm.Events.Menu.cs).

2.1. Open... (Ctrl+O)

Opens a structure file. Tal supports:

If a file contains multiple structures (e.g. multi-record CIF), Tal prompts to open all, only the first, or none. Loaded structures are added to StructureStorage / Structure List.

2.2. Open Recent

Recent files are managed by the singleton RecentFilesManager and persisted to %LocalAppData%\Tal\RecentFiles.json.

2.3. Save (Ctrl+S) / Save As...

Both menu items call the same handler OnSaveAs: a save dialog is always shown. The currently selected structure from the Structure List is written via StructureWriter.Save(), dispatched by extension.

Supported write formats

ExtensionWriterNotes
.cifCifWriterIUCr CIF with metadata (cell, symmetry, refinement, ADP, disorder).
.res, .insResWriterSHELX; uses SymmetryHelper (no guessed operations).
.xyzXyzWriterCartesian XYZ.
.pdb, .entPdbWriterHETATM; CRYST1 from cell.
.mol, .sdf, .sdMolWriterV2000 molfile (bonds currently not exported).
.mol2, .ml2Mol2WriterTripos MOL2; optional CRYSIN.
.gjf, .com, .gauGaussianWriterDefault route #p B3LYP/6-31G(d) Opt.
.d12CrystalWriterCRYSTAL17 input; may require standard space-group setting.

CRYSTAL17 export and standard setting

Before writing .d12, Tal calls SpaceGroupTransformer.GetTransformToStandard(). If the structure is non-standard, a dialog is shown:

Note: Transformation updates cell parameters, fractional coordinates, ADP tensors, symmetry operations, and the space-group name.

2.4. Save Screenshot... (Ctrl+Shift+S)

Opens ScreenshotDialog: captures the active MonoGame viewport at a chosen supersampled resolution.

ControlDescription
cmbScale / ScaleMultiplier: 1×, 2×, 3×, 4×, 6×, 8× (labels include original / HD / 4K / max). Default often 2×.
Resolution previewLive “→ W × H px”; shows “(max)” when clamped.
Full frameDefault — 3D + labels + color scale.
Scene onlyPure 3D, no overlay UI.
SupersampleExtra AA inside the renderer (slower).

Final size: ResultW = round(baseWidth × Scale), clamped to MAX_DIMENSION = 8192 px per axis. Base size = current backbuffer of the active document.

2.5. Export to POV-Ray...

Exports the scene (atoms, bonds, unit cell, visible cavities, polyhedra, planes) to a *.pov file via PovRayGenerator. Uses current camera, lighting, background, and display flags. See section 13.

2.6. Close (Ctrl+W) / Exit (Alt+F4)

2b. Edit, View, Window & Help Menus

Edit menu

CommandShortcutHandler
UndoCtrl+ZOnUndo
RedoCtrl+YOnRedo
CutCtrl+XOnCut
CopyCtrl+COnCopy
PasteCtrl+VOnPaste
Select AllCtrl+AOnSelectAll

For atom/molecule selection prefer the Selection menu and picking modes.

View menu

Window menu

Help menu

3. Selection & Manipulation

The Selection menu, picking toolbar, mouse modes, and the right-click context menu control interaction with atoms and molecules.

3.1. Picking Mode (toolbar)

The picking strip combo box (MainForm.Toolbars.cs) offers:

ModeBehaviour
Rotate (default)Left-drag rotate; right-drag zoom; middle-drag pan.
Pick AtomsClick atoms to select/deselect (measurements, planes, individual colours).
Lasso AtomsDrag a rectangle; atoms inside become selected.
Expand ContactsClick an atom to grow its short-contact environment (symmetry/translation images), analogous to Mercury-style expansion. Option “Select whole molecule (Expand Contacts)” lives in Short Contacts settings.
Toggle LabelsClick to toggle label visibility on individual atoms.
Move LabelsDrag atom labels to reposition them.
Measure DistancesClick pairs of atoms for distance measurements.
Measure AnglesThree-atom angle measurements.
Measure TorsionsFour-atom torsion measurements.
Reveal Symmetry-Generated MoleculesReveal molecules generated by symmetry (see also MainForm.RevealSymmetry.Helpers).

Also on the picking strip: Clear Measurements, and Show Labels for: All atoms / Not hydrogen atoms / Selected atoms / None.

3.2. Selection menu

Note: Selected atoms are highlighted in yellow. Selection drives individual colours, planes, measurements, and hide operations.

3.3. Context menu (right-click)

Built in MainForm.ContextMenu.cs, updated from the atom under the cursor:

These call into AtomVisibilityManager (see section 16). After bulk hides, use Remove hanging contacts so contact lines are not drawn to invisible atoms.

3.4. Programmatic selection helpers

MeanPlaneCalculator (namespace Tal.Core.PowderDiffraction) exposes:

Used by the Crystal Planes tool when building mean planes.

4. Visual Display

The Display menu, style toolbar, view toolbar, and docked Display Options window control appearance.

4.1. Style (toolbar + Display → Style)

Global style combo (exact labels from code):

Per-atom overrides remain available via Atom Display Settings / colour tools.

4.2. Colour atom

Opens the atom colouring window. Modes include:

4.3. Labels

Opens LabelsWindow (🏷️ Labels Settings): show/hide, font, shadow, default offset, optional property beside the name. Individual repositioning: picking mode Move Labels.

4.4. Show Hydrogens / Show Unit Cell / Reset View

4.5. View toolbar

4.6. Background... (Ctrl+B)

BackgroundColorWindow — solid colour or two-colour gradient; presets or custom colours.

4.7. Lighting...

LightingWindow — primary and secondary directional lights (direction, diffuse, ambient, specular colour/power/intensity); optional per-pixel lighting.

4.8. Atom Display Settings

Window title 🎨 Atom Display Settings (AtomDisplayWindow):

Buttons: Default, Reset, Apply. Probability is the source of truth for ellipsoid scale (χ² table).

4.9. Atom Colors (global)...

Edits the persistent per-element palette (atomic number index) via AtomPropertiesWindow / related helpers. Overrides are stored in Flags.AtomColorOverrides and applied on startup. Reset Selected / Reset All restore defaults.

4.10. Short Contacts...

Opens ShortContactsWindow (“Define Short Contacts”):

Detected contacts are drawn as dashed lines. Calculate → Contacts... reuses this path (OnCalcContacts → OnShortContacts).

4.11. Scene Quality...

SceneQualityWindow (labels from Designer; defaults from Flags):

4.12. Display Options (Window menu)

DisplayOptionsWindow quick toggles:

Depth cue details: DepthCueWindow. Stereo and Z-clipping are experimental depending on build.

4b. Symmetry Elements

The Symmetry Elements window visualizes the symmetry operations of the crystal structure in the style of Mercury and the ITA (International Tables for Crystallography) conventions. It shows rotation axes, screw axes, mirror planes, glide planes, inversion centers, and rotoinversion axes directly in the 3D viewport.

4b.1. Opening the window

The window is opened from Display → 🔷 Symmetry Elements... or with the keyboard shortcut Ctrl+Shift+E (OnSymmetryElements in MainForm.Events.Menu.cs). It is a dockable DockContent (SymmetryElementsWindow) and can be placed anywhere in the layout, but is shown by default on the left dock with the standard tool-window width.

Live updates. Every control in this window acts immediately — checking a box, moving a slider, or picking a color updates the 3D view at once. There is no Apply button. The mechanism is the same as in Atom Color and Atom Display Settings: each change writes the new value into Flags.Instance, bumps VisualRevision (which resets Progressive AA), invalidates the renderer's element cache, and triggers RefreshAllDocuments().

4b.2. Master switch

The Show Symmetry Elements checkbox at the top of the window controls Flags.ShowSymmetryElements. When it is off, the renderer skips the entire pass and nothing is drawn. This is the only global toggle; every other control in the window affects one specific kind of element.

4b.3. Element types & colors

The window groups every symmetry element into a two-column grid: the left column is a checkbox that controls visibility, the right column is a small color swatch that opens a standard ColorDialog. Each row is independent: you can hide, for example, only the 3₂ screw axes while keeping 3₁ visible, and each one has its own color.

GroupRowsFlags (visibility / color)
Rotation axes 2, 3, 4, 6 ShowRotationAxis2/3/4/6 · SymmetryAxis2Color* … SymmetryAxis6Color*
Screw axes 2₁, 3₁, 3₂, 4₁, 4₂, 4₃, 6₁, 6₂, 6₃, 6₄, 6₅ ShowScrew21 … ShowScrew65 · SymmetryScrew21Color* … SymmetryScrew65Color*
Rotoinversion axes −3, −4, −6 ShowRotoinv3/4/6 · SymmetryRotoinv3Color* … SymmetryRotoinv6Color*
Planes m (mirror) · a, b, c, n, d, g (glide — one row per family) ShowMirrorPlanes, ShowGlidePlanes · SymmetryMirrorColor*, SymmetryGlideColor*
Centers −1 (inversion centers) ShowInversionCenters · SymmetryCenterColor*
Why per-subscript colors? Screw axes with the same order but different subscripts (3₁ vs 3₂, 6₃ vs 6₁) are different symmetry operations — they represent different translation components along the same rotation axis. Mercury-style tools color them separately so that a complex packing with several screw families is readable at a glance. Tal follows this convention and gives each subscript its own checkbox and swatch.

4b.4. Rendering style (ITA markers)

Elements are drawn in the style of the International Tables for Crystallography:

Long rod → arrow. The renderer draws each axis as a straight rod that starts at one cell wall and stops a fixed distance before the opposite wall, where the marker (cone, triangle, rhombus, hexagon) is placed. The distance between the end of the rod and the marker is 5 × arrow length, which keeps the marker from clipping into the wall and makes the direction of the axis obvious at a glance. Earlier versions drew a full rod through the cell with no gap; that is what the changelog calls "długa belka".

4b.5. Sizes and transparency

The bottom group of the window controls the sizes and opacities. These are the same parameters for every element of that kind — the per-element controls are only for visibility and color.

SliderFlagRangeWhat it affects
Axis Thickness SymmetryAxisThickness 0.001 – 0.200 Å The radius of the cylindrical rod used for every axis and for the edges of planes.
Marker Size SymmetryMarkerSize 0.05 – 2.00 The size of the ITA markers (triangle / rhombus / hexagon / ring) relative to the axis direction.
Mirror Plane α SymmetryMirrorAlpha 0 – 100 % The fill alpha of mirror planes (the polygon face).
Glide Plane α SymmetryGlideAlpha 0 – 100 % The fill alpha of glide planes.
Center Radius SymmetryCenterRadius 0.01 – 2.00 Å The radius of the sphere used for inversion centers (and the length unit of its crosshair).

All five sliders are clamped to their declared range, so an old tal_settings.json with values outside the range (for example a leftover 0.185 for axis thickness) cannot crash the window — it is silently clamped to the nearest legal value.

4b.6. Live updates and colors

Colors and sizes are read live from Flags.Instance every frame by the renderer CellSymmetryRenderer. The pipeline is:

  1. A control changes a Flags field, calls Flags.MarkDirty(), and calls the shared RefreshView() helper.
  2. RefreshView() bumps Flags.VisualRevision, calls MainForm.InvalidateSymmetryRenderers() (which sets a rebuild flag in every renderer), and calls MainForm.RefreshAllDocuments().
  3. On the next frame, CellSymmetryRenderer.Draw recomputes the color for each element via GetColorForElement(kind, order, subscript), which reads the current Flags values. The rod thickness, marker size, inversion radius and plane alphas are also read from Flags inside the drawing code.
  4. The rebuild flag triggers a full rebuild of the cached _elements list from the current Molecule if the geometry needs to change (for example after a different symmetry operation became visible). For simple color changes, the rebuild is skipped and only the colors change.

This makes the behavior identical to what you see in Atom Color and Atom Display Settings: no Apply button, no OK/Close required, no need to reopen the window . The change appears as soon as the next frame is rendered.

4b.7. Refresh and Reset buttons

The window has two buttons at the bottom:

4b.8. What is shown and where it comes from

Every element copy that intersects the unit cell [0,1]³ is generated. The pipeline is:

  1. The full symmetry group is closed from the generators stored in Molecule.SymmetryOperations. If the CIF (or RES) file only lists generators, the closure expands them up to the full group (max 192 operations).
  2. Every operation is classified by trace and determinant of its linear part: det = +1 with trace −1, 0, 1, 2 is a rotation of order 2/3/4/6; det = −1 with trace 1 is a mirror plane; det = −1 with trace −3 is an inversion center; and the remaining det = −1 cases are rotoinversion axes −3, −4, −6.
  3. For each operation and for every lattice translation L ∈ {−2 … 2}³, the intersection of the element with the unit cell is computed in fractional coordinates, then converted to Cartesian using the crystal's fractional-to-Cartesian matrix.
  4. Duplicate copies (same axis line, same plane, same point) are deduplicated with a geometric key, so a 4-fold axis in a cell with four equivalent copies is not drawn four times on top of itself.
  5. Screw subscripts are derived from the intrinsic translation along the axis: w = (1/n)·Σ Rᵏ·t, projected onto the axis direction and quantized to 1/n. The rotation sense (right-hand rule in Cartesian space) decides whether a 3-fold becomes 3₁ or 3₂.
  6. Glide letters are derived from the glide vector w = (t + R·t) / 2. A single half-translation along a/b/c gives a/b/c; two half-translations give n; quarter-translations give d; anything else is g.

4b.9. Interaction with other tools

4b.10. Keyboard shortcuts and menu map

ActionShortcut / path
Open the window Display → 🔷 Symmetry Elements... · Ctrl+Shift+E
Close the window Standard docking close (click the × in the tab, or View → Reset Layout to restore the default layout)

5. Calculations

The Calculate menu (MainForm.Menu.cs). Several entries are fully wired; a few are still stubs.

5.1. Centroids...

Not implemented. OnCalcCentroids only sets the status bar. Reserved for the geometric centroid of selected atoms.

5.2. Planes...

Opens PlanesWindow (dock left). Define planes from selected atoms (mean plane via MeanPlaneCalculator) or Miller indices (hkl). Visibility, colour, transparency; distance-to-plane measurements. See section 9.

5.3. Packing...

Opens PackingWindow (“📦 Packing”). Controls fractional extent of the displayed block:

Typical presets in code: asymmetric-style 0–0.5, 0–1 unit cell, 0–2 / 0–3 supercells.

5.4. Contacts...

Alias of Display → Short Contacts (OnCalcContacts → OnShortContacts). See §4.10 and Expand Contacts picking mode.

5.5. Powder Pattern...

Opens PowderDiffractionWindow when an active document with atoms exists. See section 8.

5.6. Molecular Shell...

Not implemented. OnCalcMolecularShell only updates the status bar.

5.7. Graph Sets...

Not implemented. OnCalcGraphSets only updates the status bar (reserved for H-bond graph-set analysis).

5.8. Cavity Settings... (Ctrl+Shift+C)

Opens CavitySettingsWindow (title 🕳️ Cavity Settings). Full algorithm and parameters: section 6. UI highlights: Enable Cavity Display; Resolution (Å); VdW Multiplier; Smoothing; Transparency; Use thermal motion (Uij/Uiso); Detect / Clear / Export Report; display modes Hidden / Normal Cavity / Property Map / Topology Map / Channel Paths / Sphere Fits; property types Charge, Hydrophobicity, Atomic Number, VdW Radius, Occupancy.

5.9. Transform space group...

Transforms the active structure between equivalent settings (e.g. P2₁/c ↔ P2₁/n) via SpaceGroupTransformer. Updates cell, coordinates, ADP tensors, and symmetry in place. See section 13.

Warning: Destructive on the structure in storage — save before use.

5.10. Normalize Hydrogens...

Opens HydrogenNormalizationWindow (🧪 Hydrogen Normalization). Loads the active molecule, then docks the tool window. Uses HydrogenNormalizer (neutron-standard X–H lengths, optional skip of bridging H, Apply / Reset / cache of originals). Details and Hirshfeld auto-path: section 7.2 and the dedicated section below if present.

6. Cavity Analysis

Tal includes a powerful and robust engine for the detection and analysis of voids and cavities in crystal structures. This is a key feature for studying porosity, host–guest chemistry, and gas storage potential.

6.1. Cavity Detection & Display Window

This is the main control panel for all cavity-related operations, opened from Calculate → Cavity Settings... (Ctrl+Shift+C).

Detection Parameters

Controls in CavitySettingsWindow (Designer labels) and corresponding Flags / detector fields:

Engine-only parameters (present on CavityDetector / static fields, not as dedicated controls on the current settings form):
  • ProbeRadius (default 0.0 Å) — added to every effective radius; Connolly-style probe.
  • MinCavityVolume (default 0.1 ų) — cavities below this volume are discarded.
  • ScanMargin = 2 Å — periodic neighbour margin for atom generation.

Actions

Display Modes

Once cavities have been detected, you can choose how to visualize them:

6.2. The Cavity Detection Algorithm

Tal's cavity detector is a sophisticated implementation designed for speed and accuracy, even for large and complex structures.

  1. Atom generation: The scanner first generates all atoms of the unit cell together with their neighbours (the atoms in the 26 periodic images that are within ScanMargin = 2 Å of the cell boundary). Every atom is stored with its effective radius:
    r_eff(i) = r_vdw(i) · VdWValue + ProbeRadius
    This radius is baked into the atom and into the SDF that is computed from it. There is no separate "base SDF" cached across VdW values — see section 15.
  2. Signed Distance Function (SDF) calculation: A high-resolution 3D grid (the "voxel grid") is created over the unit cell, extended by a margin of ScanMargin on each side. For every voxel the algorithm computes the signed distance to the surface of any atom:
    d_i       = distance from the voxel to the surface of atom i
                = |p − c_i| − r_eff(i)      (spherical case)
    SDF(p)    = min_i d_i
    
    SDF is negative inside an atom and positive outside. It is truncated at MaxDistFine = 5 Å — voxels further than 5 Å from any atom are set to that value, which is more than enough for the flood-fill to distinguish "empty" from "occupied".
  3. Handling of anisotropic displacement parameters (ADPs): When Use thermal motion (Uij/Uiso) is enabled, the distance is not computed against a sphere but against an ellipsoid whose principal half-axes are:
    semi-axis_i = r_vdw(i) · VdWValue + ProbeRadius + √|λ_i|
    where √|λ_i| is the i-th row length of the Un matrix produced by the Eigen decomposition of the ADP tensor (see section 6.2). The half-axes are stored per atom and used inside the SDF hot loop.
    Symmetry-equivalent atoms. The ADP of every symmetry-generated atom is rotated together with its position:
    Un' = Un · R_cartᵀ        Ue' = Ue · R_cartᵀ
    R_cart = A · R_frac · A⁻¹
    where A is the fractional → Cartesian matrix (x_cart = A · x_frac, column convention) and R_frac is the 3 × 3 linear part of the symmetry operation (translations do not rotate the ADP). This is valid for every crystal system, including hexagonal, trigonal and rhombohedral settings; no diagonal-only or sign-only shortcut is used on this path.
    The split between the two consumers is deliberate:
    • Cavity detector / VoxelScanner reads the rows of Un' as the ellipsoid axes. Because Un' = Un · R_cartᵀ, each row i has length √|λ_i| (the 1σ half-axis is preserved) and direction R_cart · e_i (the eigenvector is rotated). Transformation is ignored by this path (ReExpandSymmetry = false).
    • ORTEP / Draw multiplies on the left by Transformation = R_cart, so the rendered quadric is R_cart · Un · R_cartᵀ · prob — the same form as the old S · Un · S for diagonal sign-only operations, generalized to full R_cart.
    The fallback path (TransformUnSignOnly + Transformation = diag(sign1)) is used only when the cell matrix A is not invertible; it is bit-compatible with the historical CalculateSymmetryEquivalents. See the header of Molecule.Symmetry.cs for the full derivation.
  4. Periodicity and flood-fill: The grid is extended into a margin around the unit cell. A flood-fill algorithm starts from the outermost boundary of this extended grid (the "exterior") and fills all connected empty voxels. Any empty voxels not reached by this fill are considered to be part of a "buried" cavity.
  5. Wall detection: For every detected cavity the algorithm records which unit-cell walls it touches (WallXMin/Max, WallYMin/Max, WallZMin/Max). This is what tells you later whether a cavity is closed or part of a percolating channel.
  6. Marching Cubes mesh generation: The Marching Cubes algorithm is applied to the SDF grid to generate a smooth triangle mesh for the surface of each cavity. The quality of the mesh is controlled via the Smoothing parameter (see section 6.8).
  7. Post-processing per cavity: For each cavity the engine also computes:
    • volume (voxel count × voxel volume) and its error estimate,
    • surface area and its error estimate,
    • sphericity,
    • center of mass,
    • fractional coordinates of every voxel (used by property mapping),
    • per-voxel charge, hydrophobicity and Z-effective (used by the property map display mode).
Anisotropic VdW — exact math. The ADP matrix Un produced by the Eigen decomposition has the form Un = −D · Eᵀ (the overall sign is an implementation convention and does not affect the row lengths), where E is the matrix of eigenvectors (as columns) and D = diag(√|λ₁|, √|λ₂|, √|λ₃|). Therefore the row i of Un has length √|λ_i|, which is the physical half-axis of the thermal ellipsoid in the direction of eigenvector e_i. Tal reads those row lengths once per atom, stores them as atomRx, atomRy, atomRz together with the orthonormalized eigenvectors, and uses the ellipsoid quadratic form
q(p) = ex²·invRx² + ey²·invRy² + ez²·invRz²
to test inside/outside and to estimate the distance to the surface. Voxels with q ≤ 1 are inside the ellipsoid.
Important: the VdW multiplier scales only the base r_vdw. The thermal-motion term √|λ_i| and the ProbeRadius are added as constants, independent of the multiplier. This is intentional: the multiplier is a probe of the contact radius, not of the thermal motion amplitude.
Split Un' vs Transformation. The same atom record stores both Un' = Un · R_cartᵀ and Transformation = R_cart (set in Molecule.Symmetry.cs). The two consumers read different fields from that record:
  • Cavity / VoxelScanner uses only the rows of Un' (half-axes = row lengths, directions = rotated eigenvectors). It never reads Transformation (ReExpandSymmetry = false).
  • ORTEP / Draw multiplies Transformation · Un · Scale(probability), which expands to R_cart · Un · R_cartᵀ · prob — the generalisation of the old diagonal S · Un · S.
This split avoids the two classic mistakes: Un' = R · Un · Rᵀ with T = I (Draw OK, SDF destroys row lengths — bad voids in rhombohedral/hexagonal cells) and Un' = Un · Rᵀ with T = I (SDF OK, Draw misses the left R — inverted principal axes on screen). See the header of Molecule.Symmetry.cs, section 2.
ADP in the cavity detector — conventions.
  • The thermal term is 1σ, not the displayed ellipsoid. The amplitude added to every atom is √|λi| of the Cartesian ADP tensor, i.e. the RMS displacement (1σ; the ellipsoid encloses ≈ 20% of the probability). It does not depend on the ellipsoid probability set for the ORTEP display (default 50%, scale factor 1.538). The two are different quantities: the displayed ellipsoid is a multiple of the 1σ ellipsoid, whereas the cavity radius is the 1σ amplitude plus a constant. Along a principal axis with RMS amplitude σ, the distance between the cavity boundary and an ellipsoid drawn with scale factor s is
    gap = VdWValue·r_vdw + ProbeRadius − (s − 1)·σ
    (positive: the cavity boundary lies outside the drawn ellipsoid). For carbon at a VdW multiplier of 0.1 and s = 1.538 this is 0.17 Å − 0.538·σ. It vanishes at σ ≈ 0.32 Å: for smaller amplitudes the cavity is larger than the drawn ellipsoid, for larger ones the ellipsoid protrudes. A tight visual fit at one multiplier is therefore a coincidence of these numbers and not a property of the model. For an exact comparison set the ellipsoid probability to about 20% (s ≈ 1.0): the cavity boundary is then the drawn ellipsoid shifted outwards by the constant VdWValue·r_vdw + ProbeRadius.
  • Symmetry-equivalent atoms. The ADP tensors of symmetry-equivalent atoms are rotated together with their positions:
    Un' = Un · R_cartᵀ        Ue' = Ue · R_cartᵀ        R_cart = A · R_frac · A⁻¹
    where A is the fractional → Cartesian matrix (x_cart = A · x_frac, column convention) and R_frac is the 3 × 3 linear part of the symmetry operation (translations do not rotate the ADP). This holds for all crystal systems, including hexagonal, trigonal and rhombohedral settings. The cavity detector uses these symmetry-equivalent atoms as they are (the symmetry operations are not applied a second time), so the ellipsoids that define the cavity have the same orientation as the ellipsoids drawn in the 3D view.
    Note: the previously documented form Ue' = R_cart · Ue · R_cartᵀ was used by older builds and by the SpaceGroupTransformer path. The cavity pipeline uses the row convention Un · R_cartᵀ so that the eigenvalues (the physical 1σ half-axes) are preserved bit-for-bit while the eigenvectors are rotated. Do not mix the two conventions when comparing numbers between tools.
  • Atoms without anisotropic ADPs (for example riding hydrogens) are treated as spheres of half-axis √Uiso. The CIF reader raises Uiso to at least 0.05 Ų (0.22 Å); the RES reader replaces only missing values (below 0.001 Ų) by 0.05 Ų.

6.3. Resolution, memory and performance

The number of voxels in the supercell grid grows cubically with the resolution:

N_voxels ≈ (cell_size / resolution)³ · (1 + 2 · margin_fraction)³

Tal enforces a hard limit Flags.MAX_VOXELS on the total voxel count. If the grid exceeds this limit, detection is aborted with an error message and a suggested coarser resolution. Two additional thresholds trigger warnings:

The memory footprint is roughly 4 bytes × N_voxels for the density grid, plus about the same again for the periodic copy, gradient arrays, and the voxel mask. A 512³ grid (≈ 134 M voxels) already needs ~1.5 GB of RAM, which is why the default resolution is capped around 0.10–0.15 Å for typical organic crystals.

6.4. Why the void volume shrinks: an erosion model

Both the VdW multiplier and the anisotropic (ADP) mode make every atom larger and therefore reduce the free space. The reduction is often much larger than the relative growth of the atoms , and the number of cavities usually changes as well. This section gives a simple model that predicts the size of the effect from two numbers that Tal already reports (void volume V and void surface S), and explains when a void splits or disappears. Throughout, κADP = V(ADP = YES) / V(ADP = NO); this ratio is not the same quantity as the κ = V(1.5)/V(1.0) of the VdW sweep (section 15.6).

The core idea: inflating the atoms erodes the void

Let d(p) be the distance from a point p to the nearest atom surface (this is the SDF of section 6.2). A point belongs to the void when d(p) > 0. If every atom is inflated by the same amount δ, the point stays in the void only when d(p) > δ. In other words, the new void is the old void eroded by a ball of radius δ: every wall of the void moves inwards by δ. In ADP mode δ is not the same for every wall: it is the displacement amplitude of the nearest atom in the direction of the wall normal (see "What is δ for an ellipsoid?" below).

Effect 1 — loss of volume

Because every wall moves inwards by δ, the volume lost per unit of δ is exactly the area of the wall. For the eroded void this gives the exact relation

dV/dδ = − S(δ)          V(δ) = V₀ − ∫₀^δ S(t) dt

where S(δ) is the void surface after the erosion. To first order in δ:

κ_ADP ≈ 1 − δ·S₀/V₀ = 1 − δ/ℓ          ℓ = V₀/S₀

The characteristic length ℓ = V/S of the void measured without ADP decides everything: a void is robust when δ ≪ ℓ and collapses when δ is comparable to ℓ. For simple shapes with inscribed radius ρ the erosion can be written exactly:

Void shapeℓ = V/SκADP(δ)Disappears at
Compact pocket (sphere, radius ρ)ρ / 3(1 − δ/ρ)³δ = ρ
Channel (cylinder, radius ρ)ρ / 2(1 − δ/ρ)²δ = ρ
Layer (slab, half-thickness ρ)ρ1 − δ/ρδ = ρ

All three are of the form (1 − δ/ρ)m with m = 3, 2, 1 for pockets, channels and layers. Real voids are mixtures of these. A void of large volume but small ℓ (thin, branched, low sphericity) loses most of its volume at a very small δ, even if its equivalent-sphere radius is large. Note that a void of inscribed radius ρ (the "max sphere radius" of the sphere fit) disappears completely at δ = ρ, regardless of its volume.

Effect 2 — change of connectivity

A neck of free width w (the diameter of the sphere that can pass through it) closes when its two walls have both moved by δ, i.e. when δ ≥ w/2. A void network therefore splits at the bottleneck radius δc = wmin/2 of the narrowest neck on its connecting path (MinWidth of the channel analysis, section 15.10, is a local diameter, so δc = MinWidth/2). Splitting changes the number of cavities but, taken alone, hardly changes the total volume (a neck contributes very little volume). The total volume falls additionally through fragmentation only in two ways:

Consequently, the number of cavities, the total volume and κADP depend on Min cavity volume. Always quote that parameter together with κADP. The effects of volume loss and of connectivity change are two observables of the same erosion; they are not two independent factors that multiply each other.

What is δ for an ellipsoid?

An atom with the displacement tensor U (Cartesian, Ų) is represented by the 1σ ellipsoid with half-axes √λi. A wall with outward normal n is moved by the extent of this ellipsoid in the direction n, that is, by

δ(n) = √(nᵀ·U·n)         √λ_min ≤ δ(n) ≤ √λ_max

So it is the vibration towards the void that matters, not the mean amplitude √Ueq. Typical values of √Ueq for C, N, O atoms are ≈ 0.12–0.16 Å near 100 K (Ueq ≈ 0.015–0.025 Ų) and ≈ 0.20–0.25 Å at room temperature (Ueq ≈ 0.04–0.06 Ų). Here U is the 1σ tensor of section 6.2; a thermal term scaled by a factor s (e.g. 1.538 for ellipsoids drawn at 50%) would scale δ by the same factor.

Illustration: a lattice without necks (FCC)

The following didactic model contains only the volume effect and no necks. It is not a molecular crystal. Take an FCC lattice with a = 3.615 Å (4 atoms per cell, Vcell = a³ = 47.24 ų, nearest-neighbour distance dnn = a/√2 = 2.556 Å), a base radius rbase = 1.00 Å (deliberately smaller than a real VdW radius, so that free space exists) and an isotropic increase δ. As long as the spheres do not overlap (δ < δtouch = dnn/2 − rbase = 0.278 Å):

V_free(δ) = a³ − (16π/3)·(r_base + δ)³
δ (Å) radius r (Å) V_atom (ų) 4·V_atom (ų) V_free (ų) % of cell κ = V_free(δ)/V_free(0)
0.001.0004.1916.7630.4964.5%1.000
0.051.0504.8519.4027.8558.9%0.913
0.101.1005.5822.3024.9452.8%0.818
0.151.1506.3725.4821.7646.1%0.714
0.201.2007.2428.9518.2938.7%0.600
0.251.2508.1832.7214.5230.7%0.476
0.2781.2788.7534.9812.2626.0%0.402

At contact the spheres fill 74.0% of the cell (the close-packed value), so the free volume is 26.0%, which is a useful check of the table. The model illustrates how fast the free volume falls when the radius grows by only δ/rbase = 10–25%. The numbers are specific to this lattice and are not transferable to a real crystal (real VdW radii are larger, so the same δ is a smaller relative change; real voids have a different ℓ). Also note that this lattice has no necks to close: for a point probe the free space of FCC stays connected through the triangular windows up to r = dnn/√3 = 1.476 Å, i.e. well beyond the contact. Fragmentation is therefore described by Effect 2 above, not by this model.

6.5. Meaning of κADP and relation to packing coefficients

The packing coefficient of Kitaigorodskii, k = Vmolecules / Vcell (for hard spheres at most 0.74), is a static measure: it counts the cell volume occupied by the VdW envelopes of the molecules. Since 1 − k is the whole space not covered by the VdW envelopes, including the narrow interstices that Tal does not report as cavities, the void fill fraction of Tal (probe 0, VdW multiplier 1.0) is at most 1 − k. The two numbers are not expected to be equal.

κADP adds a different piece of information: how much of the reported void lies within a distance of about δ of the walls. By section 6.4, this depends on ℓ = V/S of the void, hence on its shape (low sphericity means a small ℓ). A high κADP (close to 1) means that the voids are compact and survive the thermal inflation; a low value means that the reported volume consists largely of thin regions near the walls.

Remarks on interpretation. κADP is a descriptor of this particular model. It depends on the temperature of the data collection (ADPs grow with temperature), on Min cavity volume and on the grid resolution, so only values obtained under comparable conditions should be compared. A low κADP is a reason to examine the structure more closely (for example, for sensitivity to temperature or to guest inclusion), but it is not a proof of such sensitivity. For the classical packing analysis, see e.g. E. V. Peresypkina, V. A. Blatov, Acta Cryst. B56 (2000) 501–511 and 1035–1045 (molecular coordination numbers, topology of molecular packings); these papers are background, not a basis of the algorithm.

6.6. Worked example: ADP versus spheres

Consider a molecular crystal (P2₁/n, cell 11.80 × 10.39 × 19.46 Å, β = 103.73°, Vcell = 2317.08 ų, 224 atoms in the cell, data collected at 293 K), detected twice with identical parameters (resolution 0.15 Å, VdW multiplier 1.0, probe radius 0.0, Min cavity volume = 0.1 ų, the default). The only difference is the Use Anisotropic VdW flag.

Quantity ADP = NO
(spheres at mean positions)
ADP = YES
(1σ ellipsoids)
Change
Number of cavities16×6
Total void volume (ų)323.0367.66−79%
Total void surface (Ų)1390.64372.95−73%
Characteristic length ℓ = V/S (Å)0.2320.181−22%
Void fill fraction13.94%2.92%−79%
I_frag (cavities per 56 atoms)0.0180.107×6
Mean sphericity0.1640.399+143%
κADP = V(YES)/V(NO)——0.209

Reading the result with the erosion model

Quick estimate before running ADP mode

  1. Run the detection with ADP = NO and read V and S from the report.
  2. Compute ℓ = V/S.
  3. Take δ ≈ √Ueq of the atoms lining the void (see above).
  4. If δ ≪ ℓ, the void is robust. If δ is comparable to or larger than ℓ, expect a strong decrease of volume and a split into several cavities. Cavities whose max sphere radius is smaller than δ will disappear.

Which run is the right one?

The two runs answer different questions.

Limitations of the ADP mode.
  • Meaning of 1σ. The ellipsoid with half-axes √λi encloses ≈ 20% of the probability density of the displacement. The ORTEP ellipsoids in the 3D view are usually drawn at 50% (1.54σ); the cavity detector does not follow that setting (see the ADP conventions in section 6.2). Using 50% ellipsoids for the cavities would increase δ by a factor 1.54 and lower κADP accordingly.
  • What ADPs contain. Refined ADPs include libration, internal vibrations, static disorder and the errors of the model (e.g. absorption). Hydrogen atoms usually have only a riding isotropic Uiso. In ADP mode such atoms are spheres of half-axis √Uiso; the CIF reader raises Uiso to at least 0.05 Ų (see the ADP conventions in section 6.2), so for low-temperature data the hydrogen atoms may be inflated slightly.
  • Non-positive-definite tensors. Tal uses √|λi|. An atom with a negative eigenvalue (a non-positive-definite ADP, typically a sign of a poor refinement) is therefore silently given a positive amplitude. Check the ADPs of the atoms that line a cavity.
  • Approximate geometry. The half-axes are added as rvdw·VdWValue + ProbeRadius + √|λi|. This is exact along the principal axes. In other directions it is slightly smaller than the exact offset of the ellipsoid (the difference is negligible for √|λ| ≪ rvdw). The iso-surface q = 1 is exact, but the distance estimated from the quadratic form is not a Euclidean distance, so sphere-fit radii and channel widths are approximate in ADP mode.
  • Resolution. Necks with a width of about 2δ ≈ 0.3 Å are sampled by only two voxels at a resolution of 0.15 Å. The number of cavities and the volumes of small pieces can therefore depend on the grid; for important results repeat the run with a finer resolution (e.g. 0.10 and 0.075 Å) and compare.

6.7. Single-run report

After a successful detection, the Export Report button writes a complete text report. It is deliberately self-contained: everything needed to reproduce or interpret the numbers is in the file. Sections:

6.8. Smoothing and the Marching Cubes pipeline

Smoothing is applied in two stages, both controlled by the Smoothing slider:

  1. Grid-level smoothing (applied before Marching Cubes):
    • Gaussian blur — a separable 3-tap convolution with a Gaussian kernel whose σ is derived from the smoothing level. This softens the SDF field itself.
    • Laplacian smoothing — a few iterations of the 6-neighbour average, weighted by a smoothing factor (0.25–0.40 depending on the level). This removes the residual staircase from the Gaussian step.
    • Gradient scaling — a small multiplier (0.80–1.00) applied to the deviation from the mean density. Lower values produce flatter, smoother surfaces at the cost of some volume shrinkage.
  2. Mesh-level snapping (applied after Marching Cubes):
    • Vertex welding — vertices that are within SnapTolerance of each other are merged. The tolerance is 0.0004–0.0008 Å depending on the level.
    • Normal recomputation — vertex normals are recomputed from the SDF gradient (not from the triangle geometry), which gives smoother shading on the final mesh.

Smoothing level 0 disables both stages. Levels 1–5 follow the presets shown in section 4. Higher levels produce smoother surfaces but also systematically shrink the cavity volume by 5–15%, so for quantitative work it is recommended to use a low smoothing level (0 or 1) and only enable smoothing for publication figures.

6.9. Clearing and resetting

Clear Cavities removes all detected cavities and their meshes, but it does not clear the cached atoms or the voxel grid — a subsequent detection at the same parameters reuses the cached data and is nearly instant. To fully reset the detector (free all memory, drop the atom cache, drop the spatial index), use Calculate → Cavity Detection → Force Reset. This is also the recommended action when switching to a different crystal structure.

6.10. Space-group transformation — consistency test

The same crystal structure can be expressed in several equivalent settings of the same space group — for example, P21/n and P21/c are the two standard monoclinic settings of space group #14. They describe the same crystal, with the same unit-cell volume and the same number of atoms per cell, but with different cell parameters and different symmetry-operation strings. Because Tal can transform between settings in place (Calculate → Transform space group...), a natural question is: does the cavity detector give the same answer for both settings? The short answer is yes — but only if the ADP tensors of the symmetry equivalents are rotated correctly. This section describes the consistency test and what to expect from it.

Why this test matters

When the atoms carry anisotropic displacement parameters (ADPs), the cavity detector replaces each atom with its 1σ thermal ellipsoid. The shape and orientation of that ellipsoid are stored as the 3×3 tensor Un, and the detector reads the rows of Un as the three half-axes of the ellipsoid (see section 6.2, "Anisotropic VdW — exact math"). If the transformation between two space-group settings rotates the tensor in the wrong convention — for example, full rotation R·U·RT instead of the row-wise U·RT that the detector expects — the row lengths no longer match √λi, the ellipsoids come out physically wrong, and the cavity volumes differ between settings. The test below detects exactly this class of error.

Test procedure

  1. Load a structure and record the cell parameters (a, b, c, α, β, γ, V, Z, atoms in asymmetric unit).
  2. Run a normal cavity detection (Calculate → Cavity Settings → Detect) with Thermal motion = YES, at a chosen resolution (for example 0.15 Å). Export the single-run report.
  3. Transform the structure to the other equivalent setting (Calculate → Transform space group...) and confirm that:
    • the unit-cell volume V is unchanged,
    • the number of atoms in the asymmetric unit is unchanged,
    • the number of symmetry operations is unchanged.
  4. Run cavity detection again, at the same resolution and the same settings as in step 2. Export the second report.
  5. Compare the two reports on the quantities listed in the table below. For a fully consistent implementation, ΔV should be well below 1 % at resolution 0.15 Å, and it should decrease when the resolution is reduced to 0.06 Å.
  6. Optionally, repeat the whole cycle with Thermal motion = NO (spheres). This isolates the contribution of the ADP rotation: any remaining difference in V between settings must be due to the rasterization of the two unit cells, not to the ADP handling.

What to compare

Quantity Expected agreement Comment
Unit-cell volume Vcell exact Both settings describe the same crystal; V is invariant.
Atoms in asymmetric unit exact Transformation must not add or remove atoms.
Symmetry operations count exact Same group number, same order.
Number of cavities exact (typically 1 in both) A single symmetry setting cannot create or destroy cavities unless the geometry really changed.
Total void volume V ≤ 1 % at resolution 0.15 Å;
≤ 0.5 % at resolution 0.06 Å
The key metric. It must converge as the resolution improves. Any stable offset above 1 % indicates a rotation convention bug in the transformation of ADP tensors.
Total void surface S 5–10 % difference is normal Marching Cubes samples the same iso-surface on two different grids. The surface area is a local quantity, so it depends on the orientation of the grid relative to the cavity walls. Do not use S alone to judge consistency.
Sphericity same tolerance as S Sphericity is derived from V and S, so it inherits the rasterization difference of S.
Void fill fraction ≤ 1 % at 0.15 Å Follows V directly, since Vcell is invariant.
Molecule count in cell exact (must be Z) Independent of cavity detection. If it differs between settings, there is a separate bug in the molecule-grouping routine (see "Known issues" below).

Worked example — structure 488_auto

The table below shows the full set of measurements that were taken on the same crystal of 488_auto (space group #14, Z = 4, 38 atoms in the asymmetric unit, 152 atoms per cell) in the two settings P21/n and P21/c. The test was performed twice at each of two resolutions (0.15 Å and 0.06 Å), once with Thermal motion = YES and once with Thermal motion = NO.

Resolution Thermal motion VP21/n (ų) VP21/c (ų) ΔV Interpretation
0.15 Å YES 174.94 190.92 +9.1 % ADP rotation wrong — before fix.
0.06 Å YES 173.05 189.69 +9.6 % ADP rotation wrong — difference does not decrease with resolution, proving the problem is not numerical.
0.06 Å NO 516.65 516.36 −0.06 % Spheres only. Positions are correct; only the ADP path was broken.
0.15 Å YES 192.47 190.92 −0.81 % ADP rotation fixed — after fix.
0.06 Å YES 190.20 189.69 −0.25 % ADP rotation fixed — difference now decreases with resolution, proving numerical convergence.

Two things are worth pointing out in this example:

Known residual differences after the fix

Even after the ADP rotation is correct, two differences between the two settings remain and are expected:

Practical recommendation. When comparing cavity results between two space-group settings, use volume V, V/S and porosity as the primary metrics. Do not expect the surface area or sphericity to match to better than about 10 % unless the two settings happen to give the same grid orientation — which is rarely the case for monoclinic and triclinic cells.

7. Hirshfeld Surface Analysis

This advanced tool, opened from the Advanced menu, is essential for understanding and quantifying intermolecular interactions in molecular crystals. It is based on the Hirshfeld partitioning scheme, which defines a molecule's space in a crystal based on the electron density.

Self-contained pipeline — no preparation required. The Hirshfeld analysis does not depend on the current Packing mode, on Molecule.atom[], or on any presentation state of the main 3D view. The pipeline builds its own fresh unit cell from Molecule.AsymetricPart and Molecule.SymmetryOperations, runs the molecule finder on that fresh cell to detect distinct molecules, marks the chosen target as host and collects all surrounding atoms (guest atoms in the base cell + periodic copies of host and guests) within a neighbour cutoff. The resulting surface is therefore identical regardless of what you see in the main viewport. You can open the window and click Generate at any time — no Packing setup needed.

7.1. The Workflow

  1. Open the Hirshfeld Surface Analysis window from the Advanced menu.
  2. The window automatically builds the fresh unit cell and enumerates all molecules found in it. The Target molecule dropdown is filled with the detected molecules (each entry shows the number of atoms).
  3. Select a target molecule from the dropdown, or click Use current selection to pick the molecule that contains the currently selected atoms in the main viewport.
  4. Choose the density model (isotropic or anisotropic) and the surface coloring mode.
  5. Click the Generate button. The application computes the isosurface, the per-vertex properties (d_e, d_i, d_norm, charge, hydrophobicity) and the contact-type decomposition.
  6. The resulting surface is displayed in the 3D view, colored according to the selected property. The corresponding 2D fingerprint plot can be opened with Show Fingerprint....

7.2. Hydrogen Normalization (automatic)

Hirshfeld surface analysis, by convention (CrystalExplorer, Tonto and all crystallographic tools), requires neutron-normalized hydrogen positions. X-ray diffraction locates the maximum of the electron density of an H atom, which is systematically shifted toward the heavy atom relative to the true nuclear position. Using X-ray H positions in a Hirshfeld analysis introduces a bias of typically 0.10–0.20 Å on every X–H bond, which distorts both the surface and the fingerprint decomposition. Tal therefore always normalizes H before generating a Hirshfeld surface — silently, without a dialog.

What "normalized" means

Each hydrogen atom is placed at its neutron bond length along the direction defined by the heavy-atom coordinate and the current H coordinate. The bond lengths used are tabulated in HydrogenNormalizer.NeutronLengths:

Heavy atomBondNeutron length (Å)
Boron (5)B–H1.180
Carbon (6)C–H1.083
Nitrogen (7)N–H1.009
Oxygen (8)O–H0.983
Fluorine (9)F–H0.950
Phosphorus (15)P–H1.420
Sulfur (16)S–H1.340
Chlorine (17)Cl–H1.280

Detection and application

When Generate is pressed, the pipeline calls Molecule.CheckHydrogenState, which uses the pure function HydrogenNormalizer.Check to analyze every H in the asymmetric part without modifying anything. Each H is classified into one of five states:

  1. Finds the nearest heavy neighbor for every H. The search uses a 1.3 Å cutoff and a minimum-image fractional difference , so H and heavy atoms in different unit cells are matched correctly.
  2. Skips bridging hydrogens (H that is within the cutoff of more than one heavy atom at the same time, e.g. a hydrogen bond donor that also sits near a second acceptor). Skipping avoids forcing a wrong bond direction on a shared H. Skipped H are reported with reason "bridging".
  3. Applies a Cartesian delta: cartShift = unit · (target − len), where unit is the normalized direction from the heavy atom to the current H. This shift is then converted to a fractional delta via Molecule.CartDeltaToFracDelta and added to the H's fractional coordinates. The H stays in the same copy of the unit cell as its heavy neighbor (the fractional coordinates are never wrapped), so cross-boundary molecules remain intact.
  4. Rebuilds the whole 3D scene from the modified asymmetric part via Molecule.RebuildFromAsymetricPart. This regenerates the symmetry equivalents, the 3D atom array, the bond graph, the hydrogen bond graph, and the MoleculeShift.

The pipeline then closes any open fingerprint window (its per-vertex data is now stale), clears the previous Hirshfeld surface (it was built on X-ray H), and continues with the normal detection algorithm.

Deterministic behaviour. The detection is based on the geometric state of the bonds (|Δ| < 0.005 Å) rather than on any persistent flag. Re-running Generate a second time therefore produces the log line "✓ Hydrogens already normalized" and does not modify anything — the second click cannot move H further than the first.
Reversibility. Every normalization step stores a snapshot of the original fractional positions in a static HydrogenNormalizer._originalCache keyed by the Molecule instance. The user can reopen the Calculate → Normalize Hydrogens... window and press Reset at any time to restore the original X-ray H positions in the 3D view. The Reset is a pure operation on the asymmetric part followed by the same RebuildFromAsymetricPart used by the normalizer, so the two operations commute.
Performance warning. Calling RebuildFromAsymetricPart invalidates the entire Hirshfeld surface state (mesh, per-vertex arrays, vertex bins, contact decomposition). The pipeline therefore always clears the old surface before proceeding, and the fingerprint window is closed if it is open. Do not close the Hydrogen Normalization window during a Generate run — the pipeline has already updated the underlying molecule and re-reading it mid-run would produce a stale table.

7.3. Hirshfeld Surface Properties

Note on missing data: If the source file does not provide per-atom charge or Hydrophobicity values, the corresponding coloring mode will show a uniform color (yellow for charge, white for hydrophobicity), and a warning is written to the debug log.

7.4. Electron Density Model — Slater-Type Orbitals (STO)

The promolecule density ρ_target(r) and the surrounding density ρ_outer(r) are computed from a Slater-Type Orbital basis. Each atom in the crystal is represented by the sum of the squared radial orbitals of its electronic configuration, with the coefficients and exponents taken from the tabulated Roothaan–Hartree–Fock STO basis of Koga and coworkers.

STO radial function

For an orbital with principal quantum number n and exponent ξ, the normalized radial part is

R_n(r) = N · r^(n-1) · exp(−ξ·r)
N     = (2ξ)^(n+1/2) / sqrt( (2n)! )

The spherical density of a single shell is the sum of the squared contractions over all orbitals of that shell:

ρ_shell(r) = Σ_orbital  occ(orbital) · | Σ_term  c_term · R_term(r) |²

where occ is the occupancy of the orbital in the atom's configuration (alpha and beta spin summed). The total density of the atom is

ρ_atom(r) = (ρ_s + ρ_p + ρ_d + ρ_f) / (4π)

Dataset — Koga Roothaan–Hartree–Fock

Tal ships with three JSON files that contain the complete STO basis for every element that appears in the neutral, cation and anion datasets. The files are located next to the executable (or in one of the search paths listed in StoDatabase.EnsureLoaded):

FileContentReference
sto_atoms_neutral.json Neutral atoms, Z = 1–95 (through Am). T. Koga, K. Kanayama, S. Watanabe, A. J. Thakkar, Analytical Hartree–Fock wave functions in Slater-type basis sets for the neutral atoms He–Lr , Int. J. Quantum Chem. 71 (1999) 491–503.
sto_atoms_cation.json Cations with charge state +1. T. Koga, H. Tatewaki, A. J. Thakkar, Analytical Hartree–Fock wave functions in Slater-type basis sets for the mono-cations of the atoms Li–Lr , Phys. Rev. A 68 (2003) 032701.
sto_atoms_anion.json Anions with charge state −1. T. Koga, A. J. Thakkar, Analytical Hartree–Fock wave functions in Slater-type basis sets for the mono-anions of the atoms H–Lu , J. Phys. B 29 (1996) 2973.

Each JSON entry contains the atom symbol, total, kinetic and potential energy, the config string (e.g. "CARBON 1S(2)2S(2)2P(2), 3P"), and the four shells s, p, d, f, each of which is a list of terms {n, ξ, coefs[]}. The shell also carries the array of per-orbital occupancies OccAlpha and OccBeta (parsed on the fly by ElectronConfigParser).

Density LUT

Because evaluating the sum of squared contractions at every voxel is expensive, the density of each symbol is precomputed once per session into a StoDensityLut. The LUT is a 1-D array of ρ(r) sampled on a uniform grid with step 0.03 Å up to a hard limit of 15 Å. Three global parameters control the size and shape of the LUT:

ParameterMeaningDefault
DefaultStep Grid step (Å) of the radial LUT. 0.03
ExpCutoff rMax = ExpCutoff / ξmin — the LUT is truncated where exp(−ξmin·r) < exp(−ExpCutoff). 10.0
MaxRMax Hard upper bound on rMax, in Å. 15.0

These three values can be changed at runtime from the Advanced panel (see 7.5). Any change requires a rebuild of the LUT cache, which is done via the "Rebuild LUT cache & Regenerate" button. Because the LUT is shared by all atoms of the same symbol, the cache must be cleared before a regeneration picks up the new parameters.

Cutoff rationale. exp(−10) ≈ 4.5·10⁻⁵ is well below the density threshold that affects the Hirshfeld weight function w(r) = ρ_target / (ρ_target + ρ_outer). Truncating the LUT earlier would distort the surface near the boundary of the promolecule, while truncating later would only waste CPU cycles on voxels whose contribution to w(r) is numerically invisible. The ExpCutoff = 10 value is a compromise validated against the same molecule computed with the full-range 1e-12 basis.

7.5. Advanced Panel — STO Tuning

The right-side control panel contains an Advanced checkbox. Ticking it reveals a small group with three numeric up-down controls and a rebuild button:

ControlMeaningRange
DEFAULT_STEP Grid step (Å) of the radial LUT (see 7.4). 0.005 – 0.100
EXP_CUTOFF LUT truncation exponent (see 7.4). 5.0 – 40.0
MAX_RMAX Hard upper bound on the LUT radius, in Å (see 7.4). 5.0 – 50.0
Rebuild LUT cache & Regenerate — writes the three values into StoDensityLut, clears the per-symbol LUT cache in StoDatabase, prebuilds LUTs for the common symbols (H, C, N, O, F, P, S, Cl, Br, I), and re-runs Generate with the new settings.
Effect of each parameter.
  • DEFAULT_STEP controls the accuracy of the linear interpolation between LUT samples. Halving it doubles the LUT memory (still small: 2–6 KB per element) and reduces the interpolation error by roughly a factor of four.
  • EXP_CUTOFF is the main speed/quality knob. Lower values (8–10) give very fast runs but eventually start clipping the density tails of heavy atoms; higher values (12–15) restore the tails at the cost of roughly a linear increase in the number of voxels touched by FillDensitySto.
  • MAX_RMAX is a safety net. GetMaxRadius returns ExpCutoff / ξmin; for very small exponents (heavy atoms, low-lying valence orbitals) this can exceed 30 Å. The hard limit prevents a single heavy atom from allocating a LUT larger than needed for the current scene.
The three values are not persisted across sessions — they reset to 0.03 / 10.0 / 15.0 on every launch, which keeps the default behaviour deterministic for users who never touch the Advanced panel.

7.6. 2D Fingerprint Plots

This is the cornerstone of Hirshfeld analysis. The fingerprint plot is a 2D histogram of the d_i versus d_e values for every point on the Hirshfeld surface.

7.7. Hirshfeld Window (HirshfeldDocument)

The main 3D view for the Hirshfeld analysis is the HirshfeldDocument — a DockContent that hosts a MonoGame viewport and a right-side control panel. It is created from the Hirshfeld Surface Analysis menu item and reused on subsequent invocations.

Layout

Control panel — Target selection

ControlDescription
_lblMoleculeTitle Section header: "Target molecule:".
_cmbMolecule Combo box listing molecules detected in the active document via HirshfeldTargetSelector.EnumerateMolecules. Each entry is of the form "Molecule #N (X atoms)".
_btnUseSelection Replaces the combo selection with the molecule that the currently selected atoms belong to. If the user selected atoms from more than one molecule (or no atoms at all), a message box is shown.
_lblDensityModelTitle / _cmbDensityModel Density model dropdown: Spherical (isotropic) or Anisotropic (ADP). The anisotropic mode requires valid ADP tensors. The combo is initialised to SelectedIndex = 0, so the window never opens with an empty combo.

Control panel — Detection parameters

ControlParameterRange / Default
_numVoxelSize Voxel size (Å) 0.01 – 0.20, default 0.10. Smaller → higher resolution, more memory and time.
_numSigmaMult Sigma multiplier 0.80 – 2.00, default 1.00. Multiplies the per-element Gaussian σ. 1.0 gives the sharpest, most physical surface.
Note: in STO mode this control has no effect — the STO basis carries its own exponents. It only affects the Gaussian fallback used for atoms that are missing an STO entry.
_numIsoLevel Iso level for w(r) 0.30 – 0.70, default 0.50. Standard Hirshfeld definition.
_numMargin Grid margin (Å) 1.00 – 8.00, default 3.00. Extra space added around the target's bounding sphere. If the surface appears artificially clipped near the outer boundary of the grid, increase the margin (typical values for organic crystals: 3–5 Å).

Control panel — Property & display

ControlDescription
_cmbPropertyMode Surface coloring mode. Six options are available (index → enum):
  • Base color — flat light-blue color (BaseColor = 0).
  • d_norm (blue-white-red) — default; classic CrystalExplorer-style map (DNorm = 1).
  • d_e (external) — distance to nearest outer atom (DE = 2).
  • d_i (internal) — distance to nearest inner atom (DI = 3).
  • Charge (green-yellow-red) — per-vertex partial charge (Charge = 4).
  • Hydrophobicity (blue-white-brown) — per-vertex hydrophobicity (Hydrophobicity = 5).
The combo is initialized to SelectedIndex = 1 (d_norm).
_trackTransparency + _lblTransparencyValue Surface alpha (40 – 255, default 200). The label shows "N / 255".
_trackDNormScale + _lblDNormScaleValue d_norm scale slider (0 – 100 → Flags.HirshfeldDNormScale = 0.2 – 3.0). Controls how fast the surface color saturates when |d_norm| grows.
  • Small value (0.2): even weak contacts are fully saturated — many red/blue areas.
  • Large value (3.0): only extreme d_norm values reach full color — mostly white.
Effect is immediate (no regeneration needed).
_chkShowMesh Master switch for the isosurface mesh.
_chkShowWireframe Draws the triangle edges of the mesh as thin dark lines. Skipped automatically if the mesh contains more than 200,000 triangles.
_chkShowMolecule Draw the source molecule behind the surface.
_chkShowAxes Draw the world XYZ axes (length 20 units, red/green/blue).
_chkShowCell Draw the unit cell edges as orange lines.
_lblZoomInfo Current zoom factor as "Zoom: X.XX× (mouse wheel)". The mouse wheel applies a multiplicative step of 1.05 / 0.95, clamped to 0.2 – 8.0.
Recommended settings for a sharp, publication-quality surface:
  • Voxel size = 0.10 Å
  • Sigma multiplier = 1.00 (only affects Gaussian fallback)
  • Iso level = 0.50
  • Grid margin = 3.0 – 5.0 Å
  • d_norm scale ≈ 1.0 (or tuned to taste)
  • Advanced panel: DEFAULT_STEP = 0.03, EXP_CUTOFF = 10.0, MAX_RMAX = 15.0

Control panel — Actions

ButtonAction
_btnGenerate Runs the pipeline (GenerateAsync). If H are not yet neutron-normalized, the pipeline silently normalizes them first (see 7.2). Disabled while a run is in progress. Cancellation is supported via an internal CancellationTokenSource.
_btnClear Disposes the current surface, clears the smoothing cache (HirshfeldSmoothing.ClearCache), and resets the diagnostics.
_btnSavePly Saves the current surface as an ASCII PLY file (vertices, normals, per-vertex RGB). Useful for MeshLab, Blender, CloudCompare.
_btnExportPng Renders the current view into a RenderTarget2D with SSAA = 2 and saves it as PNG.
_btnFingerprint Opens the HirshfeldFingerprintWindow and pushes the raw per-vertex d_e/d_i arrays plus the atom-name arrays. Registers callbacks OnRegionClicked, OnVertexBinsUpdated, and OnContactFilterChanged.
_btnShowDiagnostics Pushes HirshfeldAtomDistanceDiagnostics to the DebugWindow. The report contains grid info, target/outer statistics, a histogram and a cutoff analysis table.

Progress bar & status label

GenerateAsync — nine-step algorithm

  1. STEP 0/9 — Hydrogen normalization. NormalizeHydrogensForHirshfeldSilently runs first (see 7.2). If any H needed normalization, the pipeline rebuilds the 3D scene, closes the fingerprint window, and clears the previous surface before proceeding.
  2. STEP 1/9 — Building HirshfeldScene. Calls HirshfeldScene.Build(targetMolId, neighborCutoff). If the target molecule is not found, the operation aborts.
  3. STEP 2/9 — Preparing atom entries. Each SceneAtom is wrapped either as an AtomStoParams (if the STO database is loaded and a LUT exists for the element) or, as a fallback, as an AtomGaussParams via HirshfeldParameters.BuildFromAtomSimple. The log line "Total: STO=N, Gaussian=M" shows how many atoms went through each path.
  4. STEP 3/9 — Building density grid. A cubic grid is created around the target center. Resolution n = ceil(2·(r_max + margin) / voxelSize). Fails gracefully if n³ exceeds Flags.MAX_VOXELS.
  5. STEP 4/9 — Filling density grid. Parallel per-thread accumulation of density contributions. Uses all available processor cores (not ProcessorCount − 1); the atoms are distributed round-robin across the threads so that load is even. The hot loop calls StoDensityLut.SampleSq for STO atoms and HirshfeldExpLut.FastExpNeg for the Gaussian fallback.
  6. STEP 5/9 — Computing w(r). In-place reduction: W = RhoPro / (RhoPro + RhoProm_Outer) with a strict fallback of 0 for empty voxels. The distribution is written to the debug log as w(r) [after compute].
  7. STEP 6/9 — Marching Cubes. Calls HirshfeldMarchingCubes.Extract, which delegates to HirshfeldMarchingCubesGenerator.Extract. It reads the flat W array, subtracts isoLevel inline, and produces a triangle mesh. Duplicate vertices are merged with a 27-neighbour hash grid.
  8. STEP 7/9 — Metadata. Populates TargetMoleculeId, TargetAtomCount, OuterAtomCount, Center, UsedAnisotropic, and calls ComputeBoundingRadius().
  9. STEP 8/9 — Per-vertex properties. HirshfeldProperties.Compute populates DE, DI, DNorm, Charge, Hydrophobicity, and stores the nearest-atom indices and names. The original values are also cloned into DEOriginal, DIOriginal, DNormOriginal.
  10. STEP 9/9 — Post-processing. Wireframe buffer, axes buffer, cell buffer, and view matrix are rebuilt; the MoleculeShift sentinel is updated; the fingerprint window (if open) is refreshed with the new analysis.

Mouse & keyboard

MoleculeShift compensation

The surface vertices are stored in "raw" Cartesian coordinates (no −MoleculeShift). At render time the document subtracts the current MoleculeShift from each vertex, so the surface always aligns with the atoms rendered by the main viewport. When the user changes the presentation mode (and hence the shift), the buffers are rebuilt automatically — the change is detected via the _lastMoleculeShift sentinel field.

7.8. Hirshfeld Pipeline — Computational Classes

The Hirshfeld surface is produced by a self-contained pipeline that does not depend on the main document's presentation state. It is built on top of the same Molecule data used by the rest of Tal, but with its own cell reconstruction step.

HirshfeldScene

Central coordinator of the pipeline. Given a Molecule and a target molecule ID, it reconstructs a compact unit cell, runs the molecule finder, marks the host atoms and collects all outer (guest) atoms within a neighbour cutoff.

HirshfeldUnitCell

Independent representation of the unit cell, built exclusively from Molecule.AsymetricPart and Molecule.SymmetryOperations. It deliberately does not read Molecule.SymetryEquivalents or Molecule.atom[], because those are mutated by the presentation-mode code.

  1. For each atom of the asymmetric part and each symmetry operation, generate a raw fractional position.
  2. Deduplicate against the growing list with a fractional tolerance DEDUP_TOL_FRAC = 0.005.
  3. For each symmetry operation ops[k], k ≠ 0, find the anchor atom (first non-hydrogen of ops[0]) and its counterpart in ops[k]. Compute a single integer translation from the anchor to its counterpart, then apply that same translation to all atoms of ops[k]. This keeps symmetry-generated molecules intact.
  4. Deduplicate again to remove post-translation collisions.
  5. v14: the per-atom Charge and Hydrophobicity values are copied from AtomSimple so they propagate through the whole pipeline.

HirshfeldDetector

Detects the surface and returns a HirshfeldSurface. Its main entry point is GenerateAsync, which drives the nine-step algorithm described above. Important public properties:

MemberDescription
Surface The produced HirshfeldSurface.
Grid The internal HirshfeldGrid with flat W, RhoPro, RhoProm_Outer arrays.
Scene The HirshfeldScene used to build the surface.
BlurSigma, BlurIterations Blur parameters. Both default to 0 and are not exposed in the current UI — the surface is generated without any grid-level blur.

HirshfeldGrid

Wraps a VoxelScanner.VoxelGrid and holds flat density arrays:

float[] W;                // w(r) = rho_pro / (rho_pro + rho_outer)
float[] RhoPro;           // promolecule (target)
float[] RhoProm_Outer;    // surrounding atoms

Indexing is idx = i + Nx·(j + Ny·k) — the deepest loop index k gives sequential memory access. ComputeRelativeDensity is parallelized and iterates linearly, with the historical fallback of 0 for empty voxels.

HirshfeldParameters & HirshfeldAtomicRadii

HirshfeldParameters.BuildFromAtomSimple converts an atom into an AtomGaussParams:

This class is only used by the Gaussian fallback path. When the STO database is loaded and contains an entry for the element, the density comes from the LUT instead, and none of these parameters are involved.

HirshfeldExpLut

Hot loop replacement for MathF.Exp(-x). A static LUT of 4096 entries over x ∈ [0, 25] with linear interpolation. Maximum relative error around 1e-4, invisible in the final rendering. Used only by the Gaussian fallback path.

HirshfeldMarchingCubes & HirshfeldMarchingCubesGenerator

A dedicated copy of Tal's standard Marching Cubes generator, adapted for flat density arrays and Hirshfeld iso conventions. It never modifies the input array; the iso offset is subtracted inline. Duplicate vertices are merged with a 27-neighbour spatial hash with tolerance 0.0001.

HirshfeldProperties

For each mesh vertex, computes:

d_e     = min distance to any outer atom
d_i     = min distance to any target atom
d_norm  = soft-min average of (d_e − r_vdW) / r_vdW  +  (d_i − r_vdW) / r_vdW
charge  = gaussian-weighted average of atom.charge
hydro   = gaussian-weighted average of atom.Hydrophobicity

The d_norm value uses a soft-min over the K = 8 nearest neighbours with a Gaussian weight exp(−β·d) and β = 8. This removes the Voronoi discontinuities that a hard nearest-neighbour approach produces on the surface. The result is clamped to [−3, +3].

The charge and hydrophobicity values use a plain Gaussian average over all target and outer atoms with σ = 0.6 Å; the outer atoms are weighted by an extra factor 0.7. If the source file does not provide these per-atom values, the corresponding surface field is uniform and a warning is logged.

HirshfeldSmoothing

A geometry-only Taubin (λ | μ) smoothing applied to the mesh itself. Unlike a plain Laplacian, the negative μ step compensates for shrinkage, so the surface volume is approximately preserved. The original vertex positions and the adjacency list are cached per HirshfeldSurface. In the current version this class is not wired to a UI slider — it remains available for programmatic use.

HirshfeldMeshRelaxation

An optional pre-color smoothing step that moves vertices tangentially along the mesh and slightly along their normals to remove the "staircase" artefacts of Marching Cubes without losing the physical shape. The maximum displacement is clamped to 5% of the grid unit to avoid overshooting. In the current version this class is not wired to a UI slider — it remains available for programmatic use.

HirshfeldTargetSelector

Simple helper that lists all molecules in a Molecule by their IndividualMoleculeInCell ID. Used by the _cmbMolecule dropdown and by UseCurrentSelection().

7.9. Fingerprint 2D Window (HirshfeldFingerprintWindow)

A separate DockContent that renders a 2D heat map of the d_e × d_i distribution. Each bin corresponds to a small angular sector of the surface; the number of vertices in a bin is mapped to a log-scaled color ramp.

Layout

Side panel — Plot parameters

ControlDescription
_numFontSize Label font size in points. Range 2 – 36, default 4.
_numBinSize Bin size in Å. Range 0.001 – 0.05, default 0.005. Changing it triggers a live re-bucket.
_numRangeMin Axis origin in Å. Range 0.0 – 3.0, default 0.0. Changing it triggers a live re-bucket.
_btnResetOffsets Full reset (identical to RMB double-click): label offsets, font, zoom factor, range, bin size, and marker all return to their original values.

Side panel — Contact decompose

This is the essential feature that makes the fingerprint usable for real chemistry. It consists of a CheckedListBox that lists every contact type present on the surface, a pair of buttons for mass (de)selection, a read-only TextBox with the full decompose table, and a Copy table button.

ControlDescription
_lblDecomposeTitle Section header: "Contact types (check = show):".
_lstContactTypes A CheckedListBox that is populated from analysis.ContactTypePercentages. Each row shows "C···H (12.5%, 2340 v)" — the contact type, its area percentage, and the number of vertices. Rows are sorted by descending percentage. All items start checked, which means no filter is applied: the mesh and the heat map show the full surface. Unchecking a type:
  • grays out the corresponding vertices on the 3D mesh,
  • removes the corresponding vertices from the 2D histogram, so the heat map is rebuilt to show only the remaining types.
_btnSelectAllTypes / _btnClearTypes Quick mass-select or mass-deselect all contact types. Clear selection makes the mesh fully gray and the heat map empty; Select all restores the full view.
_txtDecomposeTable A read-only multi-line text box containing the full decompose table produced by HirshfeldContactDecompose.BuildTable. Columns:
Contact   Area (Ų)      %    Verts
C···H         245.3    18.4      9823
O···H         185.7    13.9      7412
H···H         412.5    30.9     16840
...
The last row is the total. The table can be copied with the button below or with Ctrl+C.
_btnCopyTable Copies the decompose table to the clipboard as plain text.
Interaction between the filter and the heat map: when you uncheck a contact type, the RebuildHeatmapFromRaw method re-reads the raw d_e/d_i arrays and rebuilds the histogram using only the vertices that pass the filter. This is why the 2D map reacts instantly to the checklist — it is not just a color overlay on top of a fixed histogram, the histogram itself is recomputed.

Bin construction & re-bucketing

Rendering

Mouse interaction

GestureEffect
LMB click on the plot Selects a single bin, notifies the document via OnRegionClicked(binDe, binDi), and stores the pixel position for the red marker.
LMB drag on the plot Rubber-band zoom. The selected rectangle is converted into a new [rangeMin, rangeMax] pair and a new bin size.
LMB drag on a label Moves the corresponding label individually. Supported labels: title, d_e (Å), d_i (Å), tick numbers.
RMB click on the plot Places a red marker at the cursor and selects the bin below it (same effect as LMB click).
RMB double-click anywhere Full reset (identical to Reset offsets + zoom).
Wheel Zooms the range around its center by a factor of 0.85 (in) or 1.18 (out). Clamped so the visible range never shrinks below 0.1 Å and never exceeds the original range.

Highlight sync

When a bin is selected the window calls OnRegionClicked(binDe, binDi). The HirshfeldDocument stores _highlightedBin and rebuilds the vertex buffer. Every vertex whose (binDe, binDi) matches is colored bright yellow (255, 255, 0, alpha). When the user changes the bin size or the range, the new bins are pushed back through OnVertexBinsUpdated so the highlight remains correct.

Save PNG

_btnSavePng renders the same content into a 2048 × 2048 RenderTarget2D and saves it as PNG. The label offsets, axis ranges, and highlight state are preserved.

7.10. Surface Data Model and Diagnostics

HirshfeldSurface

The data container for a computed surface. All vertex-based arrays share the same length.

FieldDescription
Vertices, Indices, Normals Mesh data. Normals are recomputed after smoothing/relaxation.
DE, DI, DNorm Per-vertex geometric properties.
Charge, Hydrophobicity Per-vertex chemical properties, computed as Gaussian-weighted averages over the nearby atoms.
ChargeMin, ChargeMax Range of the charge values actually present on this surface. Used for auto-scaling the color map.
HydrophobicityMin, HydrophobicityMax Range of the hydrophobicity values on this surface.
DEOriginal, DIOriginal, DNormOriginal Clones of the raw values, kept for reference.
NearestInnerAtom, NearestOuterAtom Indices into the scene's target/outer arrays.
NearestInnerAtomName, NearestOuterAtomName The names of those atoms (e.g. "O1", "H12").
TargetMoleculeId, TargetAtomCount, OuterAtomCount Metadata captured at generation time.
Center, BoundingRadius Geometric center and enclosing sphere radius.
IsoLevel, UsedAnisotropic Generation parameters.

HirshfeldAnalysisResults

A compact result set that is passed to the fingerprint window. It contains:

HirshfeldAtomDistanceDiagnostics

A diagnostic object built after each generation. It contains:

The diagnostic is shown in the DebugWindow via the Show Diagnostics in Debug… button. The formatting is handled by HirshfeldDiagnosticsFormatter, which builds a list of (text, System.Drawing.Color) lines.

Export formats

Performance tip. A typical organic molecule with ~30 heavy atoms and a voxel size of 0.10 Å produces a grid of ~160³ ≈ 4.1 M voxels. With the default EXP_CUTOFF = 10 the STO LUT extends to ~10 Å per atom, and the whole generation runs in 2–5 s on a modern desktop. Increasing the voxel size to 0.15 Å roughly halves the runtime, and lowering EXP_CUTOFF to 8 Å roughly doubles it again — but the surface quality degrades quickly below EXP_CUTOFF ≈ 8.

8. Powder Diffraction

The Powder Diffraction window simulates the theoretical powder X-ray diffraction pattern for the loaded crystal structure. It is opened from Calculate → Powder Pattern....

The underlying engine (PowderDiffractionEngine) enumerates allowed hkl reflections in the 2θ range, computes the structure factor F(hkl) from the full unit cell, applies Lorentz–polarization and Debye–Waller corrections, groups symmetry-equivalent reflections, and renders the resulting profile in a WinForms plot.

8.1. Window Layout

The window is a DockContent composed of a large plot panel (panelPlot, a BufferedPanel) and a bottom control strip (panelControls) divided into group boxes.

8.2. Controls and Parameters

Range 2θ (groupBoxRange)

ControlDescription
numericMinThetaMinimum 2θ in degrees. Range 0–90, default 5.0.
numericMaxThetaMaximum 2θ in degrees. Range 0–180, default 90.0.

Peak parameters (groupBoxPeak)

ControlDescription
numericFWHMPeak full-width at half-maximum (FWHM) in the units of the X axis. Range 0.01–10, default 0.05.
comboProfileProfile type: Gaussian, Lorentzian, Pseudo-Voigt (default).
numericResolutionNumber of profile sample points per pixel (profile resolution). Range 1–10, default 2.

Debye-Waller (groupBoxDebye)

ControlDescription
numericDebyeWallerDebye-Waller isotropic B factor in Ų. Range 0–20, default 0.5.
trackDebyeWallerSlider linked to the numeric box (value = B × 10). Range 0–200, default 5.

Radiation source (groupBoxSource)

ControlDescription
comboSourceX-ray source. The wavelength is derived automatically from the source (the Wavelength setter is intentionally a no-op to prevent desynchronization).

Available sources (from XRaySource enum):

SourceWavelength (Å)Energy (keV)
Cu Kα1.54068.05
Mo Kα0.710717.44
Co Kα1.78896.93
Fe Kα1.93606.40
Cr Kα2.29105.41
Ag Kα0.560922.10

Display options (groupBoxDisplay)

ControlDescription
chkShowDScaleDraw a secondary top axis in d-spacing (Å) and switch the hover readout to d. Default: off.
chkShowBackgroundFill the area under the pattern curve with a translucent brush. Default: on.
chkLPCorrectionApply Lorentz-polarization correction. Default: on.

Info and actions (groupBoxHover, buttons)

ControlDescription
labelStatusStatus text ("Calculating…", "Calculated N peaks", "No peaks in range", errors).
labelPeakCountNumber of peaks in the current range.
labelHoverInfo2θ or d value under the cursor.
labelHoverDetailNearest peak details: 2θ, d, (hkl), intensity.
btnExportExport dialog (CSV data or PNG/JPEG/BMP image).
btnCopyCopy the current plot bitmap to the clipboard (as bitmap + PNG bytes).

8.3. Mouse Interaction

8.4. Algorithm and Formulas

  1. d-spacing: Computed from the metric tensor of the triclinic cell using the standard reciprocal-lattice formula.
  2. Angle: 2θ = 2 · asin(λ / (2d)), rejected if sinθ > 1.
  3. Centering prefilter: Determined from the first letter of the actual Hermann–Mauguin symbol (P/A/B/C/F/I/R), not from the space-group number. This is only a fast performance filter — systematic absences from screw axes and glide planes are handled naturally by F(hkl).
  4. Structure factor: F(hkl) = Σj fj(s) · exp(−B·s²) · occj · exp(2πi(h·xj + k·yj + l·zj)), summed over all atoms in the unit cell (using SymetryEquivalents if available). The atomic scattering factors fj(s) use the Cromer–Mann 4-Gaussian + constant approximation (International Tables Vol. C, Table 6.1.1.4).
  5. Lorentz-polarization correction: L·P = (1 / (2 sin²θ cosθ)) · ((1 + cos²2θ) / 2).
  6. Intensity: I = |F|² · L·P.
  7. Grouping: Reflections with the same rounded 2θ (4 decimal places) are merged; intensities are summed, multiplicities added. This is valid for every crystal system without hard-coded symmetry tables.
  8. Profile: The rendered profile is a sum of pseudo-Voigt profiles centered at each peak 2θk with the chosen FWHM:
    • Gaussian: exp(−4 ln 2 · x²) where x = (2θ − 2θk) / FWHM.
    • Lorentzian: 1 / (1 + 4x²).
    • Pseudo-Voigt: η · G + (1 − η) · L with η = PseudoVoigtEta (default 0.5).

8.5. Export

The ExportData() dialog offers two paths:

btnCopy places the bitmap on the clipboard as both a DataFormats.Bitmap and a raw PNG stream.

8.6. Settings Persistence

The PowderDiffractionSettings class holds all numeric and boolean values as properties with defaults defined in constants (single source of truth). Clone() and ResetToDefaults() are available for programmatic use. The window synchronizes controls to settings via LoadSettingsToUI() and SaveUIToSettings().

Note: The wavelength setter on PowderDiffractionSettings is a no-op. The wavelength is always derived from the currently selected Source. Code that assigns settings.Wavelength = … is kept for compatibility but has no effect.

9. Crystal Planes & Mean Planes

Tal can define, edit and visualize two kinds of crystallographic planes:

9.1. Planes Window (list manager)

Opened from Calculate → Planes... or from the main Planes toolbar button. It is a DockContent named PlanesWindow, docked left/right or floatable.

Controls

ControlDescription
listViewPlanesDetails-view list of the current document's planes. Each item shows the plane description; the icon is a 16×16 colored square representing the plane color. The item is greyed out when Visible == false.
btnNewPlaneOpens PlanePropertiesWindow for a new plane, shown non-modally (Show, not ShowDialog) so that the 3D view remains interactive for atom picking.
btnEditOpens PlanePropertiesWindow for the selected plane. Enabled only when a row is selected.
btnDeleteAsks for confirmation and removes the plane from the list.
btnCloseCloses the window.

The window maintains a color icon cache (_colorImageCache) keyed by ARGB value, so repeated planes of the same color reuse the same ImageList index.

9.2. Plane Properties Window

PlanePropertiesWindow is a modal-ish dialog (border fixed, center-parent) with a tab control for the two plane types and a common options group.

Tab: Mean plane

When the Mean plane tab is active, the window enters plane selection mode in the main form. A 300 ms timer monitors the selection count and refreshes the label. Atom clicks are routed through HandleAtomPick(int atomIndex) according to the current selection mode:

Tab: hkl

Common options (groupBoxCommon)

ControlDescription
chkShowPlane visibility. Default: on.
chkLabelShow/hide the plane label. Default: on.
txtLabelPlane name. Default "mean:" or "hkl (hkl)" if empty.
chkTransparentDraw the plane with alpha = 0.35 instead of 0.85. Default: on.
panelColorClick to open a ColorDialog (FullOpen). Default red.

On OK, SaveUIToPlane() copies the UI state to the working plane and calls CalculatePlaneGeometry(). The result is exposed via the Result property.

9.3. Mean Plane Calculation

The least-squares mean plane is computed by MeanPlaneCalculator.CalculateMeanPlane():

  1. Collect the selected atom positions in raw Cartesian space. Because Molecule.atom[i].Coordinate is stored with −MoleculeShift applied, the shift is added back so the plane is independent of re-centering.
  2. Compute the centroid.
  3. Build the 3×3 symmetric covariance matrix of the centered positions.
  4. Eigendecompose it with the Jacobi algorithm (max 50 sweeps, ε = 1e-12). Eigenvectors are returned as columns.
  5. The eigenvector with the smallest eigenvalue is the plane normal.
  6. Corners are built by PlaneGeometry.BuildRectangle(center, normal, size), which constructs an orthonormal (u, v) basis inside the plane and returns the four corners center ± u·size ± v·size.

The plane equation coefficients A, B, C, D are stored (Ax + By + Cz + D = 0). The corners are stored in raw Cartesian (without shift), so the draw layer subtracts the current shift at render time.

9.4. hkl Plane Calculation

PlaneGeometry.CalculateHKLPlane() uses the molecule's fractional-to-Cartesian matrix (transposed, to match Molecule.PrepareUnitCell) to convert intercept points into Cartesian space. The intercepts depend on how many Miller indices are zero:

9.5. Plane Intersection

PlaneGeometry.ComputeIntersection(p1, p2) returns a PlaneIntersection struct with a point on the line, the normalized direction, and a half-length. The algorithm:

  1. Direction = n₁ × n₂ (normalized). If nearly zero, the planes are parallel and the result is invalid.
  2. A starting point is found by two Newton-style projections (onto plane 1, then along the projected n₂, then again onto plane 1).
  3. For each plane, the line (point + t·dir) is projected into the plane's local (u, v) basis. The u/v extents are computed by scanning all 4 corners, so any corner ordering works. The permitted t-interval is the intersection of the u and v slab constraints.
  4. The final point is at the midpoint of the interval; the half-length is half the interval length (minimum 0.1 Å). A bounding-box fallback is used if the precise computation fails.

9.6. Ring Finder

RingFinder finds the smallest ring containing a clicked atom:

  1. For each bonded neighbor of the clicked atom, run a BFS back to the clicked atom.
  2. The BFS forbids passing through the start atom as an intermediate and forbids the direct neighbor → start edge (which would give a trivial 2-ring).
  3. Candidate rings shorter than maxRingSize and free of duplicate atoms are tested for planarity by PCA (smallest eigenvalue of the covariance matrix).
  4. The smallest planar ring is returned. If none is planar, the smallest non-planar ring is not returned — the caller falls back to single-atom selection.

9.7. CrystalPlane Data Model

PropertyMeaning
IdGUID.
NameUser-facing name.
TypeMeanPlane or HKL.
ColorSystem.Drawing.Color.
Visible, ShowLabel, TransparentDisplay flags.
SelectionModeFor mean planes: PickedAtom / PickedMolecule / Ring / All / NonHydrogen / Hydrogen.
AtomIndicesIndices of atoms used for the mean plane.
H, K, LMiller indices (HKL planes).
A, B, C, DPlane equation coefficients.
Center, Corners, SizeGeometry.

10. Morphology

Opened from the Advanced menu, this tool is used for analyzing and visualizing the macroscopic crystal habit (morphology) based on the internal crystal structure.

The morphology is predicted using the Bravais-Friedel-Donnay-Harker (BFDH) method, which relates the importance of a crystal face to its interplanar spacing (dhkl) and the lattice geometry. The tool allows you to:

10.1. Morphology Window

The main control window is the MorphologyWindow, a DockContent that can be docked left, right, or floated. It is opened from Advanced → Crystal Morphology.... The window is divided into three vertical zones:

10.2. Calculation Parameters

All parameters are exposed as WinForms controls in the top panel:

ControlParameterDescription
_numMaxIndexMax Miller indexMaximum value of |h|, |k|, |l| considered when enumerating candidate planes. Range 1–6, default 3. Increasing this value generates more candidate faces (and a more complex polyhedron) at the cost of computation time.
_numMinDMin d-spacing (Å)Planes with a d-spacing smaller than this value are discarded before any clipping is attempted. Range 0.1–10.0, default 0.5. This is the primary way to control the number of "small" faces that appear on the crystal.
_numSofteningSoftening strengthControls the amount of compression applied by the selected softening mode. Range 0.05–20.0, default 1.00 (pure BFDH). The meaning of the value depends on the mode (see below).
_cmbSofteningModeSoftening modeDetermines how the relative growth rates are compressed. Options: None (pure BFDH), Clamp, Sqrt, Tanh, Linear. Default: None. Selecting a mode auto-fills the softening strength with a suggested value (see the table in section 10.5).

10.3. Face Table

The face table is a ListView with View.Details, FullRowSelect, and custom owner-drawing (DrawColumnHeader / DrawItem / DrawSubItem). The columns are:

ColumnDescription
FaceThe face name in ASCII (hkl) format (e.g. (100), (1-10), (111)). This column is drawn with a small colored square (12 px) in front of the text; the color is deterministically derived from the face name via MorphologyCrystal.NameToColor (FNV-1a hash of the name mapped to HSV hue).
d (Å)The interplanar spacing of the (hkl) plane.
R (rel.)The relative growth rate, R ∝ 1/d.
DistFracThe fractional distance from the crystal center to the face, after softening. A value of 1.0 corresponds to the largest face; larger values correspond to smaller faces.
MultThe multiplicity of the face, i.e. the number of symmetry-equivalent (hkl) planes in the crystal's space group. This is computed from the real space group operations (not a cubic approximation) via CrystalMorphologyFinder.CountEquivalentPlanesReal, which builds the orbit of the (hkl) vector under the linear part of every symmetry operation and counts the distinct canonical forms.
DescriptionA short human-readable description built from three components: the morphological role (dominant, major, minor, trace), the crystallographic role (basal a/b/c, prism ab/ac/bc, diagonal, octahedral, pyramidal, general), and the relative size (large, medium, small, tiny).

Clicking a row highlights the corresponding face in the 3D view (all triangles with the same FaceName get a yellow outline). This is implemented via MorphologyDocument.HighlightFace(string faceName), which rebuilds a dedicated VertexBuffer (_highlightVB) containing the face's triangle edges.

10.4. The BFDH Algorithm

The BFDH (Bravais–Friedel–Donnay–Harker) method predicts crystal morphology from the crystal structure by assuming that the growth rate of a face (hkl) is inversely proportional to its interplanar spacing dhkl:

RelativeGrowthRate(hkl) ∝ 1 / d(hkl)

Faces with large dhkl grow slowly and therefore dominate the final crystal habit. The algorithm implemented in CrystalMorphologyFinder proceeds as follows:

  1. Plane enumeration: All integer triples (h, k, l) with |h|, |k|, |l| ≤ maxIndex are generated. The trivial (0,0,0) is rejected, as are all triples whose GCD is greater than 1 (they represent the same plane as the primitive triple). This is done by GenerateUniquePlanes.
  2. d-spacing calculation: For each candidate, the d-spacing is computed from the triclinic metric tensor by CalculateDSpacing. Planes with d < minDSpacing are discarded.
  3. Multiplicity calculation: The real multiplicity is computed from the crystal's space group by CountEquivalentPlanesReal, which builds the orbit of (hkl) under the linear parts of all symmetry operations and counts distinct canonical forms. If the space group is unavailable or invalid, the code falls back to a cubic approximation (CountEquivalentPlanesCubic), which is clearly marked as a fallback.
  4. Relative growth rate: RelativeGrowthRate = 1 / d.
  5. DistFrac computation: DistFrac is defined as RelativeGrowthRate / min(RelativeGrowthRate), so the largest face has DistFrac = 1.
  6. Softening (optional): The DistFrac values are compressed according to the selected SofteningMode (see section 10.5).
  7. Relative area: RelativeArea = 1 / DistFrac, normalized to the maximum. Note that this is a visual heuristic — the code explicitly does not multiply by multiplicity, because the physical area of a face depends on the depth of the cut (DistFrac), not on the number of symmetry-equivalent planes.
  8. Sorting: The face list is sorted by DistFrac ascending, so the largest faces come first.
  9. Habitus classification: ClassifyHabitus inspects the relative areas of the {100}, {010}, {001}, {110}, {101}, {011}, and {111} families and returns one of: cubic / octahedral, plate, needle / prismatic, rhombic / dodecahedral, equant (isometric), prismatic, or unknown.
Caching: CrystalMorphologyFinder keeps a single-entry cache keyed by the molecule's cell parameters, space group, and the three calculation parameters. If the same molecule is recalculated with the same parameters, the cached CrystalMorphologyResult is returned immediately. This avoids the expensive enumeration + clipping cycle during UI experimentation. Call ClearCache() to invalidate it explicitly.

10.5. Softening Modes

Pure BFDH (SofteningMode.None) is physically correct but often produces crystals with many very small, almost invisible faces. The softening modes are visual heuristics that compress the DistFrac distribution to produce more aesthetically pleasing or recognizable shapes. They are not physically based models and should be used with that in mind.

ModeFormula / OperationSuggested strengthEffect
Noneidentity1.00Pure BFDH. No compression.
Clampif DistFrac > s: DistFrac = s2.50Hard cap on the growth rate. Small faces are pulled in toward the center.
SqrtDistFrac = sqrt(DistFrac)1.00Square-root compression. Naturally reduces the spread of DistFrac values.
TanhDistFrac = 1 + tanh((DistFrac - 1) · s) / s0.50Hyperbolic-tangent compression. Most flexible mode; the strength parameter controls the sharpness of the transition.
LinearDistFrac = 1 + (DistFrac - 1) / s5.00Linear compression around 1. Preserves relative proportions between faces.

When the mode is changed in the combo box, the numeric up-down is automatically updated to the suggested strength for that mode (via GetSuggestedSoftening). The user can of course override this value.

10.6. Morphology 3D View (MorphologyDocument)

The 3D crystal is displayed in a MorphologyDocument, a DockContent that hosts a MonoGame viewport. It is created automatically the first time "Calculate" is clicked in the Morphology window, and reused on subsequent clicks (the title is updated to reflect the current structure name).

Construction

The polyhedron is built by MorphologyCrystal.Build:

  1. Start with a large cube (ConvexPolyhedronFrac.FromBox) of half-edge 3.0 · scale Å in fractional space.
  2. Sort the surviving faces by DistFrac ascending (largest faces first).
  3. For each face, compute its unit normal in Cartesian space and the scaled plane offset dScaled = DistFrac · ‖g‖ · faceOffset, where ‖g‖ is the norm of the reciprocal-lattice vector of (hkl).
  4. Clip the current polyhedron by the half-space defined by this plane (ConvexPolyhedronFrac.ClipByPlane). The clipping is a Sutherland–Hodgman-style operation on each triangle, followed by a cap construction from the intersection points (a fan triangulation of the cap polygon, oriented to match the plane normal).
  5. If a clip would destroy the polyhedron entirely, it is rolled back. A maximum of MaxSuccessfulClips = 14 successful clips is enforced to keep the polyhedron manageable.
  6. The resulting fractional-space triangles are converted to Cartesian space using the molecule's fractional-to-Cartesian matrix (transposed to match Molecule.PrepareUnitCell).
  7. Face colors are assigned deterministically from the face name (NameToColor) using an FNV-1a hash mapped to HSV. The starting cube's "cell-*" faces are rendered in neutral grey.
If the morphology result is empty or the build fails, the document falls back to MorphologyCrystal.BuildSimpleTest(), which builds a rhombic dodecahedron-like test shape so the user can verify that the 3D viewer itself is working.

Rendering and interaction

Side panel controls

Edge detection algorithm

Edges are drawn only along the true boundaries between two different faces. The algorithm is deliberately careful about numerical robustness:

  1. All triangle vertices are merged into unique positions using a 27-neighbour hash grid with a tolerance EPS_world = BoundingRadius · 1e-4 (in the crystal's original scale, not after any zoom or scale transform).
  2. The set of unique raw edges (pairs of vertex indices) is built from all triangles.
  3. T-junction removal: for each raw edge, all other unique vertices are tested for lying on the segment (parameter t ∈ (tEps, 1 − tEps), perpendicular distance < 4 · EPS_world). Vertices that pass are sorted by t and the edge is split into a chain. This removes the visual gaps that appear at T-junctions where two triangles of the same face share a boundary with a third triangle of a different face.
  4. For each triangle, the chain is walked and each sub-segment is tagged with the face ID of the triangle. If a sub-segment ends up tagged with two or more distinct face IDs, it is drawn once as a boundary edge.
  5. Face IDs are computed by GetFaceId: if the triangle has a non-empty FaceName, that name is used; otherwise a synthetic ID is built from the sign of the triangle normal.

Face highlighting

When the user selects a row in the face table, MorphologyWindow calls MorphologyDocument.HighlightFace(faceName) with the selected face name (or null to clear). This rebuilds the highlight vertex buffer by:

  1. Collecting all triangles whose FaceName equals the requested name.
  2. Merging their vertices into a small local hash grid (tolerance derived from the bounding radius and the current zoom).
  3. Emitting a yellow (255, 255, 0, 255) line segment for every edge that appears in exactly one triangle of the selected face.
  4. The highlight is drawn with DepthStencilState.None so it always appears on top of the crystal, regardless of depth ordering.

10.7. Wulff Plot (Stereographic Projection)

The Wulff plot is opened by clicking the Wulff Plot button in the Morphology window. It opens a WulffPlotDocument, a DockContent that renders a stereographic projection of the crystal faces. This is the standard graphical tool in crystallography for visualizing the angular relationships between faces and the symmetry of the crystal.

Stereographic projection

Each face is represented by its unit normal vector n in Cartesian space. The projection is defined with the +c axis at the center of the plot:

θ = acos(n.z)                    // polar angle from +c
φ = atan2(n.y, n.x)               // azimuthal angle in the ab plane
r = tan(θ / 2)                    // stereographic radius (gnomonic-scaled)
x_plot = r · cos(φ)
y_plot = r · sin(φ)

The factor tan(θ/2) is the standard Wulff-net stereographic projection (equal-angle). Faces close to +c project near the center; faces in the ab plane (θ = 90°) project onto the outer circle. Lower-hemisphere faces (n.z < 0) are mirrored onto the upper hemisphere for display (their z component is negated before projection), and are drawn as hollow circles; upper-hemisphere faces are drawn as filled circles.

Plot layout

Side panel controls

Mouse interaction

The only mouse interaction is the wheel: scrolling up multiplies the zoom factor by 1.15, scrolling down by 0.87. The zoom factor is clamped to 0.3–5.0.

Vertex buffer strategy

Three separate VertexBuffers are maintained and rebuilt whenever the plot needs to be refreshed:

The plot background is taken from Flags.Instance.color1, matching the 3D viewport so that screenshots and the on-screen plot have consistent backgrounds.

11. Structure Overlay

Opened from the Advanced menu, the 🔬 Structure Overlay window is a powerful tool for comparing multiple molecular structures.

Key Features:

This tool is invaluable for comparing different conformations of a flexible molecule, or comparing a calculated structure with an experimental one.

11.1. Overlay Manager Window

The main control window is the MoleculeOverlayWindow (a DockContent named "🧬 Overlay Manager"), opened from Advanced → Overlay Manager.... It can be docked left, right, or floated, and holds an OverlayManager instance that is the shared data model for the whole overlay feature. The window is laid out as three vertical panels:

11.2. The Overlay Data Model

Every entry in the overlay is an instance of OverlayMoleculeData, which holds:

PropertyMeaning
IdUnique integer ID within the overlay, assigned sequentially by OverlayManager._nextId.
NameUser-facing name (e.g. "Molecule 1", "Selection 2", "Structure 3").
SourceNameName of the document the entry was extracted from (used for the tooltip and the source column in the list).
OriginalAtomsImmutable copy of the atoms as they were when the entry was created. This is the "source of truth" — all subsequent transformations, mirror operations, and fits are computed from these.
WorkingAtomsThe current, transformed copy of the atoms. This is what is rendered and what the fitting algorithm modifies in-place.
OriginalReferencePointsArray of exactly 3 Vector3 positions taken from the 3 selected atoms at the time the entry was created. Used as the source basis for fitting.
WorkingReferencePointsCurrent positions of the 3 reference points after transformations and fits.
ReferencePointNamesNames of the 3 reference atoms (for display and debugging).
ReferencePointIndicesIndices of the 3 reference atoms in the source molecule.
HasReferencePointsTrue if 3 reference points were successfully captured.
CurrentTransformationOne of TransformationMode.None, Mirror, RotationY, RotationZ, SymmetryCenter.
ColorThe per-molecule tint applied to all atoms (a Vector3 RGB).
IsVisibleWhether the molecule is rendered in the Overlay View.
IsStructureTrue for entries added via "Structure" (whole crystal).
IsSelectionTrue for entries added via "Add Selection" (an arbitrary set of picked atoms).

The class exposes two important methods: InitializeWorkingData() (deep-copies OriginalAtoms into WorkingAtoms and stores the original ADP matrices) and ResetToOriginal() (reverses the process, restoring the working atoms from the originals while preserving the current color and style).

11.3. The Overlay Manager (OverlayManager.cs)

OverlayManager owns the list of OverlayMoleculeData entries and exposes a small API used by the window. The public surface is:

11.4. Adding Molecules — Workflow

The Overlay Manager exposes three "Add" buttons, each backed by a different method of OverlayManager. All three share the same underlying extraction logic, but differ in which atoms are collected:

  1. Add Molecule (AddSelectedMolecule): collects all atoms of the molecule that contains the currently selected atoms. The molecule ID is taken from IndividualMoleculeInCell. If the source document has no molecule numbering (single-molecule file), all atoms are added as one entry. This is the most common case.
  2. Add Selection (AddSelectedAtoms): collects only the currently selected atoms. This produces an entry marked IsSelection = true and is useful for comparing, e.g., just a ring system or a metal coordination sphere. Note: the current version of the Molecule Overlay window does not expose a button for this method — the button is commented out in the source, but the API remains available for programmatic use.
  3. Add Structure (AddSelectedStructure): collects every atom of the source document's molecule, including all symmetry-generated atoms. This produces an entry marked IsStructure = true. Use this when comparing two whole crystal packings.

In all three cases, the first 3 selected atoms are stored as reference points. If fewer than 3 atoms are selected, the entry is still created, but HasReferencePoints is false and the automatic fitting (section 11.5) is skipped.

11.5. Automatic Fitting (3-Point Rigid-Body Superposition)

The core of the overlay feature is the automatic fitting algorithm, implemented as OverlayManager.IterativeFit(sourceMolecule, sourcePoints, targetPoints). Despite the name "iterative", the current implementation is a single-pass, closed-form rigid-body transformation that maps a source reference frame (defined by the 3 source points) onto a target reference frame (defined by the 3 target points).

The algorithm

  1. Build a local orthonormal basis for the source points:
    xs = normalize(P1_source − P0_source)
    zs = normalize(cross(xs, P2_source − P0_source))
    ys = cross(zs, xs)   // already unit length
    The three vectors (xs, ys, zs) form a right-handed orthonormal frame anchored at P0_source.
  2. Build the corresponding local basis for the target points using the exact same construction (same cross-product order, same sign conventions), anchored at P0_target. Because the construction is identical, the two bases have the same handedness, and the map between them is guaranteed to be a proper rotation (determinant +1, no reflection).
  3. Compute the rigid-body transformation as a change of basis. For any atom at position p (in the source frame), its new position is:
    v = p − P0_source
    a = dot(v, xs), b = dot(v, ys), c = dot(v, zs)
    p_new = P0_target + a · xt + b · yt + c · zt
    This is the classic "re-express in the source basis, then rebuild in the target basis" trick. It implements a pure rotation + translation, with no scaling and no reflection. The three source points are mapped exactly onto the three target points by construction.
  4. Apply the same mapping to every working atom and to the working reference points.
  5. Recalculate bonds via RecalculateBondsForMolecule, which rebuilds the per-atom bond lists, distances, angles, and precomputed bond matrices from the new coordinates. This is necessary because the overlay atoms are rendered with the same bond-drawing code as regular molecules.
  6. Report the fit quality: the initial and final sum of distances between the three source points and the three target points is logged, along with the improvement (in Å and as a percentage). For a successful rigid-body fit, the final error should be 0.0000 Å (to floating-point precision) — any non-zero residual indicates either that the three source points were collinear (in which case the fit is aborted with a "punkty współliniowe" error) or that the fit was called on a degenerate reference frame.
Why "iterative"? The method name reflects an earlier implementation that used a 5-iteration Newton-like refinement. The current version is a single closed-form pass — the name is preserved for API compatibility. The 3-point superposition is mathematically exact, so no iteration is needed.
Degenerate cases: If P1_source == P0_source (or the equivalent for target), the algorithm logs an error and returns without modifying the molecule. If the three points are collinear, the cross product in step 1 has near-zero length, the basis cannot be built, and the fit is aborted. In practice this means you should always pick three atoms that are not collinear — e.g., three atoms of a ring, or an atom plus two of its substituents.

When is fitting triggered?

11.6. Base Molecule and Locked Operations

The first entry in the overlay list is special: it is the base molecule (marked with ⭐ BASE in the list). All subsequent molecules are fitted onto the base. To preserve a stable reference, the following operations are disabled for the base molecule when the overlay contains more than one entry:

The only operation that remains enabled for the base is Color: changing the color of the base is purely cosmetic and does not affect any fit.

When the overlay contains exactly one entry, all operations are enabled for that single molecule (it is not yet "base" in any meaningful sense). When the overlay is empty, all operations are disabled.

11.7. Transformations (Mirror and Inversion)

Before fitting, the user can apply one of three transformations to a non-base molecule. These are exposed as radio buttons in the bottom panel:

ModeOperationEffect
NoneidentityNo transformation. The molecule is fitted as-is.
Mirror(x, y, z) → (x, y, −z)Reflection across the XY plane. Implemented by ApplyMirrorToMolecule(id), which resets the working atoms from the originals and then flips the Z coordinate of every atom and reference point. This is useful for comparing enantiomers or mirror-related conformations.
Inversion(x, y, z) → (−x, −y, −z)Inversion through the origin (a.k.a. symmetry center). Implemented through the generic SetTransformationForMolecule path with TransformationMode.SymmetryCenter, which applies Matrix.CreateScale(-1) to every working atom. Mathematically, inversion is equivalent to a mirror plus a 180° rotation, and since the 3-point fit optimizes rotation, the net effect on the fitted result is often indistinguishable from a pure mirror. The distinction is kept for clarity and for potential future use with non-centrosymmetric reference frames.

Transformation is applied before the automatic fit. The workflow is:

  1. The user selects a transformation radio button.
  2. RbTransformation_CheckedChanged verifies the molecule is not the base (otherwise it restores the previous radio state and shows a "Cannot transform BASE molecule" status).
  3. SetTransformationForMolecule(id, mode) is called. It resets the working atoms from the originals, preserves the current color and style, and applies the appropriate transform matrix.
  4. AutoFitSelectedMolecule() is called, which re-fits the (now transformed) molecule onto the base using the reference points.
  5. Both the list and the Overlay View are refreshed.

11.8. Colors and Styles

Each overlay entry has its own color, chosen from a random hue at creation time (via GetRandomColor, which picks a random hue and converts to RGB with a sine-based palette). The color is applied to every atom's CustomColor field, so the whole molecule is rendered in a single, distinct color.

To change the color of the selected molecule, click the colored button in the top panel (its background is the current molecule color). A standard ColorDialog opens; on OK, the color is applied via SetMoleculeColor and the list is refreshed. Color can be changed even for the base molecule — it is the only operation that remains enabled for the base.

The style of an overlay entry can be changed programmatically via SetMoleculeStyle(id, styleIndex), but this is not currently exposed as a UI control in the Overlay Manager window. The available styles are 0 = CPK, 1 = Sticks, 2 = Balls and Sticks (the default), 3 = Ellipsoids.

11.9. Overlay View Window

The Show button in the Overlay Manager opens (or brings to front) the OverlayViewWindow, a DockContent hosting a MonoGame viewport that renders all visible overlay atoms. The window is subscribed to OverlayManager.MoleculesChanged, so any change to the overlay (add, remove, transform, color change, visibility toggle) is reflected immediately.

Rendering

OverlayViewWindow.UpdateDisplay() collects the WorkingAtoms of every visible entry and passes them to the underlying GameWindow via SetOverlayAtoms(). The atoms are rendered with the same pipeline as regular molecules, so all display styles (CPK, balls & sticks, ellipsoids) and per-atom colors work as expected. Bond detection is done per-molecule (the overlay does not merge bonds across molecules), so an intermolecular contact in the overlay is not drawn as a bond — this is intentional, since the overlay is meant for comparing molecular geometries, not crystal packings.

Lifecycle

11.10. Typical Workflow — Step by Step

  1. Load the base structure. Open a CIF/RES/etc. file. The structure appears in the main 3D view.
  2. Pick 3 reference atoms on the base. Switch to Pick Atoms mode (or use the default rotate mode with Ctrl+click). Click three atoms that define a unique, non-collinear frame — for example, three atoms of a phenyl ring, or a central atom plus two substituents. You should see them highlighted in yellow.
  3. Open the Overlay Manager (Advanced → Overlay Manager...). The list is initially empty.
  4. Add the base as the first entry. Click Add Molecule. The molecule containing the 3 selected atoms is added as the first entry, marked ⭐ BASE. Its 3 reference points are stored.
  5. Load the target structure. Open a second file. The main 3D view switches to the new structure.
  6. Pick 3 corresponding atoms on the target. These must be the "same" 3 atoms as on the base — same chemical environment, same relative geometry. For example, the equivalent three atoms of the same ring.
  7. Add the target. Click Add Molecule again. The new molecule is added to the list and automatically fitted onto the base using the 3+3 reference points. The fit status is shown in the status label ("Fitted to base").
  8. Inspect the overlay. Click Show to open the Overlay View. Both molecules should overlap at the 3 reference atoms; any deviation elsewhere is a real conformational or symmetry difference.
  9. If the fit is wrong, try a mirror. If the two molecules are related by reflection (e.g., enantiomers), select the target entry in the list and click the Mirror radio button. The molecule is re-fitted with the mirror applied first, and should now overlap correctly.
  10. Try a different set of reference atoms if needed. Remove the target entry (❌ Remove), pick a different set of 3 atoms, and re-add. The fitting is deterministic — the same 3+3 pair always produces the same result.
  11. Color and style. Use the color swatch button to distinguish the molecules. Use Show All to toggle visibility of the whole overlay.
  12. Clear when done. Click 🗑️ Clear to remove all entries, or close the Overlay Manager — the overlay state is kept in memory as long as the window is open.

11.11. Limitations and Notes

12. Advanced Tools

12.1. H-Bond Settings

Opened from the menu (or the corresponding toolbar / context command), the H-Bond Settings window controls how hydrogen bonds are detected and drawn in the 3D view.

Contact distance range

Two modes are available:

D–H···A angle

Minimum angle (degrees) — geometry filter. A candidate is accepted only if the angle ∠D–H···A is at least this value (default 63°). Typical strong H-bonds are closer to 180°; lower thresholds also accept weaker / bent interactions.

Options

Donors / Acceptors / Color

Tick which element types may act as donors and acceptors (e.g. N, O, F, P). Only checked pairs are considered. H-Bond color sets the drawing colour in the 3D view (default green).

Note: Atom types may not be classified correctly for non-CSD structures if element assignment in the input file is incomplete or non-standard.

Use Default to restore factory settings, Apply to recalculate without closing the window, or OK to apply and close.

H-bond list (example table)

After detection, the Structure Info / H-bond panel lists each bond in a table. Column meanings:

ColumnMeaning
#Row number.
DonorDonor heavy atom (D), e.g. O1, O2.
HHydrogen atom involved in the bond (if present).
AcceptorAcceptor atom (A), e.g. N1, N2.
D···ADistance donor–acceptor (Å).
H···ADistance hydrogen–acceptor (Å).
H···A − Difference of H···A relative to the VdW (or chosen) criterion (negative = shorter / stronger contact than the reference sum).
∠D–H···AAngle at the hydrogen (degrees).
Intermolecular Yes = different molecules; No = intramolecular.
Symm.op.1 / Symm.op.2 Symmetry operations applied to the first and second partner (e.g. x,y,z, 1−x,1/2+y,3/2−z).
Cell.1 / Cell.2 Unit-cell translations of each partner as (tx, ty, tz), e.g. (0,0,0) and (1,0,1).

Example (illustrative):

# Donor H Acceptor D···A H···A H···A − ∠D–H···A Intermolecular Symm.op.1 Symm.op.2 Cell.1 Cell.2
1 O1 H1 N2 2.769 1.965 −0.785 166.3 Yes x,y,z 1−x,½+y,3/2−z (0,0,0) (1,0,1)
2 O2 H2 N1 2.599 1.869 −0.881 147.7 No x,y,z x,y,z (0,0,0) (0,0,0)

Row 1 is an intermolecular O–H···N bond to a symmetry-related molecule in a neighbouring cell. Row 2 is an intramolecular O–H···N contact within the same asymmetric unit (both partners at x,y,z, cell (0,0,0)).

12.2. Atom Display Settings

Refer to section 4.6 for a detailed description of this window.

12.3. Debug Window

Opened from the Window menu, this window shows a detailed log of symmetry operations found in the loaded structure. It's a diagnostic tool for developers and advanced users to troubleshoot issues with symmetry or file parsing.

12.4. BufferedPanel (helper control)

Tal.Forms.BufferedPanel is a System.Windows.Forms.Panel subclass that enables DoubleBuffered plus AllPaintingInWmPaint, OptimizedDoubleBuffer and UserPaint control styles. It is used wherever smooth custom drawing is required (e.g., the powder diffraction plot).

12.5. Structure Info Window

Opened from Tools → Structure Info. The StructureInfoWindow is a dockable panel (a DockContent) that acts as the central, always-available reference for every numeric and textual property of the currently loaded structure. It replaces the need to dig through the raw file or the debug log to find a specific value: every quantity the parsers extracted (cell parameters, symmetry operations, refinement statistics, atomic coordinates with uncertainties, contacts, H-bonds, measured distances, etc.) is laid out in a clean, scrollable table.

12.5.1. Window Layout

The window is divided into four regions:

12.5.2. Section Buttons and Tables

Each button populates the table with a dedicated Section object (a small inner class that holds a list of column headers and a list of string rows). The current sections are:

ButtonColumnsContent
Structure Property, Value General identification (identifier, literature reference, DOI, publication title, journal, audit creation date/method), chemical metadata (formula, compound name, space group), unit cell parameters with e.s.d.'s (a, b, c, α, β, γ, volume, Z/Z', disorder, polymorph), full refinement statistics (R1 observed / all, wR2 all / observed, GooF, reflections used, parameters refined, restraints, shift/su max and mean, Δρ max/min/RMS, weighting scheme), reflection statistics (Reflns total, observed, threshold, resolution d_min and d_max, θ_min, θ_max), and experiment details (F(000), wavelength, radiation type, measurement device, temperature, crystal colour, crystal shape, crystal size, absorption coefficient, absorption correction type, absorption T_min/T_max, density, R-factor if only the legacy value is present). Fields that are not present in the source file are simply omitted from the table.
Diagram Message Placeholder for a future graphic schematic of the structure.
Atoms Name, El, x, y, z, σx, σy, σz, Occ, Ueq One row per atom in the asymmetric unit. Coordinates are fractional. Ueq is computed on the fly as the trace of the anisotropic Ue matrix divided by 3.
Bonds #, Atom1, Atom2, Length (Å), Error, Occ Every unique covalent bond, deduplicated via a sorted (i, j) pair set. The error column is an e.s.d. estimate computed from the two atoms' Coordinate_error vectors projected along the bond direction (see 12.5.3). Occupancy is the minimum of the two atoms' occupancies.
Contacts #, Atom1, Atom2, Length, Length-VdW, Symm.op.1, Symm.op.2, Expanded, X1, Y1, Z1, X2, Y2, Z2, Xfrac1, Yfrac1, Zfrac1, Xfrac2, Yfrac2, Zfrac2 Every unique short contact detected by the current Define Short Contacts settings. The Length-VdW column is d − (r_vdw(1) + r_vdw(2)) — negative values indicate closer-than-van-der-Waals contacts. Symmetry operation strings include the cell shift (e.g. x+1,y,z). Rows where Cell.1 or Cell.2 is non-zero are given a warm background to draw attention to the neighbouring cell.
H-Bonds #, Donor, H, Acceptor, D⋯A (Å), H⋯A (Å), Lenght-VdW, ∠D-H⋯A (°), Intermolecular, Symm.op.1, Symm.op.2, Cell.1, Cell.2 Every unique D-H⋯A contact. The donor is identified by scanning for a bonded neighbour with Z ∈ {7, 8, 9} (N, O, F); the acceptor comes from the H_BondedAtom list. The H⋯A − VdW column is d(H⋯A) − (r_vdw(H) + r_vdw(A)). The Intermolecular flag is computed as (cell shifts differ) OR (individual molecule indices differ).
Centroids Message Placeholder for a list of calculated ring/molecular centroids.
Planes #, Type, hkl / Info, A, B, C, D Every plane currently registered in the active document. For HKL planes the info column shows hkl (h k l); for mean planes it shows the selection mode's description.
Symmetry Number, Symm. Op., Description, Detailed Description, Order, Type The full list of symmetry operations for the space group, parsed and formatted as fractional-coordinate strings (e.g. -x,1/2+y,1/2-z). Each operation is classified (identity, inversion centre, 2-fold rotation, general) by counting the sign flips in the linear part.
Distances #, Atom 1, Atom 2, Distance (Å), Error Every measurement of type 1 (atom-atom distance) and type 4 (atom-plane distance) currently registered by the Measure tool. The arrow (→plane) is used in the second column to indicate atom-plane measurements.
Angles #, Atom 1, Atom 2, Atom 3, Angle (°), Error Every A-B-C angle derived from the covalent bond graph: for each atom B with at least two bonded neighbours, every unordered pair (A, C) of its neighbours defines an angle. Duplicates are removed via a sorted triple key. The list is capped at 50,000 entries to keep the UI responsive on very large systems.
Torsions #, Atom 1, Atom 2, Atom 3, Atom 4, Torsion (°), Error Every A-B-C-D torsion derived from the covalent bond graph: for each covalent bond B-C, every pair (A, D) of neighbours of B and C (excluding the bond partners themselves) defines a torsion. The dihedral angle is computed with the standard atan2-based formula (see 12.5.4). The list is capped at 50,000 entries.
All Angles #, Atom 1, Atom 2, Atom 3, Angle (°), Error Placeholder for a more exhaustive angle list (including non-bonded atoms).
All Torsions #, Atom 1, Atom 2, Atom 3, Atom 4, Torsion (°), Error Placeholder for a more exhaustive torsion list.

12.5.3. Error Estimation

For every geometric quantity, the window reports an estimated standard uncertainty (e.s.d.). The formulas are consistent with the ones used by the Measure tool, and propagate the per-atom positional errors stored in Coordinate_error (which already include the cell-parameter contribution):

When an error is too small to be meaningful (or the source file did not provide positional errors), the cell shows an em-dash (—) instead of a number.

12.5.4. Torsion Angle Formula

The dihedral angle A-B-C-D is computed by:

a = A − B
b = C − B
c = D − C
d = B − C

n1 = normalize(cross(a, b))
n2 = normalize(cross(c, d))

angle = acos(clamp(dot(n1, n2), -1, 1))

if dot(cross(n1, n2), normalize(b)) < 0:
    angle = −angle

The sign convention matches IUPAC: a positive torsion is a clockwise rotation of the far bond when looking along the central bond from B to C. The result is in radians internally, and converted to degrees for display.

12.5.5. Dynamic Behaviour

12.5.6. Number Formatting

The window uses the classic crystallographic "value(esd)" notation. For example, a cell parameter a = 5.6789 with a_e = 0.0012 is rendered as 5.6789(12). The number of decimal places is chosen from the magnitude of the e.s.d.: if error < 1, the number of decimals is the position of the first significant digit of the error. Volume is computed from the cell parameters and its e.s.d. is propagated analytically from the partial derivatives with respect to a, b, c, α, β, γ.

12.5.7. Source of the Populated Data (CIF / RES)

All quantities displayed by the Structure Info Window are extracted by the parsers from the source file. The mapping is fully deterministic — a value appears in the table only if the corresponding field is present in the CIF (or RES/INS) file. Missing fields are silently omitted; the window never invents data.

Cells, symmetry and atoms
Refinement statistics
Reflection statistics
Experiment
Audit and publication
Where the values come from: the parsers (CifParser, ResParser) fill a CifData / ResData container. StructureStorage.ConvertCifToStructure / ConvertResToStructure then copies every present field into a StructureData, and finally Molecule.CopyMetadataFromStructure / Molecule.CopyMetadataFromRes mirrors the values into the live Molecule object. The Structure Info Window reads its data exclusively from that Molecule instance — i.e. from the parsed CIF/RES file, never from a runtime-generated value. Anything the file did not contain is not shown.

13. Export & Format Integration

13.1. StructureWriter — dispatcher

StructureWriter.Save(structure, path) inspects the file extension and dispatches to the appropriate writer. Returns true on success. Exceptions are caught and logged.

13.2. CifWriter

Writes a StructureData to a CIF file using the IUCr data-name rules. It emits: data_ block, audit lines, chemical metadata, cell parameters (with esd where available), _space_group_IT_number, H-M name, symmetry-operation loop (never guessed — via SymmetryHelper.GetOperationStrings), atom-site loop (label, type, fract coords, occupancy, U_iso, adp_type, optional disorder group), and an anisotropic displacement loop when any atom has AdpType == "Uani".

Number formatting uses CultureInfo.InvariantCulture. The file is written as UTF-8 without BOM.

13.3. ResWriter

Writes a SHELX-format .res file: TITL, CELL, ZERR, LATT, SYMM cards (identity excluded; inversion sign of LATT inferred from SymmetryHelper.ContainsInversion), SFAC/UNIT, atom lines, HKLF 4, END. Occupancy is written as occ + 10 (SHELX free-variable convention). Anisotropic atoms emit the full 6-element U line with the second-line convention.

13.4. XyzWriter

Writes atom count, comment line (structure name), and one Element x y z line per atom. Coordinates are written as stored (Cartesian for non-crystal formats). UTF-8 encoding.

13.5. PdbWriter

Writes HEADER, TITLE, an optional CRYST1 record (when a cell is present, with the H-M name truncated to 11 chars and the space-group number), one HETATM record per atom (serial, 4-char name, "UNK" residue, residue 1, x/y/z, occupancy, B-factor derived from U_iso as B = 8π²·U, element), and END.

13.6. MolWriter

Writes an MDL molfile V2000: header (title truncated to 80 chars, " TAL 3D", blank), counts line (atoms, bonds = 0), atom lines in fixed format (%10.4f%10.4f%10.4f %-3s + zeroed flags), and M END.

13.7. Mol2Writer

Writes a Tripos MOL2 file with @<TRIPOS>MOLECULE, @<TRIPOS>ATOM, and optionally @<TRIPOS>CRYSIN when a cell is available (a, b, c, α, β, γ, space-group number, setting 1). Charges are not written (charge type NO_CHARGES).

13.8. GaussianWriter

Writes a Gaussian input file with a %chk= link, a #p B3LYP/6-31G(d) Opt route, the structure title, a default charge/multiplicity line 0 1, and the Cartesian atom block. ASCII encoding.

13.9. CrystalWriter (.d12)

Writes a CRYSTAL17 geometry + SCF input deck. The writer performs the non-standard-setting check via SpaceGroupTransformer.GetTransformToStandard() and, on user confirmation, transforms a deep copy of the structure (the original in StructureStorage is untouched). The output includes the title line, CRYSTAL, the geometry flag 0 0 0, the space-group number, the cell line, the number of atoms, and one atom line per atom (Z, x, y, z, optional label). The settings block emits OPTGEOM / FULLOPTG / ENDOPT, BASISSET POB-TZVP, DFT B3LYP-D3 XLGRID, SCFDIR, memory sizes, SHRINK 8 8, LEVSHIFT 6 1.0, and TOLINTEG 7 7 7 7 25.

CrystalSpaceGroupValidator.Validate(structure) returns a warning string when the structure is not in the standard setting; otherwise null. The comparison is always done on the normalized H-M name.

13.10. PovRayGenerator

PovRayGenerator(Molecule) produces a complete POV-Ray 3.7 scene file.

Scene structure

Safety guards

Contrast / saturation constants

ConstantValueEffect
_globalBrightness1.00Global light scale.
_lightingContrast0.80Controls ambient/diffuse/phong factors.
_phongBoost2.70Multiplier on the phong value.
_saturationBoost2.30Color saturation boost (around the luminance axis).
_emissionStrength0.18Self-illumination fraction of the base color.

13.11. SpaceGroupTransformer

Transforms a StructureData from one setting of a space group to another equivalent setting (e.g. P2₁/c ↔ P2₁/n).

Public API

Algorithm

  1. Resolve the source and target operation sets (ReadSymm) from the internal SymmetryElements table.
  2. If the sets are already equivalent, return null (nothing to do).
  3. Try a hard-coded transformation; verify it by conjugating the source set and comparing with the target set (3 test points, tolerance 1e-3).
  4. If verification fails, brute-force over all integer matrices with determinant ±1 (cached) and a small set of candidate shifts (0, ¼, ½). The first matrix that maps the source set onto the target set is used.
  5. Apply the transform to the atoms (optionally wrapping to the unit cell), rotate the ADP tensors, update the cell by M' = M · P where P = Q⁻¹, regenerate the symmetry operations, and set the new name.

Refinement after transform

RefineMoleculePositionsAfterTransform(structure, bonds, maxIterations) performs an iterative relaxation of atoms around their covalent bonds (minimum-image convention), followed by an "attractor" step that pulls smaller connected fragments toward the largest one using integer fractional shifts of up to ±2. Finally, each molecule is translated so that its heaviest non-hydrogen anchor sits near (0.5, 0.5, 0.5) — a whole-molecule translation, not a wrap.

Helper methods: DetectBondsInStructure (minimum-image covalent-bond detection with element-pair thresholds), DetectMoleculesSimple (BFS over bonds), and VerifyRoundTrip (round-trip diagnostic for A→B→A).

13.12. Parsers

The application exposes a set of parsers, each of which populates a data container and then converts to a StructureData:

ParserContainerNotes
CifParserCifDataFull IUCr CIF: cell (with esd), symmetry (operations + group name/number, with fallback detection from the operation set), chemical metadata, refinement, reflections, experiment, publication. ADP (Uiso/Uani) and disorder are handled. ParseMultiBlock returns one CifData per data_ block.
ResParserResDataSHELX RES/INS. Merges continuation lines, parses TITL (extracts the space group via multi-pattern regex), CELL, ZERR, SYMM, SFAC (with optional scattering coefficients), FVAR, PART, TEMP, and REM lines (R1, wR2, GooF, parameters, restraints, Δρ, F(000), shift/su). Uses a two-pass symmetry strategy: validate file operations against the internal database and prefer the database when the file is incomplete. Second pass: recalculates U_iso for geometrically placed hydrogens with AFIX multipliers as U_iso(H) = |multiplier| · U_eq(neighbor), searching within 1.3 Å using the minimum image convention.
XyzParserXyzDataStandard XYZ: atom count, comment, element + Cartesian coordinates + optional partial charge.
PdbParserPdbDataCRYST1, ATOM/HETATM, ANISOU (converted from 10⁻⁴ Ų to Ų), CONECT. Cartesian coordinates are converted to fractional when a cell is present.
MolParserMolDataMDL molfile/SDF V2000. Fixed-width and space-separated fallbacks. Bond block and MDL charge codes. V3000 is detected but not fully supported.
Mol2ParserMol2DataTripos MOL2 with MOLECULE, ATOM, BOND, and CRYSIN sections. Sybyl atom types are converted to element symbols. Cartesian coordinates are converted to fractional when CRYSIN is present.
GaussianParserGaussianDataGaussian input: link0 lines, multi-line route section, title, charge/multiplicity, Cartesian geometry. Numeric element tokens, ghost atoms (X, Bq, XX, GH) and freeze flags are handled. Bohr coordinates are converted to Å.

All parsers call a Clean() method on their container before populating it, and set a completed flag on success.

13.13. CifDivider

CifDivider.Divide(content, filePath) splits a multi-block CIF into a list of CifBlock objects (Name, RawData). It preserves empty lines to keep line numbering intact, scans for lines whose trimmed form starts with data_, and falls back to a single block named after the file when no data_ directive is found.

13.14. RecentFilesManager

Singleton that maintains the recent-files list persisted in %LocalAppData%\Tal\RecentFiles.json. See section 2.2.

13.15. Complex (Tal.Utils)

A lightweight struct Complex (double-precision) used by the powder-diffraction structure-factor calculation. It provides the standard arithmetic operators, Magnitude, MagnitudeSquared, Phase, Conjugate, and a FromPolar factory. The BCL System.Numerics.Complex is intentionally not used to avoid name clashes and to keep the operations minimal.

14. Appendix: Technical Details

14.1. Rendering Pipeline

Tal uses a modern rendering pipeline with the following key stages:

14.2. Supersampling (SSAA)

Supersampling is the primary method of anti-aliasing in Tal. By rendering the scene at a higher resolution (e.g., 2x, 4x) and then scaling it down, the jagged edges of polygons are smoothed. The SSAASamples flag determines the multiplier. A value of 2 means the scene is rendered at 2x resolution, a value of 4 at 4x, etc. This technique provides excellent image quality, especially for thin features like bonds and ellipsoid disks.

14.3. Progressive Accumulation AA

This technique builds upon SSAA. When the user is not interacting with the scene (i.e., the camera is not moving), the application renders the scene multiple times with a sub-pixel jitter on the camera's projection matrix. The results of these jittered frames are accumulated (averaged) into a single buffer. This effectively multiplies the number of samples per pixel, producing an extremely smooth, high-quality final image. The number of accumulated frames is controlled by the ProgressiveAASamples flag (e.g., 16 frames). This method is perfect for producing publication-quality screenshots.

14.4. Van der Waals Radii and Color Tables

The VdwParameters static class is the single source of truth for all van der Waals radii and the default color palette used in the application. It contains a comprehensive, hardcoded table of radii for elements 1 through 118, along with a default color for each. The Atom Colors (global)... window allows the user to modify the color palette at runtime, and these overrides are stored in the application's settings file (Flags) and applied on startup.

14.5. Settings and Persistence

All application settings, window layouts, and user preferences are managed by the Flags singleton. Key settings are saved to a JSON file on application exit and loaded on startup. This includes window positions, lighting settings, background colors, atom color overrides, and camera parameters. The Flags.Instance.MarkDirty() method is used to flag that settings have changed and need to be saved.

14.6. Camera and POV-Ray FOV Conversion

XNA's Matrix.CreatePerspectiveFieldOfView takes a vertical field of view. POV-Ray's angle parameter is the horizontal angle. The conversion used by PovRayGenerator is:

halfV = radians(fovVertical) / 2
halfH = atan(tan(halfV) * aspect)
angle = degrees(2 * halfH)

The POV-Ray right vector must remain constant (1.0) — POV-Ray scales the image itself from the render resolution (+W/+H), and setting right = aspect would squeeze the image horizontally. The render resolution must therefore have the same aspect ratio as the TAL viewport.

14.7. Powder Diffraction: Cromer–Mann Form Factors

Atomic form factors in StructureFactorCalculator use the 4-Gaussian + constant approximation from International Tables for Crystallography, Vol. C, Table 6.1.1.4:

f0(s) = Σ aᵢ·exp(−bᵢ·s²) + c      (i = 1..4)
s = sinθ / λ

Coefficients are stored as 9-element arrays { a1, b1, a2, b2, a3, b3, a4, b4, c } keyed by atomic number. Elements without tabulated coefficients fall back to a carbon-like default.

14.8. Hydrogen U_iso Recaclulation (SHELX AFIX)

In SHELX RES/INS files, a negative U_iso value on a hydrogen atom encodes an AFIX multiplier: the hydrogen's U_iso is to be derived from its bonded neighbor's equivalent isotropic displacement as

U_iso(H) = |AFIX| · U_eq(neighbor)

ResParser performs a second pass over the parsed atoms. For each hydrogen with afix_multiplier > 0, it finds the nearest non-hydrogen atom within 1.3 Å (minimum-image convention, using the cell's fractional-to-Cartesian matrix), computes U_eq = (U11 + U22 + U33) / 3 for anisotropic neighbors or uses U_iso for isotropic ones, and writes the resulting value into iso. Hydrogens without an AFIX multiplier keep the value given directly by SHELX.

15. Advanced Cavity Analysis (VdW Scaling)

The VdW Scaling analyzer performs a systematic sweep of cavity detection at multiple van der Waals radii. For each scale it collects geometric, electrostatic and channel information, and then derives a set of QSPR descriptors that summarize the structural stability of the framework, its porosity and its potential for host-guest chemistry. This is the tool to reach for when you want to move beyond a single "static" cavity picture and characterize a porous material.

15.1. How to run the analysis

  1. Open a crystal structure (CIF, RES, etc.) so that a valid unit cell with symmetry is available.
  2. Run at least one normal cavity detection first (Calculate → Cavity Detection) so that the scanner has generated its atoms, its spatial index and at least one density grid. The VdW analysis re-uses the scanner and re-runs the SDF computation for every scale, but it needs a fully initialised scanner and a valid cell.
  3. Invoke the analysis programmatically via VdWScaleAnalyzer.RunAnalysisAsync(molecule, detector, scales) (wired from MainForm.RunVdWAnalysisAsync). In the current UI the entry is not a separate top-level Calculate menu item; single-run cavity reports still refer to it as “Calculate → VdW Scaling Analysis…”. Prefer launching it from the cavity workflow after a successful detection, or from a debug/status path that calls RunVdWAnalysisAsync.
  4. Optionally provide a custom scale list. If none is given, the default list is used:
    2.00, 1.95, 1.90, 1.85, 1.80, 1.75, 1.70, 1.65, 1.60, 1.55,
    1.50, 1.45, 1.40, 1.35, 1.30, 1.25, 1.20, 1.15, 1.10, 1.05, 1.00
    Larger scales first (aggressive shrinking of the pores), then down to 1.00. This ordering is important for the "collapse scale" descriptor — the analyzer walks from the most aggressive scale down to the physical one, and records the first scale at which total cavity volume drops below the collapse threshold.
  5. Watch the progress events (ProgressChanged, StatusChanged) or the status bar. Each scale reports its cavity count and (when available) the largest fitted sphere.
  6. When the loop finishes, the analyzer restores the original VdW multiplier and SDF flag state, and stores the VdWAnalysisResults object in VdWScaleAnalyzer.CurrentResults. The report can be saved as TXT or HTML.
How the per-scale SDF is computed. There is no cached "base SDF" that gets shifted. Every scale in the sweep is computed from scratch for that exact VdW multiplier and probe radius. This is because the effective radius of every atom depends on its own element:
r_eff(i, mult) = r_vdw(i) · mult + ProbeRadius
and the difference r_eff(i, mult) − r_eff(i, mult') is r_vdw(i) · (mult − mult') — different for every element. The SDF cannot be obtained from another multiplier by a single constant shift; it has to be recomputed. The sweep is still fast because the scanner's atom cache and spatial index are reused across scales — only the SDF hot loop runs again.

15.2. VdWAnalysisResults — data model

The result object is the single source of truth for the report. Its main fields are:

FieldDescription
VdWScalesList of scales that were actually processed.
CavityResultsDictionary<int cavityId, List<VdWAnalysisResult>> — one VdWAnalysisResult per (cavity, scale).
CavityTracksDictionary<int cavityId, CavityTrackData> — a per-cavity summary of volume evolution across scales.
CriticalEventsList of CriticalEvent (disappearance, channel open/close, topology change) with the scale at which they occur.
CrystalChannelDataDictionary<int cavityId, Dictionary<string dir, ChannelPathResult>> — semi-periodic channels detected at VdW = 1.0.
SpherePackingDictionary<int cavityId, SpherePackingResult> — the multi-sphere packing summary of every cavity.
CellVolume, VoxelVolume, AtomCount, CellMassCell parameters captured before the run.
StartTime, EndTime, TotalProcessingTimeMsTiming information.
FragmentationIndexQSPR I_frag — see section 15.5.
CollapseScaleQSPR f_collapse — the VdW scale at which the total cavity volume drops below 0.1% of the cell volume.
ElectrostaticFrustrationQSPR Ω_Q — see section 15.5.
HydrophobicFrustrationQSPR Ψ_H — see section 15.5.
MeanPolarizabilityQSPR ⟨Z_cav⟩ — mean Z-effective of all cavity voxels.
MaxDipoleSensitivityQSPR α_polar — maximum d|dipole|/d(scale).

15.3. Per-cavity snapshot (VdWAnalysisResult)

For every (cavity, scale) pair the analyzer stores both geometric and electrostatic data in a single VdWAnalysisResult:

The most important derived quantity is VolumeToSurfaceRatio (V/S), which behaves like a generalized radius: it grows with the cavity size and shrinks when a cavity becomes rough or fragmented.

15.4. Cavity tracking and critical events

After the sweep, the analyzer builds a CavityTrackData for every cavity ID it saw at least once. The tracking object contains one CavitySnapshot per scale, with the volume/surface/electrostatic data frozen at that point. The DisappearanceScale is the first scale at which the volume drops below EPSILON after having been positive earlier.

CriticalEvent records all interesting transitions:

15.5. QSPR descriptors

Six descriptors summarize the whole sweep:

DescriptorFormula (informal)Interpretation
I_fragN_cavities_max / N_atomsStructural fragmentation. Large values (> 0.5) suggest a highly porous, "beads-on-a-string" framework.
f_collapseFirst scale s at which the total volume fraction drops below 0.1%.Structural stability. Larger values imply the cavities survive at more aggressive VdW radii.
Ω_QΣ Δs · ⟨Var_Q(s)⟩ · ⟨V(s)⟩Electrostatic frustration: how heterogeneous the charge distribution is across the whole porous network.
Ψ_HΣ Δs · ⟨Var_H(s)⟩ · ⟨S(s)⟩Hydrophobic frustration: how heterogeneous the hydrophobicity is on the cavity surface.
⟨Z_cav⟩Mean of MeanZEffective over all cavity snapshots (non-duplicates).Polarizability capacity. Larger values correlate with stronger dispersion interactions with guests.
α_polarmax over scales of (dipole(si+1) − dipole(si)) / ΔsResponsiveness to an external electric field.

15.6. Interpreting the shape of the V(mult) curve

The VdW sweep is not just a way to compute f_collapse. The shape of the total volume curve V(mult) is itself diagnostic and allows you to distinguish qualitatively different kinds of cavities:

Type of cavityκ = V(1.5) / V(1.0)Shape of V(mult)Physical interpretation
Large oval cavity > 0.5 Smooth, close to (R_0 − δ·mult)³ Well-packed framework, cavity survives even at aggressive VdW radii. Stable, good host candidate.
Medium cavity 0.2 – 0.5 Moderate cubic decline Real cavity, limited selectivity, likely stable.
Narrow slit 0.05 – 0.2 Steep, almost linear in the last part Thin gap between molecules, weak contacts, structure may be fragile with respect to T or P.
Sub-VdW slit < 0.05 Vertical drop, cavity disappears before mult = 1.05 Numerical artefact or extremely weak contact. Structure likely has non-optimal packing.

The volume of a spherical cavity of radius R scales as V ∝ R³. The VdW multiplier shrinks R by δ = (mult − 1) · r_vdw, so the expected profile for a compact cavity is a cubic polynomial in mult. Deviations from that cubic — in particular a sharp drop followed by a long tail — indicate that the cavity is not a compact object but a network of narrow necks. This is exactly the kind of qualitative distinction that the VdW sweep is designed to make visible.

Relationship to packing theory. The idea of using the slope of V(mult) as a measure of packing tightness is closely related to the classical packing coefficient k = V_molecules / V_cell of Kitaigorodskii, and to the topological analysis of molecular coordination numbers by Peresypkina and Blatov (Acta Cryst. B56, 501–511 and 1035–1045, 2000), who showed that organic molecules pack with an average molecular coordination number of 14 rather than the 12 predicted by the hard-sphere model. The VdW sweep in Tal is a dynamic extension of that idea: instead of a single packing coefficient, it measures how quickly the free volume collapses when the atomic radii are inflated.

15.7. Sphere packing analysis (SphereFitAnalyzer)

For each cavity detected at VdW = 1.0 the analyzer builds a SpherePackingResult. The sphere fits themselves are found by SphereFitAnalyzer, which operates entirely on the Cavity object and does not need a graphics device.

Small cavities

If the cavity has fewer than MIN_VOXELS_SMALL = 50 voxels, the analyzer runs a FindSingleMaximum routine: a coarse grid scan (step = min(resX,resY,resZ)/20) locates the maximum of the SDF, then a local refinement over a radius of 2·step voxels finds the true apex. A single SphereFitResult is created with MaxSphereRadius = SDF_max.

Large cavities

For larger cavities the analyzer enumerates all local maxima of the SDF in parallel and then selects a non-overlapping subset:

  1. FindLocalMaximaParallel: scan with step = min(resX,resY,resZ)/15, test the 6 face neighbours, and refine each candidate over a 3-voxel cube.
  2. Filter: discard every local maximum whose SDF is below MIN_SDF_VALUE_LARGE = 0.5 Å.
  3. SelectSpheres: sort by descending SDF; add a candidate to the selected set only if its distance to every already-selected sphere is at least max( (rcand + rsel) · SEPARATION_FACTOR / 10, MIN_DIST_FRAC = 0.05 ) in fractional coordinates. This yields a maximal packing of spheres.
  4. Result: one SphereFitResult per selected sphere, with the cavity ID, sphere radius, sphere center, fractional coordinates, wall flag, and distance to the cavity center.

SpherePackingResult

FieldMeaning
MaxSphereRadius, EffectiveDiameterRadius and diameter of the largest fitted sphere.
SphereCountNumber of non-overlapping spheres.
SpheresList of PackedSphere — one per fitted sphere, with radius, center, fractional coords, wall flag and distance to the cavity center.
UsableVolumeSum of the volumes of all fitted spheres.
PackingEfficiencyUsableVolume / CavityVolume (values around 0.74 indicate a close-packed cavity).
HasChannels, ChannelInfoChannel state at VdW = 1.0.

15.8. Gas compatibility

For every cavity the analyzer cross-checks the fitted spheres against a fixed library of gases. Each gas is a GuestMolecule with a kinetic diameter, polarizability, dipole moment, and a flag IsGas = true. The library includes:

For each gas the result is a GasGuestResult:

15.9. Solvent compatibility

The same library (with IsGas = false) is used to score solvents. For each solvent the analyzer builds a SolventGuestResult whose score combines four indicators:

The final score is a weighted sum (0.25 size + 0.15 polar + 0.15 charge + 0.15 hydro + 0.10 shape + 0.10 voxel) and VoxelClusterMatch is a derived 0.5–1.0 confidence that the solvent fits the shape of the cavity's voxel cloud, not just the largest sphere.

15.10. Semi-periodic channels (CavityPathFinder)

Channel analysis is run at VdW = 1.0 for every cavity that touches at least one cell wall (WallConnections > 0). The CavityPathFinder performs a semi-periodic A* search along the three cell axes and stores its results in cavity.CrystalChannelResults.

Path result

FieldMeaning
IsConnectedWhether a path through the periodic image was found.
TotalLength, OriginalLength, SmoothedLengthPath lengths in Å.
SmoothingError, SmoothingErrorPercentRMS of the smoothing displacement.
AverageWidth, MinWidth, MaxWidthLocal diameter, sampled along the path.
EffectiveDiameterAverageWidth · 0.8 — a conservative estimate of the "clear" diameter.
HelicitySigned angle change of the path direction. Small values (≈ 0°) indicate a straight channel; large values indicate a helical channel.
CellCrossingsNumber of times the path crosses a cell boundary.
WorldPath, WorldPathXnaPath vertices in Å (System.Numerics and XNA variants).

Algorithm

  1. Setup: the density grid, the periodic grid and the inverse fractional-to-Cartesian matrix are flattened into contiguous arrays. The maximum SDF value inside the cavity volume is recorded for the heuristic.
  2. Start points: up to MaxStartPoints = 20 candidates are enumerated in expanding shells around the center of the periodic cell. A candidate is accepted only if the local voxel is not a wall and has at least 2 units of free space in the 6 axis directions.
  3. A* search: for each start point, two separate A* searches are run — one toward the minimum wall and one toward the maximum wall. Each search uses a 26-neighbour stencil and a cost function distance + distance · (1 − SDF / SDF_max) · SDFWeight so that the path prefers to stay near the center of the cavity.
  4. Path assembly: the two half-paths are concatenated. Paths with fewer than 3 points are discarded.
  5. Measurement: total length, helicity (angle between the first and second half), cell crossings, and the diameter at each of up to 20 sample points.
  6. Smoothing (optional): a Laplacian smoothing pass (iterations controlled by SmoothingIterations, default 3). The RMS displacement is reported as SmoothingError.
  7. Statistics: the analyzer aggregates the results of all start points into a ChannelPathStatistics object (min/max/avg length, diameter, helicity).
  8. XNA conversion: WorldPathXna is filled from WorldPath so the renderer can consume it directly.

15.11. Reports

VdWAnalysisReport.GenerateTxtReport(results) produces a plain-text report; GenerateHtmlReport(results) produces a self-contained dark-theme HTML report; SaveReport(results, path) dispatches to the right one by extension. Both reports are composed of the same sections:

16. Atom Visibility Manager

The AtomVisibilityManager is a small static helper class in Tal.DrawMethods that centralizes the "show / hide" operations for atoms. It is not bound to any particular tool window; any command that needs to hide or show atoms should go through it so that all the ancillary structures (contacts, hydrogen bonds, etc.) stay consistent.

The class is stateless: it does not cache any data, it just manipulates the Molecule.Atom.show flag and the atomic contact arrays. This follows the same pattern as ShortContactsFinder and HBondFinder.

16.1. Hide operations

MethodEffect
HideSelected(molecule)Hides every atom whose selected flag is true and whose show flag is currently true. Returns the number of atoms that were actually hidden.
HideReverseSelected(molecule)Hides every atom that is not selected (the complement). Returns the number of atoms hidden.
ShowAll(molecule)Restores the show flag on every atom.
HideAtom(molecule, atomIndex)Hides a single atom by index.
HideMolecule(molecule, atomIndex)Hides every atom that shares the same IndividualMoleculeInCell value and the same cell shift (CellShiftX/Y/Z) as the given atom. Returns the number of atoms hidden. This is the correct way to hide a whole molecule in a packing view: it does not rely on Cartesian proximity.
HideSymmetryEquivalent(molecule, atomIndex)Hides every atom that shares the same SymmetryEquivalentNumberInCell. This is the correct way to hide all copies of an atom generated by the same symmetry operation.
HideShortContacts(molecule)Thin wrapper around ShortContactsFinder.ClearAllContacts(molecule). Does not change the show flag of any atom.

16.2. Hanging contacts cleanup

Contacts in Tal are stored twice — once on each of the two atoms that participate in the contact. If one end is later hidden or removed, the other end is left with a dangling reference to an atom that is no longer visible. RemoveHangingContacts(molecule) walks every atom, filters its Contact* arrays, and drops every entry whose target atom is out of range or has show == false. The filtered arrays are:

ContactCount is updated to match the new list length. The method returns the total number of contacts removed and writes a single line to the debug log.

When to call it: after any bulk hide operation (hide selected, hide molecule, hide symmetry equivalent) it is recommended to call RemoveHangingContacts so that the next redraw of short contacts does not attempt to draw lines to invisible atoms. This is not done automatically because the caller may want to preserve the contact list for a subsequent "undo".

16.3. Statistics

16.4. Typical workflows

Isolate a single molecule in a packing view:

  1. Pick one atom of the target molecule (Pick Atoms mode).
  2. Call HideReverseSelected(molecule) first (so that only the picked atom's selection is kept), then HideMolecule(molecule, atomIndex) to hide every copy of that molecule except the selected one.
  3. Call RemoveHangingContacts(molecule) to clean up dangling contacts.

Hide all copies of a single symmetry-equivalent atom:

  1. Pick one atom.
  2. HideSymmetryEquivalent(molecule, atomIndex).

Undo all hides:

  1. ShowAll(molecule).

17. Toolbars

Toolbars are built in MainForm.Toolbars.cs. They sit under the menu strip and provide style, camera, and picking controls without opening dock windows.

17.1. Style strip

ControlValues / behaviour
Style comboCPK, Sticks, Balls and Sticks, Ellipsoids, Polyhedrals

17.2. View strip

ControlBehaviour
▶ AnimateStarts/stops automatic rotation of the structure.
a b cView along the real-space cell axes.
a* b* c*View along reciprocal axes.
+X … -ZRotate ±1° about the screen/world axes.
←90 →90 ↑90 ↓90Rotate 90°.
Arrow pan buttonsTranslate the scene left/right/up/down.
Zoom+ / Zoom-Change camera zoom.
Persp. / Ortho.Toggle perspective vs orthographic projection (CheckOnClick).

17.3. Picking strip

ModeBehaviour
RotateDefault navigation (LMB rotate, RMB zoom, MMB pan).
Pick AtomsToggle atom selection.
Lasso AtomsRectangular multi-select.
Expand ContactsGrow short-contact shell around the clicked atom.
Toggle LabelsClick atoms to show/hide their labels.
Move LabelsDrag labels in the viewport.
Measure DistancesTwo-atom distances.
Measure AnglesThree-atom angles.
Measure TorsionsFour-atom torsions.
Reveal Symmetry-Generated MoleculesReveal symmetry images of molecules.

18. Hydrogen Bonds (H-Bond Settings)

Detection and drawing are implemented by HBondFinder (Tal/Calculate/HBondFinder.cs). The settings UI is HBondSettingsWindow (title H-Bond Settings), opened via OnHBondSettings / Display Options → H-Bond related paths.

18.1. Contact distance range

ControlMeaning
Actual distanceUse absolute D⋯A (or H⋯A) limits in Å (numActualMin / numActualMax).
VdW distanceLimits relative to sum of donor and acceptor van der Waals radii.
Min/Max = r_vdW ± valueOperators minus / plus with numeric offsets (numVdWMinus, numVdWPlus).

18.2. D–H⋯A angle

18.3. Options

18.4. Donors / Acceptors / Color

18.5. Actions

Drawing: POV-Ray export and the real-time renderer both read H-bond geometry and Flags.HBondColor (see section 13 in the main help).

19. Hydrogen Normalization

Menu: Calculate → Normalize Hydrogens... → OnNormalizeHydrogens. Window: HydrogenNormalizationWindow (title Hydrogen Normalization). Engine: HydrogenNormalizer.

19.1. When to use it

19.2. Window controls

ControlRole
Normalize checkboxWhen checked, the table shows proposed neutron lengths; rebuilds on change.
Skip bridgingWhen checked, bridging hydrogens are left unchanged (default on).
Molecule info labelActive molecule summary.
TablePer-hydrogen status / proposed length / parent atom.
ApplyWrites normalized coordinates; originals cached for Reset.
ResetRestores coordinates from HydrogenNormalizer cache (enabled only if a cache exists).
CloseCloses the dock window.

19.3. Length table

Standard distances live in HydrogenNormalizer.NeutronLengths (element-pair keys). Analysis uses HydrogenNormalizer.Check over the asymmetric unit (and related helpers for Hirshfeld).

19.4. Workflow

  1. Open a structure with explicit hydrogen atoms.
  2. Calculate → Normalize Hydrogens... — molecule is loaded before the window is shown.
  3. Review the table; adjust Skip bridging if needed.
  4. Apply — updates coordinates in the active molecule.
  5. Optional Reset to restore the pre-normalization geometry from the session cache.
Hirshfeld: Do not close the Hydrogen Normalization window mid-generate if the pipeline holds a live reference to it (see main help §7.2). Prefer Apply before starting a long Hirshfeld run, or rely on the silent path.

20. Display Options, Depth Cue & Auto-Rotate

20.1. Display Options window

Dock window DisplayOptionsWindow, toggled from Window → 🎨 Display Options (OnToggleDisplayOptions).

Display group

Options group

Buttons

20.2. Depth Cue window

DepthCueWindow fine-tunes fog / attenuation when Depth cue is enabled. Opened from display options or related commands; parameters are stored in Flags and applied in the draw pipeline.

20.3. Auto-Rotate window

AutoRotateWindow complements the toolbar ▶ Animate button with explicit speed / axis options when available. Animation advances the camera or model rotation each frame while active.

22. License

TAL — Crystallographic Application
Copyright © 2026 Daniel M. Kamiński, Lublin, Poland
All rights reserved.

Website: https://dkami.umcs.pl/tal/
Contact: daniel.kaminski@umcs.pl

Academic / Non-Commercial License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to use, copy, modify, and distribute the Software for non-commercial, academic, and educational purposes only , subject to the conditions below.

1. Non-commercial use only

The Software may be used solely for non-commercial purposes, including but not limited to academic research, teaching, and personal study. Commercial use of any kind is strictly prohibited without a separate commercial license from the copyright holder.

“Commercial use” includes, without limitation:

2. Attribution

Any distribution, publication, or derivative work that uses or is based on the Software must include the following notice:

TAL - Crystallographic Application
Copyright (c) 2026 Daniel M. Kamiński
Contact: daniel.kaminski@umcs.pl
Website: https://dkami.umcs.pl/tal/

3. No warranty

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

4. Commercial licensing

For commercial licensing inquiries, please contact:
Daniel M. Kamiński
E-mail: daniel.kaminski@umcs.pl
Lublin, Poland

5. Third-party components

This Software incorporates third-party libraries distributed under their own licenses (including but not limited to .NET, MonoGame, DockPanel Suite, SharpDX). Those licenses remain in force and are unaffected by this license.

Note: The full license text is also distributed with the application as the file LICENSE.txt in the same folder as the executable.