Training, Generating & Evaluating from Scratch¶
Run it in a notebook
T1_finetune_density_example.ipynb runs this pipeline end to end on a density dataset: prepare the data, finetune, push to the Hub, register, generate. T4_SLME.ipynb does the same for a photovoltaic efficiency target and screens the output.
On this page is the complete pipeline for training models. All parameters and options are defined in _args.py.
Data Processing Pipeline¶
Step 1: Data Preparation (Required)¶
Input data should be a pandas DataFrame saved as Parquet file. To train a model you should save a dataframe to a parquet file which contains:
Required columns:
Database: Source database nameReduced Formula: Standard reduced chemical formulaCIF: Crystallographic structure in CIF format
For structure recovery benchmarks
Material ID: Database identifier required for structure recovery benchmarks
Optional columns:
<Property Columns>: Target properties (e.g., "Bandgap (eV)", "Density (g/cm^3)")condition_vector: Pre-computed condition vectors (for XRD studies)
The scripts in
_utils/_datasets/download and prepare Alex-MP-20, MP-20, LeMat-BulkUnique and stratified CHILI-100K with their splits. CHILI-100K needs about 70 GB of free space under--raw_dir.The scripts write CIFs in the v2.1.0 format, so their output does not match the original Hub datasets (
c-bone/mp_20,c-bone/alex_mp_20,c-bone/lematerial_clean) exactly. Most differences are symmetry settings and rounding from newer pymatgen and spglib versions, the structures are the same. The LeMat script also keeps every LeMat-BulkUnique structure, wherelematerial_cleandeduplicated using our deduplication script. The original datasets stay the defaults and work essentially the same but we'll start rolling out the datasets like this from now on.
Step 2: Deduplication and Filtering (Optional)¶
Script: _utils/_preprocessing/deduplicate.py - Removes duplicate structures and filters invalid entries based on chemical formula and space group, keeping the structure with lowest volume per formula unit.
Example Usage and Args
python _utils/_preprocessing/deduplicate.py \
--input_parquet /path/to/raw_data.parquet \
--output_parquet /path/to/deduplicated_data.parquet \
--property_columns "['Bandgap (eV)', 'Density (g/cm^3)']" \
--filter_na_columns "['Bandgap (eV)']" \
--filter_zero_columns "['Density (g/cm^3)']" \
--filter_negative_columns "['Bandgap (eV)']"
Key arguments:
--filter_na_columns: Remove entries with N/A or NaN values--filter_zero_columns: Remove entries with zero values--filter_negative_columns: Remove entries with negative values
Step 3: CIF Cleaning and Normalization (Required)¶
Script: _utils/_preprocessing/cleaning.py - Standardizes CIF format and normalizes properties for stable training. Adds atomic property blocks, rounds numerical values, and applies variable brackets.
Example Usage and Args
python _utils/_preprocessing/cleaning.py \
--input_parquet /path/to/deduplicated_data.parquet \
--output_parquet /path/to/cleaned_data.parquet \
--num_workers 8 \
--property_columns "['Bandgap (eV)', 'Density (g/cm^3)']" \
--property1_normaliser "power_log" \
--property2_normaliser "linear"
Tip: Keep a note somewhere of the lowest and highest property values for each property, so that when you have a property target later you can normalize it to the format the model expects.
Key arguments:
--property1_normaliser/--property2_normaliser: Normalization methods (linear,power_log,signed_log,log10,None)--make_disordered_ordered: Convert disordered structures to ordered ones--num_workers: Number of parallel workers for processing
Normalization methods:
linear: Simple min-max scaling to [0,1] rangepower_log: Power transformation ($\beta$=0.8) followed by logarithmic scaling for skewed distributionssigned_log: Signed logarithmic transformation for handling negative valueslog10: Base-10 logarithmic scaling for properties spanning multiple orders of magnitudeNone: No normalization applied
Step 4: Dataset Upload to HuggingFace (Required)¶
Script: _utils/_preprocessing/save_dataset_to_hf.py - Converts to HuggingFace format with train/validation/test splits and uploads to HF Hub.
Important: You need to make sure that the data trained on has been passed through CIF cleaning, a quick way to make sure is check whether the CIFs in your dataframe contain brackets. If they do then text should be ready for training.
Example Usage and Args
python _utils/_preprocessing/save_dataset_to_hf.py \
--input_parquet /path/to/processed_data.parquet \
--output_parquet "your-dataset-name" \
--test_size 0.1 \
--valid_size 0.1 \
--HF_username "your-username" \
--save_hub \
--save_local
Key arguments:
--duplicates: Prevents data leakage by splitting on Material ID (optional)--test_size/--valid_size: Split ratios (set both to 0.0 for training-only)--save_hub/--save_local: Upload to HF Hub and/or save locally (specify at least one)
Training¶
All training should be done via configuration files (.jsonc format). These files specify model architecture, hyperparameters, data paths, and training settings. See example configs in _config_files/training/ and review _args.py. for all available parameters.
The
Muonoptimiser is available for training. See this blog post for details. In our internal checks, Muon made training faster and more stable without reducing model performance.
Training behavior to be aware of:
train_batch_sizeis the global batch size. Each GPU processestrain_batch_size / (GPUs × gradient_accumulation_steps)sequences per forward pass, so it needs to divide exactly. This way it's device and compute power agnostic.- Multi-GPU training requires
torchrun --nproc_per_node=N. For single-GPU training withpython, useCUDA_VISIBLE_DEVICESto select one GPU. - We can now restart a run if it crashed or stopped for example. If you specify the same
output_dirin the config, training picks up where it left off including optimizer state. eval_losschanged inv2.1.0, it now averages the per-token loss within each validation CIF, then averages across CIFs. (so CIFs not stacked like in training and we dont add the fixed format loss term)HF_DATASETS_CACHEsets the dataset cache folder, so several runs can share one cache. If not set, you get a.cachefolder next tooutput_dir.- You can set the
NUM_PROC_TOKenvironment variable to set the number of tokenisation processes. Default is the CPU count capped at 16.
Base model training CLI example
Base Model Pretraining
Train the unconditional base models from scratch:
Multi-GPU Training:
Conditional finetuning CLI example
Conditional Fine-tuning
Fine-tune pretrained base models for property-guided generation:
Single GPU:
Multi-GPU:
torchrun --nproc_per_node=2 _train.py --config _config_files/training/conditional/ft-slme/slme_ft-prefix-opt.jsonc
Loads pretrained weights as starting point (or trains from scratch), adds conditional architecture layers, and uses split optimizer with different learning rates for conditioning vs base layers.
Advanced Generation Pipeline (Prefer using Load and Generate script instead)¶
Step 1: Create Prompts¶
Script: _utils/_generating/make_prompts.py - Generate input prompts for conditional generation with different levels of structural information.
Examples of Prompt Construction and Args
Manual Prompts:
python _utils/_generating/make_prompts.py \
--manual \
--compositions "Na1Cl1,K2S1" \
--condition_lists "0.2,0.0" "0.5,0.0" \
--level "level_3" \
--output_parquet "test_prompts.parquet"
Automatic Prompts from Dataset:
python _utils/_generating/make_prompts.py \
--automatic \
--HF_dataset "c-bone/mp_20_pxrd" \
--split "test" \
--level "level_2" \
--condition_columns "Condition Vector" \
--output_parquet "dataset_prompts.parquet"
Prompt levels --level:
level_1: Minimal (unconditional generation)level_2: Composition only (default)level_3: Composition + atomic propertieslevel_4: Up to space group information
Composition-Condition Pairing modes --mode:
Each quoted string is one complete condition vector of comma-separated property values. --mode sets how these vectors pair with --compositions:
cartesian(default): every condition vector with every composition, so the manual example above makes 4 promptspaired: one condition vector per composition, in order; the counts must match (2 prompts above)broadcast: a single condition vector applied to every composition
_load_and_generate.py has no --mode: it always pairs one condition vector per formula, or broadcasts a single one.
Step 2: Generate CIFs¶
Script: _utils/_generating/generate_cifs.py - Generate crystal structures from prompts using trained models.
Examples of CIF generation and Args
python _utils/_generating/generate_cifs.py \
--config _config_files/generation/conditional/slme/slme-PKV-opt_eval.jsonc
You can generate with arguments from the CLI, but it's easier to use the config file. You can find a lot of examples in
_config_files/generation
Key generation settings:
- Temperature: Controls randomness (default ~1.0, higher is more exploratory but higher chance of gibberish)
- Top-p/Top-k: Sampling parameters (typical: 0.95, 50)
- scoring_mode: if set to
Noneandtarget_valid_cifs = 0, then we generatemax_return_attempts * num_return_sequencesCIFs per Prompt/Condition pair without validation. If set toNoneandtarget_valid_cifs > 0, then we validate generated CIFs and stop once that many valid CIFs are found, without ranking. If set toLOGP, we validate and rank using a perplexity based scoring method. - num_return_sequences: Batch size for generation (adjust for GPU mem.)
- max_return_attempts: In raw mode, total generation for each Prompt/Condition pair =
max_return_attempts * num_return_sequences. In validation-targeted modes, generation stops whentarget_valid_cifsvalid CIFs are found ormax_return_attemptsis reached.
Step 3: Post-process¶
Script: _utils/_generating/postprocess.py - Clean and validate generated CIF structures.
Examples of postprocessing and Args
python _utils/_generating/postprocess.py \
--input_parquet "generated_cifs.parquet" \
--output_parquet "processed_cifs.parquet" \
--num_workers 4
Converts LLM outputs to standard Pymatgen style CIF format.
Evaluation¶
VUN Metrics (Validity, Uniqueness, Novelty)¶
Script: _utils/_scoring/vun_metrics.py - Validity, uniqueness and novelty metrics from structural analysis.
Required: Structures must be post-processed with Reduced Formulas column included
Metrics computed:
- Validity: Structures with correct spacegroup, reasonable bond lengths, and consistent atom multiplicities
- Uniqueness: Distinct structures within the generated set (using BAWL hashing)
- Novelty: Structures not present in the reference dataset
- Compositional Novelty: Reduced Formula not present in reference dataset
Example Usage
python _utils/_scoring/vun_metrics.py \
--input_parquet generated_structures_processed.parquet \
--huggingface_dataset "c-bone/mp_20" \
--output_parquet vun_results.parquet \
--num_workers 8
We can optionally set the --check_comp_novelty flag, which adds an is_comp_novel boolean column to the metrics dataframe.
Energy Above Hull (Stability)¶
Script: _utils/_scoring/mace_ehull.py - Calculate thermodynamic stability using MACE energy predictions. See the MACE paper for details on the surrogate model.
To calculate E_hull First, total energies are computed using the MACE-MP default calculator, predicted energies are then processed using the MaterialsProject2020Compatibility scheme for consistency between GGA and GGA+U calculations. The surrogate energy predictions are compared to formation energies of known materials from the MP dataset and used to construct a convex hull. The energy above the convex hull (E_hull) quantifies thermodynamic stability by comparing a material's formation energy to competing phases.
Example Usage and Args
python _utils/_scoring/mace_ehull.py \
--post_parquet postprocessed_structures.parquet \
--output_parquet stability_results.parquet \
--num_workers 4
Lower E_hull values indicate higher thermodynamic stability. Structures with E_hull < 0.1 eV/atom are typically considered experimentally synthesizable. (We can extend to 0.157 eV/atom if we want to account for MAE in energy predictions of this MACE model)
Additional Metrics¶
XRD or density property metrics, VUN, and stability metrics are available in _utils/_scoring/.