Source code for topo_metrics.io.cgd
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import numpy as np
import numpy.typing as npt
from pymatgen.core.lattice import Lattice as PymatgenLattice
[docs]
def parse_cgd(
filename: Path | str,
) -> tuple[
PymatgenLattice, list[str], npt.NDArray[np.floating] | None, list[list[Any]]
]:
"""
Parses a CGD file and extracts lattice, atoms, and edges.
Parameters
----------
filename
The path to the CGD file.
Returns
-------
- lattice: The lattice of the network.
- atom_labels: The labels of the atoms.
- all_coords: The fractional coordinates of the atoms.
- edges: The list of edges in the network.
"""
if not os.path.exists(filename):
raise FileNotFoundError(f"File '{filename}' not found.")
lattice = None
all_coords = []
atom_labels = []
edges = []
with open(filename) as file:
for line in file:
tokens = line.split()
if not tokens:
continue
match tokens[0]:
case "cell":
lattice = np.array(
list(map(float, tokens[1:])), dtype=np.float64
)
case "atom":
label, coords = tokens[1], list(map(float, tokens[3:]))
atom_labels.append(label)
all_coords.append(np.array(coords, dtype=np.float64))
case "edge":
edges.append([tokens[1]] + list(map(float, tokens[2:5])))
all_coords = np.array(all_coords) if all_coords else None
assert lattice is not None, "Lattice information missing in CGD file."
lattice = PymatgenLattice.from_parameters(*lattice)
return lattice, atom_labels, all_coords, edges
[docs]
def process_neighbour_list(
edges: list[list[Any]],
atom_frac_coords: npt.NDArray[np.floating] | None,
atom_labels: list[str],
) -> npt.NDArray[np.int_]:
"""
Processes edges and determines node connectivity.
Parameters
----------
edges
The list of edges in the network.
atom_frac_coords
The fractional coordinates of the atoms.
atom_labels
The labels of the atoms.
Returns
-------
The neighbour list of the network, where each row corresponds to a pair of
connected nodes and the applied image shift to the second node.
"""
# create a mapping from atom label to index for fast lookups
label_to_index = {label: i + 1 for i, label in enumerate(atom_labels)}
neighbour_list = []
for edge in edges:
node_label, frac_coords = edge[0], np.array(edge[1:], dtype=np.float64)
if node_label not in label_to_index:
raise ValueError(f"Unrecognised atom label in edge: {node_label}")
wrapped_coords = frac_coords % 1.0
img = (frac_coords - wrapped_coords).astype(int)
dists = (
np.linalg.norm(atom_frac_coords - wrapped_coords, axis=1)
if atom_frac_coords is not None
else None
)
closest_atom_idx = np.argmin(dists) if dists is not None else None
if dists is not None and dists[closest_atom_idx] > 1e-4:
raise ValueError(f"Could not find closest atom to edge {edge}.")
node1 = label_to_index[node_label]
node2 = closest_atom_idx + 1 if closest_atom_idx is not None else None
if node2 is not None:
neighbour_list.append([node1, node2, *img])
neighbour_list = (
np.array(neighbour_list, dtype=int)
if neighbour_list
else np.empty((0, 5), dtype=int)
)
return neighbour_list