Skip to content

Add lightweight PDB (Protein Data Bank) file support#7926

Open
behroozazarkhalili wants to merge 5 commits into
huggingface:mainfrom
behroozazarkhalili:feat/pdb-support
Open

Add lightweight PDB (Protein Data Bank) file support#7926
behroozazarkhalili wants to merge 5 commits into
huggingface:mainfrom
behroozazarkhalili:feat/pdb-support

Conversation

@behroozazarkhalili

@behroozazarkhalili behroozazarkhalili commented Dec 31, 2025

Copy link
Copy Markdown

Summary

This PR adds support for loading PDB (Protein Data Bank) files with load_dataset(), following the ImageFolder pattern where one row = one structure.

Based on feedback from @lhoestq in #7930, this approach makes datasets more practical for ML workflows:

  • Each row is independent, enabling train/test splits and shuffling
  • Easy to add labels (folder-based) and metadata (metadata.jsonl)
  • Compatible with Dataset Viewer (one 3D render per row)

Architecture

Uses FolderBasedBuilder pattern (like ImageFolder, AudioFolder):

class PdbFolder(FolderBasedBuilder):
    BASE_FEATURE = ProteinStructure
    BASE_COLUMN_NAME = "structure"
    EXTENSIONS = [".pdb", ".ent"]

New ProteinStructure Feature Type

# Arrow schema for lazy loading
pa.struct({"bytes": pa.binary(), "path": pa.string()})

# Decoded: returns structure file content as string
dataset = load_dataset("pdb", data_dir="structures/")
print(dataset[0]["structure"])  # Full PDB file content

Supported Extensions

.pdb, .ent

Usage

from datasets import load_dataset

# Load from directory
dataset = load_dataset("pdb", data_dir="protein_structures/")

# Load with folder-based labels
# structures/
#   enzymes/
#     1abc.pdb
#   receptors/
#     2def.pdb
dataset = load_dataset("pdb", data_dir="structures/")
print(dataset[0])  # {"structure": "HEADER...", "label": "enzymes"}

# Load with metadata
# structures/
#   1abc.pdb
#   metadata.jsonl  # {"file_name": "1abc.pdb", "resolution": 2.5}
dataset = load_dataset("pdb", data_dir="structures/")
print(dataset[0])  # {"structure": "HEADER...", "resolution": 2.5}

# Drop labels or metadata
dataset = load_dataset("pdb", data_dir="structures/", drop_labels=True)
dataset = load_dataset("pdb", data_dir="structures/", drop_metadata=True)

Test Results

All 28 PDB tests + 15 ProteinStructure feature tests pass.

Related PRs

cc @lhoestq @georgia-hf

@lhoestq

lhoestq commented Jun 5, 2026

Copy link
Copy Markdown
Member

Hi ! same comment as in #7925: is there a lib in python that parses the text of the files ? Maybe it would be more useful to return a parsed object rather than just the data as string

- Add zero-dependency pure Python parser for PDB format
- Support ATOM and HETATM record types with configurable filtering
- Handle fixed-width column parsing per official PDB specification
- Support gzip, bzip2, and xz compression via magic bytes detection
- Support .pdb and .ent file extensions
- Add comprehensive test suite with 24 tests
- Add documentation to loading.mdx

Columns include: atom_serial, atom_name, residue_name, chain_id,
residue_seq, x, y, z, occupancy, temp_factor, element, and more.

Part of the bioinformatics file format support series.
Based on reviewer feedback from @lhoestq, refactor the PDB loader to follow
the ImageFolder pattern where each row contains one complete protein structure
file, rather than one row per atom.

Changes:
- Add ProteinStructure feature type for lazy loading structure files
- Refactor PdbFolder to use FolderBasedBuilder with folder/metadata support
- Support automatic label inference from directory names
- Support metadata files (CSV/JSONL) for additional annotations
- Simplify from ~400 lines atom-parser to ~55 lines folder-based builder

This approach matches how researchers typically work with protein structures:
- One structure = one data point
- Supports downstream tools (BioPython, PyMOL, MDAnalysis)
- Consistent with ImageFolder/AudioFolder/VideoFolder patterns

Test: 43 tests passing (15 feature + 28 builder tests)
- Sort imports alphabetically in features.py
- Fix line length in protein_structure.py error messages
- Format function call in test_pdb.py
…e_files API

The embed_storage API gained local_files and remote_files parameters (v4.8.5)
to gate which paths are embedded into the Arrow array. ProteinStructure was
written before this change and raised TypeError (unexpected keyword argument
'local_files') whenever the embed path ran, e.g. on save_to_disk/push_to_hub.

Align its signature and gating logic with the other binary features
(Pdf/Image), embedding local paths only when local_files is set and remote
URLs only when remote_files is set.
@behroozazarkhalili

Copy link
Copy Markdown
Author

Hi @lhoestq, thanks for the review!

Same plan as #7925 (cross-linking so the discussion stays in one place):

On a parsing library: mature options exist — gemmi, biotite, and Biopython all parse PDB/mmCIF — but they're all compiled/heavyweight dependencies (Biopython ~150 MB installed; gemmi and biotite ship C/C++ extensions), which is a lot to add to datasets for one loader. I kept this consistent with the other lightweight bio loaders (FASTA/FASTQ, GenBank), which use small pure-Python parsers with zero dependencies.

On returning a parsed object instead of a string: agreed, and it's a regression I introduced. The original version parsed ATOM/HETATM records into typed columns (atom_name, element, residue, chain, x/y/z, occupancy, B-factor, …) with a record_types filter. When I moved to the one-row-per-structure layout you suggested in #7930, I replaced that with a ProteinStructure feature that currently returns the raw file text — so the parsing (and the record_types filter) was dropped.

Proposal — combine both so each row is one structure and a parsed object, with no new dependency:

structure: {
  atom_name: List[str], element: List[str],
  residue:   List[str], chain_id: List[str], seq_id: List[int],
  x: List[float], y: List[float], z: List[float],
  occupancy: List[float], b_factor: List[float],
}
  • reintroduce the record_types (ATOM/HETATM) filter as a config option on top of the parsed schema
  • optionally keep the raw text available too (ProteinStructure(decode=False) for the bytes, or a raw field) for users who want to parse it themselves

That keeps it queryable and ML-friendly while staying dependency-free. Does that direction work for you?

Decoding a ProteinStructure now returns a struct-of-arrays of atom-level
columns keyed by PDBx/mmCIF dictionary names (type_symbol, label_atom_id,
label_comp_id, label_asym_id, label_seq_id, Cartn_x/y/z, occupancy,
B_iso_or_equiv) instead of the raw file text, while keeping one row per
structure. PDB fixed-width records are mapped onto the same mmCIF-native
names so PDB- and mmCIF-derived datasets share one column vocabulary.

The shared parser lives in features/protein_parsing.py (single canonical
column/dtype table reused by both formats). ProteinStructure parses lazily
on decode (mirroring the Image/Audio/Mesh features); decode=False still
returns the raw {path, bytes} mapping for a custom parser. New feature/config
options include_hetatm and columns are threaded through a _base_feature()
factory on FolderBasedBuilder so folder loaders can forward config-derived
arguments to their base feature. Docs updated to match the new API.
@behroozazarkhalili

Copy link
Copy Markdown
Author

Hi @lhoestq — same update as #7925, applied here so the two PRs stay consistent.

The structure column is now parsed on decode into a struct-of-arrays of atom-level columns (one row = one structure preserved). PDB fixed-width ATOM/HETATM records are mapped onto the same PDBx/mmCIF _atom_site dictionary names the mmCIF loader uses, so a PDB dataset and an mmCIF dataset expose identical columns:

>>> ds = load_dataset("pdb", data_dir="structures")
>>> ds[0]["structure"]
{'type_symbol': ['N', 'C', ...], 'label_atom_id': ['N', 'CA', ...],
 'label_comp_id': ['ALA', 'ALA', ...], 'Cartn_x': [0.0, 1.458, ...], ...}
  • Dependency-free, shared pure-Python parser (features/protein_parsing.py) — one canonical column/dtype table used by both formats and the feature, no duplication.
  • Lazy parse on decode, mirroring Image/Audio/Mesh; storage stays {bytes, path} so ProteinStructure(decode=False) still returns the raw file.
  • Config: include_hetatm (default True) and columns=[...] subset selection.

Existing tests pass; added coverage for the parsed schema, HETATM filtering, and column selection, and loading.mdx is updated. Ready for another look — happy to tweak the schema if you'd like a different set of columns.

@lhoestq

lhoestq commented Jun 19, 2026

Copy link
Copy Markdown
Member

Hi ! same comment as in the mmCIF PR:

I feel like it would be better for people to extend the parsing and to add useful processing methods if the decoding returns an object like a biopython structure (esp. since the community has been contributing to biopython mostly so they can contribute there). IMO It's ok if the dependency is not that lightweight, what matters the most is to get an object that users can easily process/convert/prepare to train models

lmk what you think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants