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 2.6 beta · Last updated: 2026

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.

2.1. Open...

Opens a structure file. Tal supports a wide variety of formats, including but not limited to:

If a file contains multiple structures (e.g., a multi-record CIF), Tal will prompt you to open all of them, only the first one, or none. All loaded structures are added to the Structure List.

2.2. Open Recent

Provides a list of recently opened files for quick access. The list is maintained by RecentFilesManager (singleton) and persisted to %LocalAppData%\Tal\RecentFiles.json.

2.3. Save As...

Saves the currently selected structure from the Structure List to a new file. The save operation is dispatched by StructureWriter.Save() based on the target extension.

Supported Write Formats

ExtensionWriterNotes
.cifCifWriterIUCr CIF with full metadata (cell, symmetry, refinement, ADP, disorder).
.res, .insResWriterSHELX RES/INS. Uses SymmetryHelper to never guess operations.
.xyzXyzWriterSimple Cartesian XYZ.
.pdb, .entPdbWriterHETATM records; CRYST1 from cell parameters.
.mol, .sdf, .sdMolWriterV2000 molfile (bonds currently not exported).
.mol2, .ml2Mol2WriterTripos MOL2 with optional CRYSIN block.
.gjf, .com, .gauGaussianWriterGaussian input with #p B3LYP/6-31G(d) Opt.
.d12CrystalWriterCRYSTAL17 input deck. See note below.

CRYSTAL17 export and standard setting

CRYSTAL17 requires the standard setting of the space group (e.g. P2₁/c, not P2₁/n). Before writing a .d12 file, Tal calls SpaceGroupTransformer.GetTransformToStandard() and, if the structure is non-standard, shows a dialog:

Note: Transformation modifies cell parameters, atomic fractional coordinates, ADP tensors, symmetry operations, and the space-group name. Warnings (e.g. the transformation summary) are shown in a message box after the file is written.

2.4. Save Screenshot...

Opens the ScreenshotDialog (a fixed-dialog WinForms window) which captures the current 3D viewport and saves it as a high-quality image. The dialog computes a supersampled resolution and reads the final backbuffer at that multiplier.

ScreenshotDialog controls

ControlDescription
cmbScaleMultiplier: 1×, 2×, 3×, 4×, 6×, 8× (labels: original, HD, 4K, max). Default: 2×.
lblResolutionLive preview: "→ W × H px" (italic blue). Shows "(max)" if clamped.
radioFullFull frame (default) — 3D + labels + color scale.
radioSceneScene only — pure 3D, no overlay.
chkSupersampleEnable additional supersampling inside the renderer (better AA, slower).
btnSaveOK; commits the selection (sets ResultWidth, ResultHeight).
btnCancelCancel. The dialog returns DialogResult.Cancel.

Final resolution rule: ResultW = round(baseWidth × Scale), then clamped to MAX_DIMENSION = 8192 px per axis. Base dimensions are the current backbuffer dimensions of the active MonoGame document.

Static helper: ScreenshotDialog.ShowSettings(owner, baseWidth, baseHeight, out scale, out fullFrame, out supersample).

2.5. Export to POV-Ray...

Exports the entire scene (atoms, bonds, unit cell, visible cavities, polyhedra, planes) to a POV-Ray scene file (*.pov). Photorealistic rendering can then be done with the external POV-Ray engine. The generator uses the current camera, lighting, background, and display flags.

See section 13 for a detailed description of PovRayGenerator.

2.6. Close / Exit

3. Selection & Manipulation

The Selection menu and the mouse provide several ways to interact with atoms and molecules.

3.1. Mouse Interaction Modes

The main toolbar allows you to switch between different interaction modes:

3.2. Selection Menu

Note: When atoms are selected, they are highlighted in yellow. Selection is the basis for many operations, such as applying individual colors, defining planes, or making measurements.

3.3. Programmatic selection helpers

The class MeanPlaneCalculator (namespace Tal.Core.PowderDiffraction) exposes static helpers used by the plane editor and by external tools:

These are used by the Crystal Planes tool when building mean planes.

4. Visual Display

The Display menu controls all aspects of the visual representation of the structure.

4.1. Style

The main toolbar contains a dropdown to select the global display style for all atoms. This can be overridden for individual atoms through the Atom Display Settings window.

4.2. Colour atom

Opens the 🎨 Atom Color window, which allows you to choose a global coloring mode for the entire structure. This is a powerful tool for visual analysis.

Available color modes include:

4.3. Labels

Opens the 🏷️ Labels Settings window. Here you can control:

Labels can also be individually repositioned by enabling Move Labels Mode from the toolbar.

4.4. Background...

Opens the 🎨 Background Colors window. You can choose a solid color or a two-color gradient for the 3D viewport background. Several presets are available, or you can pick custom colors.

4.5. Lighting...

Opens the ⚡ Lighting window. This provides full control over the scene's illumination, including:

4.6. Atom Display Settings

Opens the 🎨 Atom Display Settings window. This window provides fine-grained control over the size of spheres and sticks, and the appearance of ellipsoids and cell lines.

4.7. Atom Colors (global)...

Opens the 🎨 Atom Colors window. This is a different tool from the "Colour atom" window. It allows you to edit the global, persistent color palette for each element, indexed by atomic number.

4.8. Short Contacts...

Opens the 📏 Define Short Contacts window. This tool finds all non-bonded contacts shorter than a user-defined threshold.

The detected contacts are drawn as red dashed lines in the 3D view.

4.9. Scene Quality...

Opens the 🎚️ Scene Quality window, which controls rendering fidelity and performance.

5. Calculations

The Calculate menu provides tools for geometric analysis of the structure.

5.1. Centroids...

Calculates the centroid of the currently selected atoms. The result is displayed in the status bar. Useful for finding the center of a ring or a group of atoms.

5.2. Planes...

Opens the Planes window. This tool allows you to define and manage crystallographic planes.

See section 9 for a detailed description of the plane tool.

5.3. Packing...

Opens the 📦 Packing window. This is the primary tool for generating and exploring crystal packing diagrams. It allows you to apply symmetry operations and translational shifts to create a supercell.

5.4. Contacts...

Placeholder for a future tool for general contact analysis.

5.5. Molecular Shell...

Placeholder for a future tool for calculating and displaying a molecular surface or shell.

5.6. Graph Sets...

Placeholder for a future tool for topological analysis of the hydrogen bond network (graph set analysis).

5.7. Powder Pattern...

Opens the PowderDiffractionWindow. See section 8 for a full description.

5.8. Transform space group...

This powerful tool allows for the transformation of a structure from one setting of a space group to an equivalent setting (e.g., from P2₁/c to P2₁/n, or from a standard to a non-standard setting).

See section 13 for details on SpaceGroupTransformer.

Warning: This is a destructive operation that modifies the currently selected structure in the database. It is recommended to save your work before using this feature.

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 the Calculate menu.

Detection Parameters

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. Signed Distance Function (SDF) Calculation: The algorithm begins by creating a high-resolution 3D grid (the "voxel grid") over the unit cell. For each point on this grid, it calculates the shortest distance to the surface of any atom. This value is negative inside an atom and positive outside. This creates a smooth field that defines the shape of the atoms.
  2. Handling of Anisotropic Displacement (ADPs): When Use Anisotropic VdW is enabled, the SDF is not calculated as a simple distance to a sphere, but as a distance to a more complex ellipsoid surface. The eigen-decomposition of the ADP tensor (U-matrix) is used to determine the principal axes and radii of the ellipsoid for each atom, providing a physically more meaningful representation.
  3. SDF Caching and Correction: For performance, Tal can compute a base SDF for a standard van der Waals radius. If you then change the VdW multiplier, the algorithm doesn't recompute the entire SDF. Instead, it uses a mathematically exact correction: SDF_new = SDF_base - delta, where delta is the change in the radius. This is because the SDF represents a distance, so scaling the atomic radii simply shifts the entire field by a constant. This trick makes recalculating with a different VdW scale extremely fast.
  4. Periodicity and Flood-Fill: The algorithm handles periodic boundary conditions by extending the grid 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. Marching Cubes Mesh Generation: Once the cavities are identified, the Marching Cubes algorithm is applied to the SDF grid to generate a smooth triangle mesh for the surface of each cavity. This mesh can then be rendered as a semi-transparent isosurface. The quality of the mesh can be controlled via the "Smoothing" parameter.

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.

Important: For Hirshfeld analysis to work, the molecule must be treated as a distinct entity. You must first use the Packing window to generate at least one full molecule (or use a symmetry mode that does this) before the Hirshfeld Analysis... menu item is available.

7.1. The Workflow

  1. Ensure a full molecule is present in the scene (e.g., by setting Packing to "Symmetry" mode).
  2. Open the Hirshfeld Surface Analysis window from the Advanced menu.
  3. In the window, select a molecule from the dropdown list (if multiple are present).
  4. Choose the type of surface you want to generate (Hirshfeld Surface, Shape Index, Curvature, etc.).
  5. Click the "Generate" button. The application will compute the property for every point on the surface and generate a color-mapped mesh.
  6. The resulting surface is displayed in the 3D view, colored according to the selected property. The corresponding 2D fingerprint plot is also generated and displayed in the tool window.

7.2. Hirshfeld Surface Properties

7.3. 2D Fingerprint Plots

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

7.4. 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
_lblMoleculeTitleSection header: "Target molecule:".
_cmbMoleculeCombo box listing molecules detected in the active document via HirshfeldTargetSelector.EnumerateMolecules. Each entry is of the form "Molecule #N (X atoms)".
_btnUseSelectionReplaces 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 and the selection is left unchanged. Implemented in UseCurrentSelection().
_lblDensityModelTitle / _cmbDensityModelDensity model dropdown: Spherical (isotropic) or Anisotropic (ADP). The anisotropic mode requires valid ADP tensors and will use the full thermal ellipsoids in the SDF.

Control panel — Detection parameters

ControlParameterRange / Default
_lblVoxelSizeTitle / _numVoxelSizeVoxel size (Å)0.01 – 0.20, default 0.06. Smaller → higher resolution, more memory and time.
_lblSigmaMultTitle / _numSigmaMultSigma multiplier0.80 – 2.00, default 1.60. Multiplies the per-element Gaussian σ from HirshfeldAtomicRadii.GetSigma.
_lblIsoLevelTitle / _numIsoLevelIso level for w(r)0.30 – 0.70, default 0.50. Standard Hirshfeld definition.
_lblMarginTitle / _numMarginGrid margin (Å)1.00 – 8.00, default 2.00. Extra space added around the target's bounding sphere.

Control panel — Property & display

ControlDescription
_cmbPropertyModeSurface coloring: Base color, d_norm (default), d_e, d_i. See section 7.2 for the meaning of each field.
_trackTransparency + _lblTransparencyValueSurface alpha (40 – 255, default 200). The label shows "N / 255".
_trackColorSmooth + _lblColorSmoothValueVertex color smoothing level 0 – 5 (default 2). Applies HirshfeldColorSmoothing.Apply to the per-vertex DNorm/DE/DI arrays without changing geometry. Level 0 = raw values; level 5 = heavy smoothing.
_trackBlur + _lblBlurValuePre-blur of the density grid, 0 – 2 (default 1). Causes an automatic regeneration after an 800 ms debounce (_blurDebounceTimer). level 0 → no blur, 1 → σ = 0.7, 1 iter, 2 → σ = 0.7, 2 iter + second pass σ = 1.26, 1 iter.
_chkShowMeshMaster switch for the isosurface mesh.
_chkShowWireframeDraws the triangle edges of the mesh as thin dark lines. Skipped automatically if the mesh contains more than 200 000 triangles.
_chkShowMoleculeDraw the source molecule behind the surface.
_chkShowAxesDraw the world XYZ axes (length 20 units, red/green/blue).
_chkShowCellDraw the unit cell edges as orange lines.
_lblZoomInfoCurrent 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.

Control panel — Actions

ButtonAction
_btnGenerateRuns the pipeline (GenerateAsync). Disabled while a run is in progress. Cancellation is supported via an internal CancellationTokenSource.
_btnClearDisposes the current surface, invalidates the smoothing cache (HirshfeldSmoothing.ClearCache), and resets the diagnostics.
_btnSavePlySaves the current surface as an ASCII PLY file (vertices, normals, per-vertex RGB). Triangle mesh is written as a vertex_indices list. Useful for external tools (MeshLab, Blender).
_btnExportPngRenders the current view into a RenderTarget2D with SSAA = 2 and saves it as PNG.
_btnFingerprintOpens the HirshfeldFingerprintWindow and pushes the raw per-vertex de/di + atom-name arrays into it. The document registers two callbacks: OnRegionClicked (highlight a bin) and OnVertexBinsUpdated (rebucket + rebuild).
_btnShowDiagnosticsPushes HirshfeldAtomDistanceDiagnostics to the DebugWindow (see section 7.7). The report contains grid info, target/outer statistics, a histogram and a cutoff analysis table.

Progress bar & status label

GenerateAsync — nine-step algorithm

  1. KROK 1/9 — Building HirshfeldScene. Calls HirshfeldScene.Build(targetMolId, neighborCutoff). If the target molecule is not found, the operation aborts with a friendly message.
  2. KROK 2/9 — Preparing atom entries. Wraps each SceneAtom into an AtomGaussParams via HirshfeldParameters.BuildFromAtomSimple. Isotropic (σ) or anisotropic (ADP eigen-decomposition) parameters are selected based on the Density model dropdown.
  3. KROK 3/9 — Building density grid. A cubic grid is created around the target center. Its resolution is n = ceil(2·(r_max + margin) / voxelSize). Fails gracefully if n³ exceeds Flags.MAX_VOXELS.
  4. KROK 4/9 — Filling density grid. Parallel per-thread accumulation of Gaussian contributions. Uses the fast path from v2.9: sphere bounds for isotropic atoms, bounding box for anisotropic ones. Hot loop uses HirshfeldExpLut.FastExpNeg.
  5. KROK 5/9 — Computing w(r). In-place reduction: W = RhoPro / (RhoPro + RhoProm_Outer) with a strict fallback of 0 for empty voxels. Diagnostic distribution is written to the debug log.
  6. KROK 6/9 — Optional blur. BlurDensityField(sigma, iterations) with separable 1-D convolution passes along X, Y, Z. A second pass uses sigma * 1.8 when BlurIterations ≥ 2.
  7. KROK 7/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. KROK 8/9 — Metadata. Populates _surface.TargetMoleculeId, TargetAtomCount, OuterAtomCount, Center, UsedAnisotropic, and calls ComputeBoundingRadius().
  9. KROK 9/9 — Per-vertex properties. HirshfeldProperties.Compute populates DE, DI, DNorm, and stores the nearest-atom indices and names (used later by the fingerprint plot). The original values are also cloned into DEOriginal, DIOriginal, DNormOriginal so that color smoothing is fully deterministic.

After step 9, the document calls HirshfeldColorSmoothing.Apply(_surface, _colorSmoothingLevel), rebuilds all vertex buffers, resets the camera and raises Flags.Instance.VisualRevision + MarkDirty(). If the fingerprint window is already open, its analysis is rebuilt and pushed.

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.5. 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 is essential: if you translated each atom independently you would tear molecules apart.
  4. Deduplicate again to remove the post-translation collisions.

The result is a cell where every molecule generated by symmetry is geometrically compact — molecules may stick out of the cell but never straddle two distant locations.

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
SurfaceThe produced HirshfeldSurface.
GridThe internal HirshfeldGrid with flat W, RhoPro, RhoProm_Outer arrays.
SceneThe HirshfeldScene used to build the surface.
BlurSigma, BlurIterationsCurrently applied blur parameters (see section 7.4).

The detector writes a lot of timestamped diagnostic output ([t=…ms]) to the debug stream, including per-thread parallel configuration, grid dimension, memory estimate and the distribution of w(r) before and after 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 parallelised and iterates linearly, with the historical fallback of 0 for empty voxels (an earlier version used 0.5 which produced spurious triangles).

HirshfeldParameters & HirshfeldAtomicRadii

HirshfeldParameters.BuildFromAtomSimple converts an atom (position, atomic number, ADP matrix) into an AtomGaussParams:

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, i.e. invisible in the final rendering. Used by every Evaluate and by the fill loop inside HirshfeldDetector.

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; instead 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 = (d_e − r_vdW(outer)) / r_vdW(outer)
       + (d_i − r_vdW(inner)) / r_vdW(inner)

The d_norm value is clamped to [−1.5, +1.5] for display purposes. The names of the nearest inner and outer atoms are stored so the fingerprint plot can label contact types.

HirshfeldColorSmoothing

A color-only smoothing pass that never changes geometry. It re-runs over the current mesh vertices and:

  1. Restores DNorm/DE/DI from the *Original arrays (idempotent, independent of the previous smoothing level).
  2. Builds a spatial hash (0.20–0.80 Å radius depending on the level) and accumulates the values with 1/d² weights.
  3. Applies 1–3 Jacobi-like iterations per level.

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. It uses a Laplacian average of neighbours, then decomposes the displacement into a tangential and a normal component. The tangential part fixes the staircase, the normal part reduces tiny spikes. The maximum displacement is clamped to 5% of the grid unit to avoid overshooting.

HirshfeldSmoothing

A geometry-only Taubin (λ | μ) smoothing applied to the mesh itself. Unlike a plain Laplacian, the negative μ step compensates the shrinkage, so the surface volume is approximately preserved. The original vertex positions and the adjacency list are cached per HirshfeldSurface, making the slider fully deterministic: level 0 always returns exactly the original mesh.

HirshfeldTargetSelector

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

7.6. 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.

Bin construction & re-bucketing

Rendering

Mouse interaction

GestureEffect
LMB click on the plotSelects a single bin, notifies the document via OnRegionClicked(binDe, binDi), and stores the pixel position for the red marker.
LMB drag on the plotRubber-band zoom. The selected rectangle is converted into a new [rangeMin, rangeMax] pair and a new bin size (selected so the new bin count stays between 10 and MAX_TOTAL_CELLS).
LMB drag on a labelMoves the corresponding label individually. Supported labels: title, d_e (Å), d_i (Å), tick numbers. The pick target uses a 70-pixel proximity test.
RMB click on the plotPlaces a red marker at the cursor and selects the bin below it (same effect as LMB click).
RMB double-click anywhereFull reset (ResetView): label offsets, font (to 4 pt), zoom factor, range, bin size and marker all return to their original values.
WheelZooms the 2θ 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.

Side panel controls

ControlDescription
_numFontSizeLabel font size in points. Range 2 – 36, default 4.
_numBinSizeBin size in Å. Range 0.001 – 0.05, default 0.005. Changing it triggers a live re-bucket.
_numRangeMinAxis origin in Å. Range 0.0 – 3.0, default 0.0. Changing it triggers a live re-bucket.
_btnResetOffsetsFull reset (identical to RMB double-click).
_chkShowAxes, _chkShowDiagonal, _chkShowMajorGrid, _chkShowMinorGridToggle the four overlay elements. The diagonal is drawn twice — thick white behind, thin black on top — so it is visible on any background.
_btnSavePngRenders the same content into a 2048 × 2048 RenderTarget2D and saves it as PNG.
_lblStatus4-line status: number of bins in range, current bin size and range, current zoom, and the interaction hints.

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.

7.7. Surface Data Model and Diagnostics

HirshfeldSurface

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

FieldDescription
Vertices, Indices, NormalsMesh data. Normals are recomputed after smoothing/relaxation.
VerticesOriginalCopy of the raw MC vertices (before any relaxation).
DE, DI, DNormPer-vertex properties (see section 7.2).
DEOriginal, DIOriginal, DNormOriginalCloned before any color smoothing.
NearestInnerAtom, NearestOuterAtomIndices into the scene's target/outer arrays.
NearestInnerAtomName, NearestOuterAtomNameThe names of those atoms (e.g. "O1", "H12").
TargetMoleculeId, TargetAtomCount, OuterAtomCountMetadata captured at generation time.
Center, BoundingRadiusGeometric center and enclosing sphere radius.
IsoLevel, UsedAnisotropicGeneration parameters.
ShiftAtGenerationThe MoleculeShift value at generation time (kept for reference; the document uses the dynamic _lastMoleculeShift instead).

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: header (cyan), grid info (white), target stats (light blue), outer stats (light green), histogram (colored bars), cutoff analysis and interpretation hints.

Export formats

Performance tip: A typical organic molecule with ~30 heavy atoms and a voxel size of 0.06 Å produces a grid of ~250³ ≈ 15.6 M voxels. The detection runs in 2–4 s on a modern desktop. Increasing the voxel size to 0.08 Å roughly halves the runtime; a margin of 2 Å is usually enough.

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 & 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 Display menu (right-click on the H-Bond checkbox) or the main menu, this window provides detailed control over hydrogen bond detection and display.

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 one normal cavity detection (Calculate → Cavity Detection) so that a base SDF is computed and cached in CavityDetector.HasBaseSDF. The VdW analysis reuses this base SDF and applies the cheap SDF_new = SDF_base − delta correction for every scale — without a base SDF the analysis cannot run.
  3. Invoke the analysis from the GUI (Calculate → VdW Scaling Analysis...) or programmatically via VdWScaleAnalyzer.RunAnalysisAsync(molecule, detector, scales).
  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.
  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. The VdWAnalysisResults object is stored in VdWScaleAnalyzer.CurrentResults and can be saved as a TXT or HTML report.
Cancellation: the analyzer has a linked CancellationTokenSource. Cancelling it aborts the current scale, restores the original settings and returns the partial results collected so far (cavities detected at scales already processed remain in the result object).

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.
Continuity with the version on the project page: The descriptors I_frag, f_collapse, Ω_Q, Ψ_H, ⟨Z_cav⟩ and α_polar are the same set that the website describes. The implementation in Tal works on the exact VdWAnalysisResults object and does not require any external tools.

15.6. 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.

The center used for the Cavity.SphereCenter property is chosen from the non-wall spheres (i.e. spheres whose fractional coordinates are all in [0.01, 0.99]) when possible, and from the overall maximum otherwise.

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.7. 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.8. 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.9. 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).
PropertiesA dictionary of extra statistics from the multi-start analysis: TotalPathsFound, ValidPathsFound, AvgLength, AvgDiameter, AvgHelicity, MinLength, MaxLength, MinDiameter, MaxDiameter, MinHelicity, MaxHelicity, SuccessRate.

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. If fewer than 3 candidates are found, random sampling is used as a fallback.
  3. A* search: for each start point, two separate A* searches are run — one toward the minimum wall (e.g. x = 0) and one toward the maximum wall (x = nx−1). 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. The local diameter is found by casting rays outward from the current voxel until a wall is hit.
  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). The BestPath is the widest path whose average width exceeds 0.8 Å, or the widest overall if none exceeds the threshold.
  8. XNA conversion: WorldPathXna is filled from WorldPath so the renderer can consume it directly.

When the analysis completes, each cavity receives a CrystalChannelResults dictionary keyed by "XX", "YY", "ZZ". The HasCrystalChannels property then returns true and the renderer can draw the channel paths as thick tubes.

15.10. 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).