Author: Daniel M. Kaminski · Lublin, Poland
https://dkami.umcs.pl/tal/ · https://dkami.umcs.pl/cavity-detector-in-cif-v1-82/
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.
Operations related to opening, saving, and exporting data are found in the File menu.
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.
Provides a list of recently opened files for quick access. The list is maintained by RecentFilesManager (singleton) and persisted to %LocalAppData%\Tal\RecentFiles.json.
MaxEntries).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.
| Extension | Writer | Notes |
|---|---|---|
.cif | CifWriter | IUCr CIF with full metadata (cell, symmetry, refinement, ADP, disorder). |
.res, .ins | ResWriter | SHELX RES/INS. Uses SymmetryHelper to never guess operations. |
.xyz | XyzWriter | Simple Cartesian XYZ. |
.pdb, .ent | PdbWriter | HETATM records; CRYST1 from cell parameters. |
.mol, .sdf, .sd | MolWriter | V2000 molfile (bonds currently not exported). |
.mol2, .ml2 | Mol2Writer | Tripos MOL2 with optional CRYSIN block. |
.gjf, .com, .gau | GaussianWriter | Gaussian input with #p B3LYP/6-31G(d) Opt. |
.d12 | CrystalWriter | CRYSTAL17 input deck. See note below. |
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:
StructureStorage is not modified) and save.
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.
| Control | Description |
|---|---|
cmbScale | Multiplier: 1×, 2×, 3×, 4×, 6×, 8× (labels: original, HD, 4K, max). Default: 2×. |
lblResolution | Live preview: "→ W × H px" (italic blue). Shows "(max)" if clamped. |
radioFull | Full frame (default) — 3D + labels + color scale. |
radioScene | Scene only — pure 3D, no overlay. |
chkSupersample | Enable additional supersampling inside the renderer (better AA, slower). |
btnSave | OK; commits the selection (sets ResultWidth, ResultHeight). |
btnCancel | Cancel. 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).
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.
Close: Closes the currently active document (3D view window).Exit: Shuts down the entire application. The layout and settings are saved automatically on exit.The Selection menu and the mouse provide several ways to interact with atoms and molecules.
The main toolbar allows you to switch between different interaction modes:
Select Atoms: Selects all visible atoms in the current structure.Select Molecules: (If applicable) Selects all atoms belonging to a single molecule in a crystal packing view.Select Unit Cell: Selects the unit cell frame.Invert Selection: Selects all unselected atoms and deselects all currently selected atoms.Clear Selection: Deselects all atoms.
The class MeanPlaneCalculator (namespace Tal.Core.PowderDiffraction) exposes static helpers used by the plane editor and by external tools:
CollectSelectedAtomIndices(Molecule) — returns a List<int> of indices of atoms whose selected flag is true.CountSelectedAtoms(Molecule) — returns the number of selected atoms.These are used by the Crystal Planes tool when building mean planes.
The Display menu controls all aspects of the visual representation of the structure.
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.
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:
Opens the 🏷️ Labels Settings window. Here you can control:
Labels can also be individually repositioned by enabling Move Labels Mode from the toolbar.
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.
Opens the ⚡ Lighting window. This provides full control over the scene's illumination, including:
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.
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.
Reset Selected and Reset All buttons allow you to revert to the original default colors.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.
Opens the 🎚️ Scene Quality window, which controls rendering fidelity and performance.
The Calculate menu provides tools for geometric analysis of the structure.
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.
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.
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.
Placeholder for a future tool for general contact analysis.
Placeholder for a future tool for calculating and displaying a molecular surface or shell.
Placeholder for a future tool for topological analysis of the hydrogen bond network (graph set analysis).
Opens the PowderDiffractionWindow. See section 8 for a full description.
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.
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.
This is the main control panel for all cavity-related operations, opened from the Calculate menu.
Once cavities have been detected, you can choose how to visualize them:
Tal's cavity detector is a sophisticated implementation designed for speed and accuracy, even for large and complex structures.
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.
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.
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.
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.
Hirshfeld Surface Analysis window from the Advanced menu.w(r) = 0.5 isosurface of the Hirshfeld weight function, which is the ratio of the electron density of a promolecule (the selected molecule) to the total electron density of the crystal (procrystal). The surface itself can be colored by various properties.
di: The distance from the surface point to the nearest nucleus *inside* the surface.de: The distance from the surface point to the nearest nucleus *outside* the surface.dnorm: A normalized contact distance, calculated as dnorm = (di - rᵢᵛᵈʷ) / rᵢᵛᵈʷ + (de - reᵛᵈʷ) / reᵛᵈʷ. This is a powerful property that highlights close intermolecular contacts. Areas with dnorm < 0 (typically colored red) are regions of strong interaction (e.g., hydrogen bonds, close packing). Blue areas represent longer, weaker contacts.S = (2/π) * atan((κ₁ + κ₂) / (κ₁ - κ₂)), where κ₁ and κ₂ are the principal curvatures. It is an excellent tool for identifying complementary shapes: convex "bumps" (like C-H groups) have S > 0 (colored blue), while concave "hollows" (like O or N lone pairs) have S < 0 (colored red). Complementary bumps and hollows indicate a favorable interaction.
C = (2/π) * ln(sqrt((κ₁² + κ₂²)/2)). Flat areas like π-stacked rings have low curvature, while sharp features like lone pairs have high curvature. It is useful for identifying planar stacking interactions.
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.
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.
_panel, fill). Renders the molecule (or isolated target), the Hirshfeld isosurface, optional wireframe, axes and unit cell. Background color follows Flags.Instance.color1._controlsHost, 300 px wide, docked Right). A FlowLayoutPanel with all controls, sorted top-down.| Control | Description |
|---|---|
_lblMoleculeTitle | Section header: "Target molecule:". |
_cmbMolecule | Combo box listing molecules detected in the active document via HirshfeldTargetSelector.EnumerateMolecules. Each entry is of the form "Molecule #N (X atoms)". |
_btnUseSelection | Replaces the combo selection with the molecule that the currently selected atoms belong to. If the user selected atoms from more than one molecule (or no atoms at all), a message box is shown and the selection is left unchanged. Implemented in UseCurrentSelection(). |
_lblDensityModelTitle / _cmbDensityModel | Density 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 | Parameter | Range / Default |
|---|---|---|
_lblVoxelSizeTitle / _numVoxelSize | Voxel size (Å) | 0.01 – 0.20, default 0.06. Smaller → higher resolution, more memory and time. |
_lblSigmaMultTitle / _numSigmaMult | Sigma multiplier | 0.80 – 2.00, default 1.60. Multiplies the per-element Gaussian σ from HirshfeldAtomicRadii.GetSigma. |
_lblIsoLevelTitle / _numIsoLevel | Iso level for w(r) | 0.30 – 0.70, default 0.50. Standard Hirshfeld definition. |
_lblMarginTitle / _numMargin | Grid margin (Å) | 1.00 – 8.00, default 2.00. Extra space added around the target's bounding sphere. |
| Control | Description |
|---|---|
_cmbPropertyMode | Surface coloring: Base color, d_norm (default), d_e, d_i. See section 7.2 for the meaning of each field. |
_trackTransparency + _lblTransparencyValue | Surface alpha (40 – 255, default 200). The label shows "N / 255". |
_trackColorSmooth + _lblColorSmoothValue | Vertex 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 + _lblBlurValue | Pre-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. |
_chkShowMesh | Master switch for the isosurface mesh. |
_chkShowWireframe | Draws the triangle edges of the mesh as thin dark lines. Skipped automatically if the mesh contains more than 200 000 triangles. |
_chkShowMolecule | Draw the source molecule behind the surface. |
_chkShowAxes | Draw the world XYZ axes (length 20 units, red/green/blue). |
_chkShowCell | Draw the unit cell edges as orange lines. |
_lblZoomInfo | Current zoom factor as "Zoom: X.XX× (mouse wheel)". The mouse wheel applies a multiplicative step of 1.05 / 0.95, clamped to 0.2 – 8.0. |
| Button | Action |
|---|---|
_btnGenerate | Runs the pipeline (GenerateAsync). Disabled while a run is in progress. Cancellation is supported via an internal CancellationTokenSource. |
_btnClear | Disposes the current surface, invalidates the smoothing cache (HirshfeldSmoothing.ClearCache), and resets the diagnostics. |
_btnSavePly | Saves 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). |
_btnExportPng | Renders the current view into a RenderTarget2D with SSAA = 2 and saves it as PNG. |
_btnFingerprint | Opens the HirshfeldFingerprintWindow and pushes the raw per-vertex de/di + atom-name arrays into it. The document registers two callbacks: OnRegionClicked (highlight a bin) and OnVertexBinsUpdated (rebucket + rebuild). |
_btnShowDiagnostics | Pushes HirshfeldAtomDistanceDiagnostics to the DebugWindow (see section 7.7). The report contains grid info, target/outer statistics, a histogram and a cutoff analysis table. |
_progressBar: ProgressBarStyle.Continuous, 0 – 100. Updated through a thread-safe BeginInvoke adapter._lblStatus: 4-line status text (monospace). Shows "Ready", "Generating…", final vertex/triangle counts, and the current smoothing/blur state.HirshfeldScene.Build(targetMolId, neighborCutoff). If the target molecule is not found, the operation aborts with a friendly message.SceneAtom into an AtomGaussParams via HirshfeldParameters.BuildFromAtomSimple. Isotropic (σ) or anisotropic (ADP eigen-decomposition) parameters are selected based on the Density model dropdown.n = ceil(2·(r_max + margin) / voxelSize). Fails gracefully if n³ exceeds Flags.MAX_VOXELS.HirshfeldExpLut.FastExpNeg.W = RhoPro / (RhoPro + RhoProm_Outer) with a strict fallback of 0 for empty voxels. Diagnostic distribution is written to the debug log.BlurDensityField(sigma, iterations) with separable 1-D convolution passes along X, Y, Z. A second pass uses sigma * 1.8 when BlurIterations ≥ 2.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._surface.TargetMoleculeId, TargetAtomCount, OuterAtomCount, Center, UsedAnisotropic, and calls ComputeBoundingRadius().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.
Camera.MouseMove). The scene sorting algorithm is invalidated and the triangles are re-sorted next frame._zoomFactor (scene scale), not the camera zoom.Ctrl+S — triggers SavePng().
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.
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.
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.
Molecule, targetMoleculeId, cutoff (Å). Default cutoff = HirshfeldParameters.CUT_OFF = 10 Å.TargetAtoms, OuterAtoms, Fragments, TargetCenter, TargetBBoxMin/Max, TargetRadius, TargetMoleculeId, LastCutoff, LastNeighborCount.SymmetryAtomIndex, CellShiftX/Y/Z, FracPosition, CartPosition, AtomicNumber, IsTarget, IsAnisotropic, Un (ADP matrix), Occupancy, Name, MoleculeId, SourceAsymIndex.
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.
DEDUP_TOL_FRAC = 0.005.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.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.
Detects the surface and returns a HirshfeldSurface. Its main entry point is GenerateAsync, which drives the nine-step algorithm described above. Important public properties:
| Member | Description |
|---|---|
Surface | The produced HirshfeldSurface. |
Grid | The internal HirshfeldGrid with flat W, RhoPro, RhoProm_Outer arrays. |
Scene | The HirshfeldScene used to build the surface. |
BlurSigma, BlurIterations | Currently 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.
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.BuildFromAtomSimple converts an atom (position, atomic number, ADP matrix) into an AtomGaussParams:
ρ(r) = Z_eff / (√(2π)σ)³ · exp(−|r−r₀|² / (2σ²)). The per-element σ comes from HirshfeldAtomicRadii.GetSigma(Z) (0.35 Å for H up to ~0.72 Å for Fr) multiplied by the user's Sigma multiplier.Un is row-normalized to get the three eigenvectors; the row lengths before normalization are the principal half-axes. Each half-axis is combined with the electronic σ as a_k = sqrt(λ_k² + σ_e²), giving an ellipsoid whose orientation follows the thermal motion.CUT_OFF × max(σ or a_k) with CUT_OFF = 10.
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.
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.
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.
A color-only smoothing pass that never changes geometry. It re-runs over the current mesh vertices and:
*Original arrays (idempotent, independent of the previous smoothing level).0.20–0.80 Å radius depending on the level) and accumulates the values with 1/d² weights.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.
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.
Simple helper that lists all molecules in a Molecule by their IndividualMoleculeInCell ID. Used by the _cmbMolecule dropdown and by UseCurrentSelection().
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.
[0.0 Å, 3.4 Å], default bin size 0.005 Å. Both can be changed live from the side panel.RebuildHeatmapFromRaw() recomputes a full histogram and a per-vertex (binDe, binDi) table. Vertices outside the range are tagged (−1, −1).HirshfeldDocument via the OnVertexBinsUpdated callback, so the 3D highlighting stays in sync even after re-bucketing.binsInRange ≤ MAX_TOTAL_CELLS = 8192 and texture bytes ≤ 512 MiB.Flags.Instance.color1 (same as the 3D viewport).Texture2D of size texSize × texSize, drawn with SamplerState.PointClamp (crisp bins).white → cyan → blue → magenta/red, driven by log(1 + count) / log(1 + maxCount) and a intensity^0.6 compression. Minimum visible intensity is 0.15 so isolated bins are still visible.d_e (Å) / d_i (Å) are drawn).| Gesture | Effect |
|---|---|
| LMB click on the plot | Selects a single bin, notifies the document via OnRegionClicked(binDe, binDi), and stores the pixel position for the red marker. |
| LMB drag on the plot | Rubber-band zoom. The selected rectangle is converted into a new [rangeMin, rangeMax] pair and a new bin size (selected so the new bin count stays between 10 and MAX_TOTAL_CELLS). |
| LMB drag on a label | Moves 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 plot | Places a red marker at the cursor and selects the bin below it (same effect as LMB click). |
| RMB double-click anywhere | Full reset (ResetView): label offsets, font (to 4 pt), zoom factor, range, bin size and marker all return to their original values. |
| Wheel | Zooms 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. |
| Control | Description |
|---|---|
_numFontSize | Label font size in points. Range 2 – 36, default 4. |
_numBinSize | Bin size in Å. Range 0.001 – 0.05, default 0.005. Changing it triggers a live re-bucket. |
_numRangeMin | Axis origin in Å. Range 0.0 – 3.0, default 0.0. Changing it triggers a live re-bucket. |
_btnResetOffsets | Full reset (identical to RMB double-click). |
_chkShowAxes, _chkShowDiagonal, _chkShowMajorGrid, _chkShowMinorGrid | Toggle the four overlay elements. The diagonal is drawn twice — thick white behind, thin black on top — so it is visible on any background. |
_btnSavePng | Renders the same content into a 2048 × 2048 RenderTarget2D and saves it as PNG. |
_lblStatus | 4-line status: number of bins in range, current bin size and range, current zoom, and the interaction hints. |
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.
The data container for a computed surface. All vertex-based arrays share the same length.
| Field | Description |
|---|---|
Vertices, Indices, Normals | Mesh data. Normals are recomputed after smoothing/relaxation. |
VerticesOriginal | Copy of the raw MC vertices (before any relaxation). |
DE, DI, DNorm | Per-vertex properties (see section 7.2). |
DEOriginal, DIOriginal, DNormOriginal | Cloned before any color smoothing. |
NearestInnerAtom, NearestOuterAtom | Indices into the scene's target/outer arrays. |
NearestInnerAtomName, NearestOuterAtomName | The names of those atoms (e.g. "O1", "H12"). |
TargetMoleculeId, TargetAtomCount, OuterAtomCount | Metadata captured at generation time. |
Center, BoundingRadius | Geometric center and enclosing sphere radius. |
IsoLevel, UsedAnisotropic | Generation parameters. |
ShiftAtGeneration | The MoleculeShift value at generation time (kept for reference; the document uses the dynamic _lastMoleculeShift instead). |
A compact result set that is passed to the fingerprint window. It contains:
FingerprintHistogram — Dictionary<(int binDe, int binDi), int>.FingerprintBinSize, FingerprintRangeMin, FingerprintRangeMax.ContactTypes — a string[] of length VertexCount with the type of each contact (e.g. "C···H").ContactTypeCounts — a dictionary built by ComputeContactTypeCounts().VertexBins — a per-vertex (binDe, binDi) table (v1.3).A diagnostic object built after each generation. It contains:
TargetCenter, RTarget, RGrid, Margin, VoxelSize, GridResX/Y/Z, TotalAtoms.R_target + delta thresholds (2–8 Å), how many atoms would be rejected.
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.
x y z nx ny nz r g b), then a face list. Compatible with MeshLab, Blender, CloudCompare.
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.
The window is a DockContent composed of a large plot panel (panelPlot, a BufferedPanel) and a bottom control strip (panelControls) divided into group boxes.
| Control | Description |
|---|---|
numericMinTheta | Minimum 2θ in degrees. Range 0–90, default 5.0. |
numericMaxTheta | Maximum 2θ in degrees. Range 0–180, default 90.0. |
| Control | Description |
|---|---|
numericFWHM | Peak full-width at half-maximum (FWHM) in the units of the X axis. Range 0.01–10, default 0.05. |
comboProfile | Profile type: Gaussian, Lorentzian, Pseudo-Voigt (default). |
numericResolution | Number of profile sample points per pixel (profile resolution). Range 1–10, default 2. |
| Control | Description |
|---|---|
numericDebyeWaller | Debye-Waller isotropic B factor in Ų. Range 0–20, default 0.5. |
trackDebyeWaller | Slider linked to the numeric box (value = B × 10). Range 0–200, default 5. |
| Control | Description |
|---|---|
comboSource | X-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):
| Source | Wavelength (Å) | Energy (keV) |
|---|---|---|
| Cu Kα | 1.5406 | 8.05 |
| Mo Kα | 0.7107 | 17.44 |
| Co Kα | 1.7889 | 6.93 |
| Fe Kα | 1.9360 | 6.40 |
| Cr Kα | 2.2910 | 5.41 |
| Ag Kα | 0.5609 | 22.10 |
| Control | Description |
|---|---|
chkShowDScale | Draw a secondary top axis in d-spacing (Å) and switch the hover readout to d. Default: off. |
chkShowBackground | Fill the area under the pattern curve with a translucent brush. Default: on. |
chkLPCorrection | Apply Lorentz-polarization correction. Default: on. |
| Control | Description |
|---|---|
labelStatus | Status text ("Calculating…", "Calculated N peaks", "No peaks in range", errors). |
labelPeakCount | Number of peaks in the current range. |
labelHoverInfo | 2θ or d value under the cursor. |
labelHoverDetail | Nearest peak details: 2θ, d, (hkl), intensity. |
btnExport | Export dialog (CSV data or PNG/JPEG/BMP image). |
btnCopy | Copy the current plot bitmap to the clipboard (as bitmap + PNG bytes). |
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).
exp(−4 ln 2 · x²) where x = (2θ − 2θk) / FWHM.1 / (1 + 4x²).η · G + (1 − η) · L with η = PseudoVoigtEta (default 0.5).The ExportData() dialog offers two paths:
EncoderParameters), or BMP.btnCopy places the bitmap on the clipboard as both a DataFormats.Bitmap and a raw PNG stream.
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().
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.
Tal can define, edit and visualize two kinds of crystallographic planes:
Opened from Calculate → Planes... or from the main Planes toolbar button. It is a DockContent named PlanesWindow, docked left/right or floatable.
| Control | Description |
|---|---|
listViewPlanes | Details-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. |
btnNewPlane | Opens PlanePropertiesWindow for a new plane, shown non-modally (Show, not ShowDialog) so that the 3D view remains interactive for atom picking. |
btnEdit | Opens PlanePropertiesWindow for the selected plane. Enabled only when a row is selected. |
btnDelete | Asks for confirmation and removes the plane from the list. |
btnClose | Closes 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.
PlanePropertiesWindow is a modal-ish dialog (border fixed, center-parent) with a tab control for the two plane types and a common options group.
Picked atom (default), Picked molecule, Ring.All, Non-hydrogen, Hydrogen.
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:
selected flag and updates SelectionOrder and NumberSelected.IndividualMoleculeInCell.RingFinder.FindSmallestRing() with parameters maxRingSize = 12, requirePlanar = true, planarityTolerance = 0.15 Å. If no ring is found, falls back to selecting the single clicked atom.h, k, l. Range −20 to +20. Default all zero; if all three are zero the dialog refuses to save and shows a warning.| Control | Description |
|---|---|
chkShow | Plane visibility. Default: on. |
chkLabel | Show/hide the plane label. Default: on. |
txtLabel | Plane name. Default "mean:" or "hkl (hkl)" if empty. |
chkTransparent | Draw the plane with alpha = 0.35 instead of 0.85. Default: on. |
panelColor | Click 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.
The least-squares mean plane is computed by MeanPlaneCalculator.CalculateMeanPlane():
Molecule.atom[i].Coordinate is stored with −MoleculeShift applied, the shift is added back so the plane is independent of re-centering.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.
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:
x = 1/h, y = 1/k, or z = 1/l.
PlaneGeometry.ComputeIntersection(p1, p2) returns a PlaneIntersection struct with a point on the line, the normalized direction, and a half-length. The algorithm:
RingFinder finds the smallest ring containing a clicked atom:
maxRingSize and free of duplicate atoms are tested for planarity by PCA (smallest eigenvalue of the covariance matrix).| Property | Meaning |
|---|---|
Id | GUID. |
Name | User-facing name. |
Type | MeanPlane or HKL. |
Color | System.Drawing.Color. |
Visible, ShowLabel, Transparent | Display flags. |
SelectionMode | For mean planes: PickedAtom / PickedMolecule / Ring / All / NonHydrogen / Hydrogen. |
AtomIndices | Indices of atoms used for the mean plane. |
H, K, L | Miller indices (HKL planes). |
A, B, C, D | Plane equation coefficients. |
Center, Corners, Size | Geometry. |
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:
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:
ListView listing every face that survived the BFDH filtering.All parameters are exposed as WinForms controls in the top panel:
| Control | Parameter | Description |
|---|---|---|
_numMaxIndex | Max Miller index | Maximum 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. |
_numMinD | Min 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. |
_numSoftening | Softening strength | Controls 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). |
_cmbSofteningMode | Softening mode | Determines 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). |
The face table is a ListView with View.Details, FullRowSelect, and custom owner-drawing (DrawColumnHeader / DrawItem / DrawSubItem). The columns are:
| Column | Description |
|---|---|
| Face | The 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. |
| DistFrac | The 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. |
| Mult | The 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. |
| Description | A 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.
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:
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.
CalculateDSpacing. Planes with d < minDSpacing are discarded.
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.
RelativeGrowthRate = 1 / d.
RelativeGrowthRate / min(RelativeGrowthRate), so the largest face has DistFrac = 1.
SofteningMode (see section 10.5).
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.
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.
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.
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.
| Mode | Formula / Operation | Suggested strength | Effect |
|---|---|---|---|
None | identity | 1.00 | Pure BFDH. No compression. |
Clamp | if DistFrac > s: DistFrac = s | 2.50 | Hard cap on the growth rate. Small faces are pulled in toward the center. |
Sqrt | DistFrac = sqrt(DistFrac) | 1.00 | Square-root compression. Naturally reduces the spread of DistFrac values. |
Tanh | DistFrac = 1 + tanh((DistFrac - 1) · s) / s | 0.50 | Hyperbolic-tangent compression. Most flexible mode; the strength parameter controls the sharpness of the transition. |
Linear | DistFrac = 1 + (DistFrac - 1) / s | 5.00 | Linear 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.
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).
The polyhedron is built by MorphologyCrystal.Build:
ConvexPolyhedronFrac.FromBox) of half-edge 3.0 · scale Å in fractional space.DistFrac ascending (largest faces first).dScaled = DistFrac · ‖g‖ · faceOffset, where ‖g‖ is the norm of the reciprocal-lattice vector of (hkl).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).MaxSuccessfulClips = 14 successful clips is enforced to keep the polyhedron manageable.Molecule.PrepareUnitCell).NameToColor) using an FNV-1a hash mapped to HSV. The starting cube's "cell-*" faces are rendered in neutral grey.MorphologyCrystal.BuildSimpleTest(), which builds a rhombic dodecahedron-like test shape so the user can verify that the 3D viewer itself is working.
Camera instance; the default position is (0, 0, 100) looking at the origin.GetRotationMatrix() so the Wulff plot can follow it.40 / BoundingRadius) that keeps the crystal nicely framed.Flags.Instance.PerspectiveMode.TrackBar with a live numeric label).SpriteFont1 and respects Flags.LabelScale, Flags.LabelColor, Flags.LabelShadow, and Flags.LabelShadowColor. Faces with duplicate names share a single label at their combined centroid.ColorDialog."Zoom: X.XX× (mouse wheel)".Edges are drawn only along the true boundaries between two different faces. The algorithm is deliberately careful about numerical robustness:
EPS_world = BoundingRadius · 1e-4 (in the crystal's original scale, not after any zoom or scale transform).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.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.
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:
FaceName equals the requested name.255, 255, 0, 255) line segment for every edge that appears in exactly one triangle of the selected face.DepthStencilState.None so it always appears on top of the crystal, regardless of depth ordering.
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.
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.
min(screenW, screenH) / 2 · 0.85, multiplied by the current zoom factor. This keeps angular relationships correct regardless of the window aspect ratio.Show Equator is enabled.SpriteFont1, respecting the global label settings from Flags.MorphologyDocument.GetRotationMatrix() and multiplied by the same view matrix used by the 3D renderer (Matrix.CreateLookAt((0,0,100), Zero, Up)) so that the two views are visually consistent.t = d / maxD as (255·t, 100 + 100·(1 − |2t − 1|), 255·(1 − t)).Upper only, Lower only, Both (default). Filters the projected points by the sign of n.z.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.
Three separate VertexBuffers are maintained and rebuilt whenever the plot needs to be refreshed:
_circleVB — the outer circle (128 segments, LineList)._gridVB — polar grid, radial lines, and equator (LineList)._pointsVB — the face markers, drawn as triangle fans (upper hemisphere) or triangle strips (lower hemisphere rings) (TriangleList).
The plot background is taken from Flags.Instance.color1, matching the 3D viewport so that screenshots and the on-screen plot have consistent backgrounds.
Opened from the Advanced menu, the 🔬 Structure Overlay window is a powerful tool for comparing multiple molecular structures.
This tool is invaluable for comparing different conformations of a flexible molecule, or comparing a calculated structure with an experimental one.
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:
ListBox of overlay entries (custom owner-drawn so each entry can be colored with the molecule's actual color).None, Mirror, Inversion) and the status label.
Every entry in the overlay is an instance of OverlayMoleculeData, which holds:
| Property | Meaning |
|---|---|
Id | Unique integer ID within the overlay, assigned sequentially by OverlayManager._nextId. |
Name | User-facing name (e.g. "Molecule 1", "Selection 2", "Structure 3"). |
SourceName | Name of the document the entry was extracted from (used for the tooltip and the source column in the list). |
OriginalAtoms | Immutable 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. |
WorkingAtoms | The current, transformed copy of the atoms. This is what is rendered and what the fitting algorithm modifies in-place. |
OriginalReferencePoints | Array 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. |
WorkingReferencePoints | Current positions of the 3 reference points after transformations and fits. |
ReferencePointNames | Names of the 3 reference atoms (for display and debugging). |
ReferencePointIndices | Indices of the 3 reference atoms in the source molecule. |
HasReferencePoints | True if 3 reference points were successfully captured. |
CurrentTransformation | One of TransformationMode.None, Mirror, RotationY, RotationZ, SymmetryCenter. |
Color | The per-molecule tint applied to all atoms (a Vector3 RGB). |
IsVisible | Whether the molecule is rendered in the Overlay View. |
IsStructure | True for entries added via "Structure" (whole crystal). |
IsSelection | True 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).
OverlayManager owns the list of OverlayMoleculeData entries and exposes a small API used by the window. The public surface is:
IReadOnlyList<OverlayMoleculeData> OverlayMolecules — read-only view of the current entries.int TotalAtomCount — sum of WorkingAtoms.Count across all entries.event EventHandler MoleculesChanged — raised whenever the list or any entry's state changes, so the window and any overlay views can refresh.AddSelectedMolecule(doc, mode) — adds the whole molecule containing the selected atoms as a single entry.AddSelectedAtoms(doc, mode) — adds just the selected atoms as a single entry (marked IsSelection = true).AddSelectedStructure(doc, mode) — adds every atom of the document's molecule (including symmetry-generated ones) as a single entry (marked IsStructure = true).RemoveMolecule(id) — removes an entry by ID and raises MoleculesChanged.ClearOverlayMolecules() — removes all entries.ToggleVisibility(id) — flips the IsVisible flag.SetMoleculeColor(id, color) — sets the per-entry color and propagates it to every CustomColor field of the working atoms.SetMoleculeStyle(id, styleIndex) — sets the per-entry style (0 = CPK, 1 = Sticks, 2 = Balls & Sticks, 3 = Ellipsoids).SetTransformationForMolecule(id, mode) — the main transformation entry point (see below).FitMoleculeToPoints(source, sourcePoints, targetPoints) — the fitting entry point (see below).IsBaseMolecule(id) / GetBaseMoleculeId() — helpers for the "base molecule" concept (see section 11.6).
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:
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.
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.
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.
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).
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.
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).
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.
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.
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.
AddSelectedMolecule (and its siblings) calls AutoFitMolecule at the end. The new molecule is fitted onto the base molecule (the first entry in the list).RbTransformation_CheckedChanged calls AutoFitSelectedMolecule(), which re-fits the molecule onto the base using the (possibly mirrored) reference points.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.
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:
| Mode | Operation | Effect |
|---|---|---|
None | identity | No 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:
RbTransformation_CheckedChanged verifies the molecule is not the base (otherwise it restores the previous radio state and shows a "Cannot transform BASE molecule" status).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.AutoFitSelectedMolecule() is called, which re-fits the (now transformed) molecule onto the base using the reference points.
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.
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.
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.
OverlayViewWindow is created with the current OverlayManager and docked as a document.FixExistingOverlayView() detects it and injects the current manager if the existing one is null. This prevents the classic "Overlay View is empty after reloading the layout" bug.MoleculesChanged and clears its atom buffer.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.Advanced → Overlay Manager...). The list is initially empty.Show All to toggle visibility of the whole overlay.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.
Refer to section 4.6 for a detailed description of this 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.
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).
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.
The window is divided into four regions:
panelTop): a read-only txtCurrentStructure showing the identifier of the structure currently loaded into the window (e.g., the CIF data block name or the file name), plus a btnCustomise button reserved for future per-window customization.panelButtons): a vertical stack of section-switching buttons. Exactly one is highlighted (back-coloured with a light blue) at any time to indicate the active section.panelTableScroll): an auto-scrolling container hosting a single TableLayoutPanel (table). The panel is a BufferedPanel, so the table redraws without flicker when the user switches sections.panelBottom): a Close button.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:
| Button | Columns | Content |
|---|---|---|
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. |
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):
p1, p2 with errors e1, e2 and d = |p1 − p2|, then
σ_d² = (dx/d)²·(e1x² + e2x²)
+ (dy/d)²·(e1y² + e2y²)
+ (dz/d)²·(e1z² + e2z²)
where (dx, dy, dz) = p1 − p2.
σ_angle ≈ √2 · mean(|e1|, |e2|, |e3|) / min(l1, l2) [rad]
→ σ_angle · 180/π [deg]
clamped to [0.001°, 10°] to avoid absurd values for near-degenerate geometries.
σ_torsion ≈ √2 · mean(|e1|, |e2|, |e3|, |e4|) / l_BC · 180/π
where l_BC is the length of the central bond, clamped to [0.001°, 30°]. This reflects the fact that torsions are most sensitive to errors when the central bond is short.
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.
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.
Section objects in a Dictionary<string, Section>. When the structure changes (a new file is loaded or the active document switches), LoadStructure re-populates every section from the new Molecule, then re-fills the currently visible table.Angles and Torsions sections are special: they are populated lazily, only when the corresponding button is clicked. This avoids the cost of enumerating up to 50,000 rows when the user is only interested in, say, the unit cell parameters.PopulateSections and simply re-displayed.
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, α, β, γ.
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.
_cell_length_* and _cell_angle_* tags, or from the RES CELL/ZERR cards._cell_volume if present; otherwise computed from a, b, c, α, β, γ._space_group_IT_number, _space_group_name_H-M_alt / _symmetry_space_group_name_H-M; RES reads TITL and LATT._space_group_symop_operation_xyz loop; RES reads the SYMM cards. The full list is displayed verbatim in the Symmetry section._atom_site_* loop (label, type symbol, fractional x/y/z, x/y/z esd's, occupancy, Uiso, ADP type, disorder group, and the six Uij components when _atom_site_adp_type = Uani); RES reads the atom cards with the SFAC mapping._refine_ls_R_factor_gt → R1 observed._refine_ls_R_factor_all → R1 all._refine_ls_wR_factor_ref / _refine_ls_wR_factor_all → wR2 (all / observed)._refine_ls_goodness_of_fit_ref → GooF._refine_ls_number_reflns, _refine_ls_number_parameters, _refine_ls_number_restraints → reflection/parameter/restraint counts._refine_ls_shift/su_max, _refine_ls_shift/su_mean → shift/su max and mean._refine_diff_density_max, _refine_diff_density_min, _refine_diff_density_rms → residual electron density._refine_ls_weighting_scheme and _refine_ls_weighting_details → weighting scheme.REM lines (R1, wR2, GooF, Nref, Npar, Nres, shift/su max/mean, Δρ max/min)._diffrn_reflns_number → total reflections._diffrn_reflns_av_R_equivalents and the _reflns_number_gt / _reflns_number_total tags → observed reflections and threshold._diffrn_reflns_theta_min, _diffrn_reflns_theta_max → θmin, θmax._diffrn_reflns_resolution_high (or _reflns_d_resolution_high), _diffrn_reflns_resolution_low → resolution limits dmin, dmax._reflns_threshold_expression → the threshold column._diffrn_radiation_wavelength, _diffrn_radiation_type → wavelength and radiation type._diffrn_measurement_device_type → measurement device._diffrn_ambient_temperature → temperature (K)._exptl_crystal_colour, _exptl_crystal_description → crystal colour and shape._exptl_crystal_size_max, _exptl_crystal_size_mid, _exptl_crystal_size_min → crystal size._exptl_absorpt_coefficient_mu → absorption coefficient μ._exptl_absorpt_correction_type, _exptl_absorpt_correction_T_min, _exptl_absorpt_correction_T_max → absorption correction type and transmission range._exptl_crystal_density_diffrn → calculated density._chemical_formula_sum, _chemical_formula_weight, _chemical_name_common / _chemical_name_systematic → formula, formula weight and compound name._cell_formula_units_Z, _cell_formula_units_Z' → Z and Z'._exptl_crystal_F_000 or _cell_formula_units_Z × _chemical_formula_weight → F(000) if present._audit_creation_date, _audit_creation_method → audit lines._publ_section_title → publication title._journal_name_full / _journal_name_abbrev → journal name._journal_volume, _journal_page_first, _journal_year → journal reference._journal_doi or _publ_doi → DOI._chemical_melting_point, _chemical_compound_source etc. — read if present; otherwise omitted.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.
StructureWriter.Save(structure, path) inspects the file extension and dispatches to the appropriate writer. Returns true on success. Exceptions are caught and logged.
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.
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.
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.
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.
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.
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).
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.
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.
PovRayGenerator(Molecule) produces a complete POV-Ray 3.7 scene file.
global_settings { assumed_gamma 2.2; max_trace_level 10 }.right, and an angle converted from XNA's vertical FOV to POV-Ray's horizontal angle via angle = degrees(2·atan(tan(vFov/2)·aspect)). The CAMERA_FOV_STRETCH constant (1.0) is applied after conversion.Flags; a global ambient is declared as #declare GlobalAmbient.color2 (bottom) and color1 (top). A sky_sphere fallback is used if no camera is available.RuntimeIcosphere.BASE_RADIUS. Balls & sticks draw two cylinders per bond (each half colored by its atom). Ellipsoids are drawn as a unit sphere transformed by Transformation · Un · Scale(probability) · Translation(position), with the model scaled by rModel.ShowHydrogenIsoSpheres is false, H atoms are drawn as small isotropic spheres; otherwise as full ellipsoids.Flags.HBondColor; dash/gap lengths are read from flags.AngleZ and AngleXY and the color saturated by _saturationBoost.Presentation == 4, each Polihedrals.VertexesList entry is written as a mesh2 with per-vertex normals, transparent faces, and an inside_vector.Molecule.Planes are written as double-sided mesh2 quads (two triangles) with thin cylinder edges. Corners are shifted by −MoleculeShift to align with the atom coordinates.mesh2. If the draw mode is CavityColorMap and per-vertex colors are available, the mesh uses texture_list and per-face texture indices; otherwise a single transparent pigment is used.Scale(cam.OneOverZoom) · Rotate · Translate is emitted once at the end of the union.IsValidSegment rejects any cylinder shorter than MIN_SEGMENT_LENGTH = 1e-4 to avoid POV-Ray "degenerate cylinder" errors.IsFiniteVector rejects NaN/Inf coordinates.| Constant | Value | Effect |
|---|---|---|
_globalBrightness | 1.00 | Global light scale. |
_lightingContrast | 0.80 | Controls ambient/diffuse/phong factors. |
_phongBoost | 2.70 | Multiplier on the phong value. |
_saturationBoost | 2.30 | Color saturation boost (around the luminance axis). |
_emissionStrength | 0.18 | Self-illumination fraction of the base color. |
Transforms a StructureData from one setting of a space group to another equivalent setting (e.g. P2₁/c ↔ P2₁/n).
TransformTo(structure, targetSettingName) — main entry point. Returns a TransformResult with success flag, applied transform, target setting name, and message.ApplyTransformToStructure(structure, transform, targetName, wrapAtoms = false) — applies the transform in place: fractional coordinates, ADP tensors (U' = R·U·Rᵀ), cell parameters (M' = M·P), symmetry operations (regenerated for the target setting) and the space-group name.ApplyTransform(structure, transform) — low-level version; atoms + ADP + cell only, no symmetry/name update.ResolveTransform(structure, targetSettingName) — finds the transform matrix. It first tries a hard-coded lookup and verifies it against the operation sets; if that fails it falls back to a brute-force search over 3×3 integer matrices with determinant ±1 and 27 candidate shifts.GetStandardName(group) — returns the standard H-M name for a space group number (first entry with orientation 0).GetAvailableSettings(group) — returns all distinct setting names for a group.GetCurrentSetting(structure) — returns the setting name currently stored in the structure (matched by normalized name against the internal table).GetOperationStringsForSetting(group, settingName) — returns the list of operation strings for a given setting.ReadSymm) from the internal SymmetryElements table.null (nothing to do).M' = M · P where P = Q⁻¹, regenerate the symmetry operations, and set the new name.
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).
The application exposes a set of parsers, each of which populates a data container and then converts to a StructureData:
| Parser | Container | Notes |
|---|---|---|
CifParser | CifData | Full 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. |
ResParser | ResData | SHELX 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. |
XyzParser | XyzData | Standard XYZ: atom count, comment, element + Cartesian coordinates + optional partial charge. |
PdbParser | PdbData | CRYST1, ATOM/HETATM, ANISOU (converted from 10⁻⁴ Ų to Ų), CONECT. Cartesian coordinates are converted to fractional when a cell is present. |
MolParser | MolData | MDL molfile/SDF V2000. Fixed-width and space-separated fallbacks. Bond block and MDL charge codes. V3000 is detected but not fully supported. |
Mol2Parser | Mol2Data | Tripos 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. |
GaussianParser | GaussianData | Gaussian 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.
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.
Singleton that maintains the recent-files list persisted in %LocalAppData%\Tal\RecentFiles.json. See section 2.2.
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.
Tal uses a modern rendering pipeline with the following key stages:
Scene Quality settings (e.g., 2x means the RT is twice the width and height of the window).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.
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.
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.
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.
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.
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.
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.
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.
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.Calculate → VdW Scaling Analysis...) or programmatically via VdWScaleAnalyzer.RunAnalysisAsync(molecule, detector, scales).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.
ProgressChanged, StatusChanged) or the status bar. Each scale reports its cavity count and (when available) the largest fitted sphere.VdWAnalysisResults object is stored in VdWScaleAnalyzer.CurrentResults and can be saved as a TXT or HTML report.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).
The result object is the single source of truth for the report. Its main fields are:
| Field | Description |
|---|---|
VdWScales | List of scales that were actually processed. |
CavityResults | Dictionary<int cavityId, List<VdWAnalysisResult>> — one VdWAnalysisResult per (cavity, scale). |
CavityTracks | Dictionary<int cavityId, CavityTrackData> — a per-cavity summary of volume evolution across scales. |
CriticalEvents | List of CriticalEvent (disappearance, channel open/close, topology change) with the scale at which they occur. |
CrystalChannelData | Dictionary<int cavityId, Dictionary<string dir, ChannelPathResult>> — semi-periodic channels detected at VdW = 1.0. |
SpherePacking | Dictionary<int cavityId, SpherePackingResult> — the multi-sphere packing summary of every cavity. |
CellVolume, VoxelVolume, AtomCount, CellMass | Cell parameters captured before the run. |
StartTime, EndTime, TotalProcessingTimeMs | Timing information. |
FragmentationIndex | QSPR I_frag — see section 15.5. |
CollapseScale | QSPR f_collapse — the VdW scale at which the total cavity volume drops below 0.1% of the cell volume. |
ElectrostaticFrustration | QSPR Ω_Q — see section 15.5. |
HydrophobicFrustration | QSPR Ψ_H — see section 15.5. |
MeanPolarizability | QSPR ⟨Z_cav⟩ — mean Z-effective of all cavity voxels. |
MaxDipoleSensitivity | QSPR α_polar — maximum d|dipole|/d(scale). |
For every (cavity, scale) pair the analyzer stores both geometric and electrostatic data in a single VdWAnalysisResult:
Volume, VolumeError, SurfaceArea, SurfaceAreaError, Sphericity, VolumeToSurfaceRatio, RatioError, TriangleCount, VoxelCount, WallConnections.MeanCharge, ChargeVariance, MeanHydrophobicity, HydrophobicityVariance, MeanZEffective, DipoleMomentX/Y/Z, DipoleMomentMagnitude. The dipole is computed as the charge-weighted first moment of the cavity voxels.HasChannelsAtThisScale, ChannelDirections. This reflects the channel state at VdW = 1.0 (channels are only analyzed at that scale to keep the runtime manageable).
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.
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:
Six descriptors summarize the whole sweep:
| Descriptor | Formula (informal) | Interpretation |
|---|---|---|
I_frag | N_cavities_max / N_atoms | Structural fragmentation. Large values (> 0.5) suggest a highly porous, "beads-on-a-string" framework. |
f_collapse | First 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. |
α_polar | max over scales of (dipole(si+1) − dipole(si)) / Δs | Responsiveness to an external electric field. |
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.
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.
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.
For larger cavities the analyzer enumerates all local maxima of the SDF in parallel and then selects a non-overlapping subset:
min(resX,resY,resZ)/15, test the 6 face neighbours, and refine each candidate over a 3-voxel cube.MIN_SDF_VALUE_LARGE = 0.5 Å.max( (rcand + rsel) · SEPARATION_FACTOR / 10, MIN_DIST_FRAC = 0.05 ) in fractional coordinates. This yields a maximal packing of spheres.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.
| Field | Meaning |
|---|---|
MaxSphereRadius, EffectiveDiameter | Radius and diameter of the largest fitted sphere. |
SphereCount | Number of non-overlapping spheres. |
Spheres | List of PackedSphere — one per fitted sphere, with radius, center, fractional coords, wall flag and distance to the cavity center. |
UsableVolume | Sum of the volumes of all fitted spheres. |
PackingEfficiency | UsableVolume / CavityVolume (values around 0.74 indicate a close-packed cavity). |
HasChannels, ChannelInfo | Channel state at VdW = 1.0. |
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:
gasRadius ≤ 0.95 · maxSphereRadius.gasVolume / cavityVolume.0.70 · sizeMatch + 0.30 · polarityMatch, where sizeMatch = 1 − |gasRadius / maxRadius − 0.5|·2 and the polarity match depends on whether a suitable non-wall sphere is available.GOOD (score ≥ 0.7), FAIR (score ≥ 0.4), POOR.floor(usableVolume / gasVolume).
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.
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.
| Field | Meaning |
|---|---|
IsConnected | Whether a path through the periodic image was found. |
TotalLength, OriginalLength, SmoothedLength | Path lengths in Å. |
SmoothingError, SmoothingErrorPercent | RMS of the smoothing displacement. |
AverageWidth, MinWidth, MaxWidth | Local diameter, sampled along the path. |
EffectiveDiameter | AverageWidth · 0.8 — a conservative estimate of the "clear" diameter. |
Helicity | Signed angle change of the path direction. Small values (≈ 0°) indicate a straight channel; large values indicate a helical channel. |
CellCrossings | Number of times the path crosses a cell boundary. |
WorldPath, WorldPathXna | Path vertices in Å (System.Numerics and XNA variants). |
Properties | A dictionary of extra statistics from the multi-start analysis: TotalPathsFound, ValidPathsFound, AvgLength, AvgDiameter, AvgHelicity, MinLength, MaxLength, MinDiameter, MaxDiameter, MinHelicity, MaxHelicity, SuccessRate. |
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.distance + distance · (1 − SDF / SDF_max) · SDFWeight so that the path prefers to stay near the center of the cavity.SmoothingIterations, default 3). The RMS displacement is reported as SmoothingError.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.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.
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:
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.
| Method | Effect |
|---|---|
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. |
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:
ContactDistanceContactAtomContactAngleZContactAngleXYPrecomputedContactMatrixContactIsNew
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.
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".
CountVisible(molecule) — number of atoms with show == true.CountHidden(molecule) — NumberOfAtoms − CountVisible(molecule).Isolate a single molecule in a packing view:
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.RemoveHangingContacts(molecule) to clean up dangling contacts.Hide all copies of a single symmetry-equivalent atom:
HideSymmetryEquivalent(molecule, atomIndex).Undo all hides:
ShowAll(molecule).