mCDR's picture
download
raw
10.8 kB
#!/usr/bin/env python
from __future__ import annotations
import argparse
import logging
import multiprocessing as mp
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from ase import Atoms
from ase.db import connect
from ase.io import write
from datasets import Dataset, load_dataset
from tqdm import tqdm
NON_METAL_ELEMENTS = {
"H",
"O",
"N",
"C",
"S",
"P",
"F",
"Cl",
"Br",
"I",
"Se",
"B",
"Si",
"Ge",
"As",
"Sb",
"Te",
}
def _extraction_worker(args_tuple: Tuple) -> List[Atoms]:
"""
A parallel worker that connects to a single DB file, extracts a list of
rows by index, applies secondary filters, and returns the valid Atoms objects.
"""
db_path, indices, contains_elements, element_mode, material_type = args_tuple
extracted_atoms = []
query_elements = set(contains_elements) if contains_elements else set()
with connect(db_path) as db:
for index in indices:
row = db.get(id=index + 1)
if not row:
continue
atoms = row.toatoms()
atoms.info = row.data
symbols = atoms.get_chemical_symbols()
bulk_elements = {
symbols[i] for i, tag in enumerate(atoms.get_tags()) if tag != 2
}
if contains_elements:
if element_mode == "any" and query_elements.isdisjoint(bulk_elements):
continue
if element_mode == "all" and not query_elements.issubset(bulk_elements):
continue
if material_type:
is_nonmetal = any(el in NON_METAL_ELEMENTS for el in bulk_elements)
if material_type == "metal" and is_nonmetal:
continue
if material_type == "nonmetal" and not is_nonmetal:
continue
extracted_atoms.append(atoms)
return extracted_atoms
class AQcatQuery:
"""An interface to lazily query the AQcat25 dataset."""
def __init__(
self,
repo_id: str = "SandboxAQ/aqcat25",
data_root: Path | str = "./aqcat_data",
):
self.repo_id = repo_id
self.data_root = Path(data_root)
self.filtered_ids: Optional[List[str]] = None
self.filtered_split: Optional[str] = None
self._loaded_splits: Dict[str, Dataset] = {}
logging.info("Initializing query engine...")
def filter(
self,
split: str,
adsorbates: Optional[List[str]] = None,
min_energy: Optional[float] = None,
max_energy: Optional[float] = None,
magnetism: Optional[str] = None,
) -> AQcatQuery:
"""Filters the dataset based on metadata from the Parquet files."""
if split not in self._loaded_splits:
logging.info(
f"Connecting to Hub and loading metadata for split: '{split}'..."
)
try:
data_file = f"parquet/{split}.parquet"
dataset_split = load_dataset(
self.repo_id,
data_files=data_file,
split="train",
trust_remote_code=True,
)
self._loaded_splits[split] = dataset_split
logging.info(
f"Successfully loaded metadata for '{split}' ({len(dataset_split):,} total frames)."
)
except Exception as e:
logging.error(
f"Could not load metadata for split '{split}'.\nError: {e}"
)
return self
dataset_split = self._loaded_splits[split]
logging.info(f"\nFiltering '{split}' split...")
filtered_data = dataset_split
if adsorbates:
filtered_data = filtered_data.filter(
lambda ex: ex["adsorbate"] in adsorbates
)
logging.info(f" - {len(filtered_data):,} frames passed adsorbate filter")
if min_energy is not None:
filtered_data = filtered_data.filter(
lambda ex: ex["adsorption_energy"] >= min_energy
)
logging.info(f" - {len(filtered_data):,} frames passed min energy filter")
if max_energy is not None:
filtered_data = filtered_data.filter(
lambda ex: ex["adsorption_energy"] <= max_energy
)
logging.info(f" - {len(filtered_data):,} frames passed max energy filter")
if magnetism:
if magnetism == "magnetic":
filtered_data = filtered_data.filter(
lambda ex: abs(ex["total_magnetization"]) > 1.0
)
elif magnetism == "non-magnetic":
filtered_data = filtered_data.filter(
lambda ex: abs(ex["total_magnetization"]) <= 1.0
)
logging.info(f" - {len(filtered_data):,} frames passed magnetism filter")
self.filtered_ids = filtered_data["frame_id"]
self.filtered_split = split
logging.info(
f"Found {len(self.filtered_ids):,} frames matching metadata criteria."
)
return self
def extract_and_save(
self,
output_file: Path,
contains_elements: Optional[List[str]] = None,
element_filter_mode: str = "any",
material_type: Optional[str] = None,
num_workers: int = 1,
chunk_size: int = 5000,
):
"""
Extracts the filtered frames in parallel, applies secondary filters,
and saves the results to chunked .extxyz files.
"""
if not self.filtered_ids:
logging.warning(
"No frames to extract after metadata filtering. Nothing to do."
)
return
logging.info("\nStarting extraction")
db_to_indices = defaultdict(list)
for frame_id in self.filtered_ids:
db_name, index_str = frame_id.split("::")
db_to_indices[db_name].append(int(index_str))
split_dir = self.data_root / self.filtered_split
if not split_dir.is_dir():
raise FileNotFoundError(
f"Data directory '{split_dir}' not found. Please run 'python download_split.py --split {self.filtered_split}'"
)
tasks = [
(
split_dir / db_name,
indices,
contains_elements,
element_filter_mode,
material_type,
)
for db_name, indices in db_to_indices.items()
]
all_atoms = []
with mp.Pool(num_workers) as pool:
results = list(
tqdm(
pool.imap(_extraction_worker, tasks),
total=len(tasks),
desc="Extracting frames",
)
)
for atoms_chunk in results:
all_atoms.extend(atoms_chunk)
logging.info(f"Successfully extracted {len(all_atoms):,} final structures.")
if all_atoms:
logging.info(
f"Writing results to chunked files (max {chunk_size} frames per file)..."
)
if len(all_atoms) <= chunk_size:
write(output_file, all_atoms)
logging.info(f"Saved {len(all_atoms)} structures to {output_file}")
else:
num_chunks = (len(all_atoms) + chunk_size - 1) // chunk_size
for i in range(num_chunks):
start_idx = i * chunk_size
end_idx = start_idx + chunk_size
chunk = all_atoms[start_idx:end_idx]
chunk_path = output_file.with_stem(f"{output_file.stem}_{i:03d}")
write(chunk_path, chunk)
logging.info(
f" - Wrote chunk {i+1}/{num_chunks} ({len(chunk)} frames) to {chunk_path}"
)
logging.info("Save complete.")
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
parser = argparse.ArgumentParser(
description="Query the AQcat25 dataset and extract atomic structures."
)
parser.add_argument("--split", required=True, help="The data split to query.")
parser.add_argument(
"--adsorbates", nargs="+", help="One or more adsorbate strings to match."
)
parser.add_argument(
"--min-energy", type=float, help="Minimum adsorption energy in eV."
)
parser.add_argument(
"--max-energy", type=float, help="Maximum adsorption energy in eV."
)
parser.add_argument(
"--magnetism",
choices=["magnetic", "non-magnetic"],
help="Filter by magnetic moment.",
)
parser.add_argument(
"--contains-elements",
nargs="+",
help="Filter for slabs containing specified elements.",
)
parser.add_argument(
"--element-filter-mode",
choices=["any", "all"],
default="any",
help="Logic for element filter.",
)
parser.add_argument(
"--material-type",
choices=["metal", "nonmetal"],
help="Filter by the bulk slab's material type.",
)
parser.add_argument(
"--data-root",
type=Path,
default=Path("./aqcat_data"),
help="Local directory of downloaded data.",
)
parser.add_argument(
"--output-file",
type=Path,
default="filtered_results.extxyz",
help="Path/prefix for the output .extxyz file(s).",
)
parser.add_argument(
"--num-workers",
type=int,
default=max(1, mp.cpu_count() - 2),
help="Number of parallel workers for data extraction.",
)
parser.add_argument(
"--chunk-size",
type=int,
default=5000,
help="Maximum number of frames per output file.",
)
parser.add_argument(
"--repo-id",
default="SandboxAQ/aqcat25",
help="The Hugging Face repository ID.",
)
args = parser.parse_args()
query = AQcatQuery(data_root=args.data_root, repo_id=args.repo_id)
query.filter(
split=args.split,
adsorbates=args.adsorbates,
min_energy=args.min_energy,
max_energy=args.max_energy,
magnetism=args.magnetism,
)
query.extract_and_save(
output_file=args.output_file,
contains_elements=args.contains_elements,
element_filter_mode=args.element_filter_mode,
material_type=args.material_type,
num_workers=args.num_workers,
chunk_size=args.chunk_size,
)

Xet Storage Details

Size:
10.8 kB
·
Xet hash:
a65c4dfe7be4e6110270656a4b6fa1de0bfdc734402bc846a9922771fe08d6da

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.