Parse physical features from a .cif#

This is the main use of cifkit: turn one crystallographic .cif file into geometry and site-environment numbers you can plot, compare, or feed into a featurizer / machine-learning model.

What you get, in order:

  1. Useful structure facts (parse)

  2. Interatomic distances

  3. Coordination numbers - how each method decides CN (this page)

  4. Polyhedron metrics (volume, packing efficiency, …)

  5. Bond fractions and site mixing

  6. Optional polyhedron plot (static PNG or interactive 3D)

Not from the .cif: composition / elemental descriptors (atomic weight, electronegativity, Mendeleev number, …) come from the separate OLED table in OLED tutorial - curated Oliynyk elemental data from the dataset paper (Data in Brief), not values read out of the CIF file.

GdSb Sb-centered coordination polyhedron, CN=6

Fig. 2 Static export. Sb-centered polyhedron in GdSb (CN=6) from plot_polyhedron on the packaged demo file.#

Interactive polyhedron (drag to rotate)#

Same shell as above - real neighbor coordinates from Example.GdSb_file_path, method dist_by_shortest_dist, CN = 6. Use this when a static PNG is not enough to see the octahedron.

In your own notebook or script you get a live PyVista window with:

cif.plot_polyhedron("Sb", is_displayed=True)  # interactive desktop window

1. Load a CIF and see what was parsed#

import pandas as pd
from cifkit import Cif, Example

cif = Cif(Example.GdSb_file_path)

props = pd.DataFrame(
    [
        ("file_name", cif.file_name),
        ("formula", cif.formula),
        ("structure", cif.structure),
        ("space_group_name", cif.space_group_name),
        ("space_group_number", cif.space_group_number),
        ("unitcell_lengths", cif.unitcell_lengths),
        ("unitcell_angles (rad)", cif.unitcell_angles),
        ("site_labels", cif.site_labels),
        ("unique_elements", sorted(cif.unique_elements)),
        ("composition_type", cif.composition_type),
        ("tag", cif.tag),
        ("db_source", cif.db_source),
        ("unitcell_atom_count", cif.unitcell_atom_count),
        ("supercell_atom_count", cif.supercell_atom_count),
    ],
    columns=["attribute", "value"],
)
print(props.to_string(index=False))

attribute

value

file_name

GdSb.cif

formula

GdSb

structure

NaCl

space_group_name

Fm-3m

space_group_number

225

unitcell_lengths

[6.21, 6.21, 6.21]

unitcell_angles (rad)

[1.5708, 1.5708, 1.5708]

site_labels

[‘Sb’, ‘Gd’]

unique_elements

[‘Gd’, ‘Sb’]

composition_type

2

tag

rt

db_source

PCD

unitcell_atom_count

8

supercell_atom_count

1000

By default the constructor preprocesses for gemmi compatibility, builds a 3×3×3 supercell, and defers coordination work until you ask (compute_CN=False).

db_source is detected from the file (here PCD). cifkit is tested against common crystallographic exports - ICSD, COD, PCD, Materials Project-style (MP), CCDC/CSD, and Materials Studio (MS) - with source-specific fixes (for example ICSD copyright lines, PCD author loops). Mixed folders can share one pipeline.

2. Interatomic distances#

Distances come from the supercell neighbor search - before coordination methods run.

print("shortest_distance:", cif.shortest_distance)
print("shortest_site_pair_distance:", cif.shortest_site_pair_distance)
print("shortest_bond_pair_distance:", cif.shortest_bond_pair_distance)
shortest_distance: 3.105
shortest_site_pair_distance: {'Sb': ('Gd', 3.105), 'Gd': ('Sb', 3.105)}
shortest_bond_pair_distance: {('Gd', 'Sb'): 3.105, ('Gd', 'Gd'): 4.391, ('Sb', 'Sb'): 4.391}

As a table - shortest distance between each element-pair type:

bond_d = pd.DataFrame(
    [
        {"pair": str(k), "shortest (Å)": v}
        for k, v in cif.shortest_bond_pair_distance.items()
    ]
)
print(bond_d.to_string(index=False))

pair

shortest (Å)

(‘Gd’, ‘Sb’)

3.105

(‘Gd’, ‘Gd’)

4.391

(‘Sb’, ‘Sb’)

4.391

Unique neighbor distances from site Gd (first shell and beyond):

seen = set()
rows = []
for label, dist, *_ in cif.connections["Gd"]:
    key = (label, round(dist, 4))
    if key in seen:
        continue
    seen.add(key)
    rows.append({"neighbor": label, "distance (Å)": round(dist, 4)})
dist_table = pd.DataFrame(rows)
print(dist_table.head(8).to_string(index=False))

neighbor

distance (Å)

Sb

3.105

Gd

4.391

Sb

5.378

Gd

6.21

Sb

6.943

Gd

7.606

Gd

8.782

Sb

9.315

These ordered shells are the raw input for every CN method.

3. Coordination numbers - what they are and how each is determined#

What is CN here?#

For each crystallographic site label, cifkit decides how many neighbors belong in the first coordination shell. That integer is the coordination number (CN). The shell defines the vertices of the coordination polyhedron (edges, faces, volume, packing efficiency).

CN is not a single universal number in messy intermetallics: different reasonable normalizations of distance can put the “largest gap” in different places. cifkit therefore runs up to four methods and then picks a best method per site using polyhedron geometry.

Algorithm (same skeleton for every method)#

For each site label:

  1. Order neighbors by increasing interatomic distance (from the supercell search; first ~20 neighbors are considered).

  2. Normalize each neighbor distance by a method-specific scale (see table below). Sorted normalized values form a step-like curve.

  3. Find the largest gap between consecutive normalized distances. The index of that gap is the CN: keep the first N neighbors, drop everything beyond the gap.

  4. Those N neighbors are the shell used for bond fractions and polyhedron metrics for that method.

If radius data are missing or the site is not full occupancy, only dist_by_shortest_dist runs; otherwise all four methods run.

The four methods (what the scale is)#

Method key

Normalize each neighbor distance by…

Intuition

dist_by_shortest_dist

the site’s shortest neighbor distance

Pure geometry: “how many times longer than the nearest bond?”

dist_by_CIF_radius_sum

sum of CIF radii of the central + neighbor elements

Bonds scaled by tabulated elemental sizes

dist_by_CIF_radius_refined_sum

sum of refined CIF radii for the pair

Same idea after a structure-aware radius tweak

dist_by_Pauling_radius_sum

sum of Pauling CN12 metallic radii

Metallic-radius scale (CN = 12 reference)

Radii for the last three methods come from cifkit’s elemental radius tables (not from OLED’s full property set, though CIF/Pauling radii also appear there as elemental columns). The geometry CN is always computed from the CIF structure + these scales.

Run it and read the per-method table#

cif.compute_CN()  # or Cif(path, compute_CN=True)

rows = []
for site, methods in cif.CN_max_gap_per_site.items():
    for method, d in methods.items():
        rows.append(
            {"site": site, "method": method, "CN": d["CN"], "max_gap": d["max_gap"]}
        )
print(pd.DataFrame(rows).to_string(index=False))

site

method

CN

max_gap

Sb

dist_by_shortest_dist

6

0.414

Sb

dist_by_CIF_radius_sum

6

0.567

Sb

dist_by_CIF_radius_refined_sum

6

0.581

Sb

dist_by_Pauling_radius_sum

6

0.464

Gd

dist_by_shortest_dist

6

0.414

Gd

dist_by_CIF_radius_sum

18

0.441

Gd

dist_by_CIF_radius_refined_sum

18

0.453

Gd

dist_by_Pauling_radius_sum

18

0.366

How to read this: for Sb every method agrees (CN = 6). For Gd the shortest-distance method finds a gap after 6 neighbors, while radius-based methods find a larger gap after 18 - classic rock-salt behavior where the second shell is still close on some scales. That is why cifkit keeps all four results and then chooses a best method.

How the “best” method is chosen#

For each method that produced a CN ≥ 4, cifkit builds the convex hull of the shell neighbors and measures how far the central atom sits from the average of the vertex positions. The method with the smallest that distance is method_used in CN_best_methods (most “centered” polyhedron). Metrics (volume, packing efficiency, faces, …) are reported for that winning method only.

Low-level helpers: Coordination API.

4. Polyhedron metrics (best method)#

best = pd.DataFrame(
    [
        {
            "site": site,
            "method_used": m["method_used"],
            "CN (vertices)": m["number_of_vertices"],
            "edges": m["number_of_edges"],
            "faces": m["number_of_faces"],
            "volume": round(m["volume_of_polyhedron"], 3),
            "packing_eff": round(m["packing_efficiency"], 3),
        }
        for site, m in cif.CN_best_methods.items()
    ]
)
print(best.to_string(index=False))

site

method_used

CN (vertices)

edges

faces

volume

packing_eff

Sb

dist_by_shortest_dist

6

12

8

39.914

0.605

Gd

dist_by_shortest_dist

6

12

8

39.914

0.605

Octahedra (6 / 12 / 8) with packing efficiency 0.605 - rock salt. method_used is the winner of the center-to-average test above.

Neighbors in the CN shell for Gd (min-dist method):

conns = cif.CN_connections_by_min_dist_method["Gd"]
neighbors = pd.DataFrame(
    [
        {"i": i + 1, "neighbor": c[0], "distance (Å)": round(c[1], 4)}
        for i, c in enumerate(conns)
    ]
)
print(neighbors.to_string(index=False))

i

neighbor

distance (Å)

1

Sb

3.105

2

Sb

3.105

3

Sb

3.105

4

Sb

3.105

5

Sb

3.105

6

Sb

3.105

5. Bond fractions and site mixing#

bonds = pd.DataFrame(
    [
        {"pair": str(k), "fraction": v}
        for k, v in cif.CN_bond_fractions_by_min_dist_method.items()
    ]
)
print(bonds.to_string(index=False))
print("site_mixing_type:", cif.site_mixing_type)

pair

fraction

(‘Gd’, ‘Sb’)

1.0

site_mixing_type: full_occupancy

Mixing info at the label-pair level is also available as mixing_info_per_label_pair (useful when sites are partially occupied or mixed).

6. Render the polyhedron#

for label in cif.site_labels:
    cif.plot_polyhedron(label, is_displayed=False, output_dir="polyhedrons")
# → GdSb_Sb.png, GdSb_Gd.png

Mode

How

Static PNG (docs, papers)

is_displayed=False, write output_dir

Interactive desktop (PyVista)

is_displayed=True

Interactive in these docs

iframe widget above (same GdSb / Sb shell)

A richer polyhedron from the JOSS paper (ErCoIn₅, In1, CN=12):

ErCoIn5 In1 polyhedron CN=12

Fig. 3 JOSS Figure 1 (left). ErCoIn₅ around In1 (CN=12).#

API reference#

What else?#

Topic

Where

Radii used in CN methods (radius_values, radius_sum)

Cif API

Site mixing types beyond full occupancy

site_mixing_type, mixing_info_per_label_pair; API

Folder-level CN / structure histograms before ML

Statistics over many CIFs

Elemental properties for ML (OLED table - not from the CIF)

OLED tutorial · Data in Brief

Downstream geometry featurizer built on cifkit

SAF

Next#


Notes (demo data, tables, citation)#

  • Numbers on this page are real outputs on the packaged GdSb demo (Example.GdSb_file_path).

  • Tables in the walkthroughs are built with pandas → Markdown so you can copy the same pattern into a notebook. They show geometry features from the CIF, not elemental property rows (those live in OLED / Data in Brief).

  • If these geometry features were useful, consider citing cifkit (JOSS 10.21105/joss.07205; BibTeX in CITATION.txt). Full publication list: home page.