Skip to content

Recognize glycans and draw SNFG diagrams

Glycan adds sugar assignments, stereochemistry, ring analysis, and glycosidic links to a molecular State. Start with a geometry, inspect its assignments, then use the same object for torsions, puckering, trajectory analysis, and an SNFG figure.

The examples use benchmark geometries bundled with this site. Every runnable block is independent; change its parameters and run it again.

1. Identify the sugars in a geometry

This trisaccharide contains three residues. Load it and inspect the information that connects molecular coordinates to chemical names.

from matmalib import Glycan

glycan = Glycan.from_file("data/fucosylated_trisaccharide.xyz")
print("Atoms:", glycan.NAtoms)
print("Residues:", len(glycan.monomers))
for monomer, identity in zip(glycan.monomers, glycan.identify_sugars()):
    print(f"Residue {monomer.id}: {identity}")
    print("  ordered ring:", monomer.ring_indices)
    print("  named positions:", monomer.atom_assignment)
print("Glycosidic links:", glycan.glycosidic_bonds)
print("Unassigned atoms:", len(glycan.unassigned_atoms))

fig, ax = glycan.snfg(show_names=True)
ax.set_title("GlcNAc, Gal, and Fuc from one geometry")

Expect GlcNAc (D, alpha), Gal (D, beta), and Fuc (L, alpha), in that monomer order. There are two glycosidic links. The SNFG diagram places the reducing end at the right. A dashed link indicates an alpha donor; a solid link indicates beta or an unknown anomer.

Experiment: change the filename to data/glycan.xyz. The larger example contains 11 monomers and 10 links. data/glucose.xyz contains one beta-D-glucose residue.

Choose a construction path

Entry point Use it when
Glycan(atoms, ...) You already have a list of Atom objects.
Glycan.from_state(state, ...) You have loaded a State and want to retain its metadata and atoms.
Glycan.from_file(path, software="xyz", ...) You want to load a geometry and assign sugars in one call.
Space.from_xyz(path, glycans=True) You want every XYZ frame stored as a separately detected Glycan.

Glycan.from_file() reads the last saved frame for software="xyz" or "cp2k". It does not return a trajectory. Other software values use the corresponding State.from_file reader. For a trajectory, establish assignments once and reuse them as shown in Trajectories; repeatedly detecting bonds in distorted frames can change or fail assignments.

from_state shares the original Atom objects. Editing an atom through the glycan changes that atom in the source state too. Use copy.deepcopy(state) first when independent geometry edits are needed.

What automatic assignment does

Assignment uses a supplied connectivity matrix (conn_mat) or NetworkX graph when available. Otherwise it infers connectivity with distXX=1.65 for heavy atoms and distXH=1.15 for hydrogen connections. These defaults assume coordinates in angstroms. Hydrogen assignment uses the nearest eligible heavy atom.

conn_mat takes precedence over graph. It must have shape (n_atoms, n_atoms), finite nonnegative entries, symmetry, and a zero diagonal; any nonzero entry denotes a bond. A supplied graph must be a simple undirected graph with exactly nodes 0 through n_atoms-1 and no self-loops. Supplied connectivity is copied. Distance cutoffs must be finite and positive when inferring bonds.

A candidate sugar ring has five or six atoms, exactly one ring oxygen, carbon at the other ring positions, and exocyclic O/N/S substituents on at least two ring carbons. Bare cyclic ethers do not meet these criteria. Detection supports both pyranose and furanose backbones and distinguishes aldose from ketose numbering. Ambiguous anomeric positions or overlapping rings raise ValueError.

Automatic ring order is oxygen first, then the anomeric carbon, then the remaining carbons around the ring. The anomeric carbon is C1 for an aldose and C2 for a ketose. Monomers are ordered by their smallest ring atom index; this order is not a sequence nomenclature or a reducing-end order.

Attribute Meaning
glycan.atoms, glycan.xyz Full molecular system, including unassigned atoms.
glycan.rings One ordered tuple of atom-list positions per sugar ring.
glycan.monomers / glycan.fragments The same list of Fragment objects, one per monomer.
monomer.atom_indices / .atoms All positions / atoms assigned to the monomer, including its substituents and hydrogens.
monomer.ring_indices / .ring_atoms Ordered ring positions / atoms.
monomer.ring_type "pyranose" for six atoms, "furanose" for five.
monomer.atom_assignment Chemical names such as "O", "C1", "C6" mapped to positions in glycan.atoms.
monomer.named_atoms The corresponding names mapped directly to Atom objects.
monomer.graph Induced connectivity subgraph, when a graph is available.
glycan.unassigned_atoms Atoms outside all monomers, often disconnected solvent or ions.
glycan.glycosidic_bonds Dictionaries with donor, acceptor, donor_atom, acceptor_atom, and bridge_atom.

All indices here are zero-based positions in the atom list, not Atom.id values. donor and acceptor instead index glycan.monomers. A bridging atom belongs to the acceptor monomer. For an anomeric-to-anomeric link, the lower monomer index is treated as donor. Covalently attached substituents stay with a monomer; a protein or aglycone may therefore need explicit boundaries.

2. Control assignments for unusual chemistry

Supply rings to control ring order while retaining automatic membership detection. Supply both rings and monomers to bypass detection completely. The following example reconstructs a known glucose ring explicitly, then checks that the geometry still gives the same pucker.

from matmalib import Glycan, Space

state = Space.from_xyz("data/glucose.xyz").states[0]
ring = (0, 10, 9, 7, 6, 8)  # O, C1, C2, C3, C4, C5 for this fixture only
glycan = Glycan.from_state(
    state,
    rings=[ring],
    monomers=[list(range(len(state.atoms)))],
)
print("Ring:", glycan.rings[0])
print("Pucker:", glycan.identify_ring_puckers()[0]["label"])
print("Shared atom:", glycan.atoms[0] is state.atoms[0])
print("Automatic chemical names:", glycan.monomers[0].atom_assignment)
print("Sugar identification:", glycan.identify_sugars()[0]["name"])

Expect pucker 4C1, shared atom True, an empty name dictionary, and sugar identity Unknown. This illustrates an important boundary: bypassing detection provides geometric membership, but does not generate named backbone atoms or glycosidic link metadata. Sugar naming and torsion analysis depend on those chemical assignments. If the chemistry is supported and only inferred bonds are wrong, prefer supplying trusted conn_mat connectivity and allowing detection to run.

For each explicit ring, provide a nonempty, unique, in-range integer sequence of length five or six in cyclic order. Rings cannot share atoms. Supply one monomer membership per ring; each membership must contain its ring and cannot share atoms with other monomers. monomers without rings is rejected. Unlisted atoms remain in the state as unassigned_atoms.

With a trusted matrix in a local workflow:

import numpy as np
from matmalib import Glycan, State

state = State.from_file("calculation.log", software="gaussian")
connectivity = np.load("connectivity.npy")  # shape (n_atoms, n_atoms), same atom order
glycan = Glycan.from_state(state, conn_mat=connectivity)

3. Interpret stereochemistry

identify_sugars() returns one dictionary per monomer with name, configuration, anomer, and ring_type. Configuration is "D", "L", or None; anomer is "alpha", "beta", or None.

Naming recognizes the eight aldohexopyranose stereochemistries (All, Alt, Glc, Man, Gul, Ido, Gal, Tal), supported C2 amino and N-acetyl derivatives, C6 deoxy derivatives, and uronic acids. It works with or without explicit hydrogens. Names describe the base residue, not a complete inventory of protecting groups or modifications.

Detection of a sugar ring and identification of its residue name are separate steps. Furanoses, ketoses, unsupported substituents, ambiguous neighbors, and degenerate stereocenters can produce Unknown even when ring assignment and puckering are available. Missing identity fields do not justify assuming a particular configuration or anomer.

4. Measure glycosidic and backbone torsions

Inspect the exact quadruplets alongside angles when comparing matmalib with another analysis package.

from matmalib import Glycan

glycan = Glycan.from_file("data/fucosylated_trisaccharide.xyz")
for link in glycan.glycosidic_torsions():
    print(f"Residue {link['donor']} -> {link['acceptor']}")
    print(f"  phi={link['phi']:.3f}, psi={link['psi']:.3f} {link['angle_unit']}")
    print("  angle definitions:", link["atom_indices"])
for item in glycan.backbone_torsions():
    omega = item["omega"]
    print("Backbone", item["monomer"], "omega:",
          "unavailable" if omega is None else f"{omega:.3f}")

The first link has phi approximately 60.520° and psi 114.569°. Fucose lacks a terminal C6 oxygen, so its backbone omega is None.

Angle Heavy-atom definition
Glycosidic phi Donor ring O → donor anomeric C → linking O → acceptor C.
Glycosidic psi Donor anomeric C → linking O → acceptor C → preceding acceptor backbone C; at an anomeric acceptor, the last atom is its ring O.
Glycosidic omega For a terminal-carbon linkage: linking O → terminal C → ring C → ring O. The key is absent when the linkage has no such omega.
Backbone omega Ring O → last ring C → terminal C → terminal O, one result per monomer. A missing or ambiguous terminal oxygen gives None.

Both methods accept degrees=False and record angle_unit. Glycosidic results include the original link metadata, donor/acceptor carbon-position strings, and atom_indices by angle name. Backbone results include monomer, omega, and its quadruplet or None. Collinear/degenerate dihedrals raise an error. These methods do not unwrap periodic coordinates.

Experiment: load data/glycan.xyz and select links with acceptor_position == "6". These include exocyclic omega, unlike the 1–4 linkages above.

5. Customize an SNFG figure

glycan.snfg(...), draw_snfg(glycan, ...), and Plot().snfg(glycan, ...) return (figure, axes). The Plot wrapper also stores them as plot.fig and plot.ax. They create Matplotlib vector shapes; no downloaded icon files are needed.

import matplotlib.pyplot as plt
from matmalib import Glycan, draw_snfg

glycan = Glycan.from_file("data/fucosylated_trisaccharide.xyz")
fig, axes = plt.subplots(1, 2, figsize=(10, 3))
draw_snfg(glycan, ax=axes[0], show_names=True,
          bond_length=1.0, symbol_size=0.25)
axes[0].set_title("Automatic positions")
draw_snfg(glycan, ax=axes[1], show_names=True,
          positions={0: (0, 0), 1: (-1.5, 0.8), 2: (-3, 0)},
          symbol_linewidth=1.5, linkage_linewidth=1.5)
axes[1].set_title("Explicit positions")
fig.tight_layout()
Option Default Effect
labels None Sequence of one standard abbreviation per monomer, or a mapping such as {0: "GlcNAc"} overriding selected names.
ax None Draw into existing axes; otherwise create a figure.
positions None Mapping of every monomer index to a finite, distinct (x, y) position.
show_linkages True Show alpha/beta and donor–acceptor carbon-position text.
show_names False Show residue abbreviations below symbols.
symbol_size 0.32 Symbol size in drawing coordinates.
symbol_linewidth 2.0 Symbol border width in points.
linkage_linewidth 2.0 Link width in points.
bond_length 1.2 Center-to-center length for horizontal and vertical automatic links.
diagonal_bond_scale 1.2 Multiply diagonal link lengths by this factor; use 1.0 for equal lengths.

Sizes and widths must be finite and positive. Disabling linkage text does not change dashed/solid bond styles. Manual labels change the displayed symbol name; they retain detected stereochemistry. Use overrides only when you know the chemical identity. Unknown abbreviations raise ValueError; use "Glc" rather than "Glucose". Nac spelling is normalized to NAc.

Automatic directions point from acceptor to donor: position 2 down, 3 lower left, 4 left, 6 upper left, and 8 up. The same directions apply within nested branches. Disconnected components are supported. Cycles, multiply linked topologies, unmapped acceptor positions, and overlapping automatic positions require explicit positions; connections between manual positions are straight lines.

Nondefault D/L configurations and furanose rings are annotated on symbols. Undetermined anomers appear as ? in linkage text. Unsupported residues use a white hexagon (Unknown).

Available symbol families

The renderer supports more names than automatic identification. These names are accepted for explicit labels:

Shape family Accepted abbreviations
Circle Hex, Glc, Man, Gal, Gul, Alt, All, Tal, Ido
Square HexNAc, GlcNAc, ManNAc, GalNAc, GulNAc, AltNAc, AllNAc, TalNAc, IdoNAc
Crossed square HexN, GlcN, ManN, GalN, GulN, AltN, AllN, TalN, IdoN
Divided diamond HexA, GlcA, ManA, GalA, GulA, AltA, AllA, TalA, IdoA
Triangle dHex, Qui, Rha, 6dGul, 6dAlt, 6dTal, Fuc
Divided triangle dHexNAc, QuiNAc, RhaNAc, 6dAltNAc, 6dTalNAc, FucNAc
Rectangle ddHex, Oli, Tyv, Abe, Par, Dig, Col
Star Pen, Ara, Lyx, Xyl, Rib
Diamond Non, Kdn, Neu5Ac, Neu5Gc, Neu, Sia
Flat diamond ddNon, Pse, Leg, Aci, 4eLeg
Hexagon Unknown, Bac, LDmanHep, Kdo, Dha, DDmanHep, MurNAc, MurNGc, Mur
Pentagon Assigned, Api, Fru, Tag, Sor, Psi

Export a figure locally

from matmalib import Glycan

glycan = Glycan.from_file("tests/data/fucosylated_trisaccharide.xyz")
fig, ax = glycan.snfg(show_names=True)
fig.savefig("glycan.svg", bbox_inches="tight")
fig.savefig("glycan.pdf", bbox_inches="tight")
fig.savefig("glycan.png", dpi=300, bbox_inches="tight")

SVG and PDF retain vector symbols. Continue with ring puckering to describe each ring's conformation or conformers to compare complete molecular geometries.

Diagnose assignment and drawing errors

Symptom What to check
No supported sugar rings found Coordinate units, unwrapped geometry, inferred bonds, and whether the chemistry meets detection criteria. Supply trusted connectivity when available.
Ambiguous anomeric carbon or overlapping ring candidates Inspect topology and protecting groups. Explicit rings and complete monomer memberships bypass ambiguous detection.
Correct ring but identity Unknown Naming supports a narrower chemistry set than ring detection. Inspect named backbone atoms and stereocenters; a display label override does not add chemical assignments.
SNFG asks for positions Automatic directions do not cover this acceptor position, topology, or overlap. Supply finite distinct positions for every monomer.
Torsions require named backbone atoms Explicit memberships bypassed chemical detection, or a linkage lacks a supported backbone definition.
Unexpected solvent/aglycone in a monomer Membership follows connectivity after glycosidic separation. Use explicit boundaries for a covalently attached noncarbohydrate component.