Structure Generation¶
This module handles everything from validating compositions to generating ready-to-simulate atomic structures with physically realistic densities. It is the starting point for any simulation workflow.
Overview¶
Creating a glass structure involves several steps:
- Define the composition — provide a dict mapping oxide formulas to mol%, e.g.
{"SiO2": 75, "Na2O": 15, "CaO": 10} - Validate the chemistry — check that each oxide is charge-neutral and elements are valid
- Plan the system — determine how many formula units of each oxide are needed to reach the target size
- Estimate the density — use Fluegel's empirical model to predict the glass density
- Calculate the box size — determine the cubic simulation box dimensions
- Place atoms randomly — distribute atoms in the box with a minimum distance constraint
All of these steps are handled automatically by the high-level get_structure_dict() function, but each individual step is also available separately for fine-grained control.
Composition Input¶
Compositions are specified as a dict mapping oxide formulas to mol% values. Values are automatically rescaled to sum to 1.0 internally:
# Molar fractions (sum to ~1.0)
{"CaO": 0.25, "Al2O3": 0.25, "SiO2": 0.5}
# Molar percentages (sum to ~100)
{"SiO2": 75, "Na2O": 15, "CaO": 10}
# Weight percentages (requires mode="weight")
{"SiO2": 79, "B2O3": 13, "Al2O3": 3, "Na2O": 4, "K2O": 1}
get_composition(composition, mode="molar")¶
Normalizes a composition dict into molar fractions. When mode="weight", it first converts weight percentages to molar fractions using the molar masses of each oxide.
from amorphouspy.fabrication import get_composition
# Molar composition
mol_frac = get_composition({"CaO": 0.25, "Al2O3": 0.25, "SiO2": 0.5})
# Returns: {'CaO': 0.25, 'Al2O3': 0.25, 'SiO2': 0.5}
# Weight% to mol% conversion
mol_frac = get_composition({"SiO2": 75, "Na2O": 15, "CaO": 10}, mode="weight")
# Returns molar fractions (different from weight fractions due to molar masses)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
composition |
dict[str, float] |
— | Oxide formula → mol% mapping |
mode |
str |
"molar" |
"molar" for mol fractions/%, "weight" for weight% |
extract_composition(composition)¶
A stricter validator that performs additional checks on top of get_composition. Use this when processing user input that may contain errors.
Validation steps:
- Element validation — verifies each element symbol against the periodic table
- Charge neutrality — uses pymatgen's oxidation state guessing to ensure each oxide is charge-neutral
- Sum validation — checks that fractions sum to ~1.0 or percentages sum to ~100
from amorphouspy.fabrication import extract_composition
comp = extract_composition({"SiO2": 0.75, "Na2O": 0.15, "CaO": 0.10})
# Returns: {'SiO2': 0.75, 'Na2O': 0.15, 'CaO': 0.1}
# These will raise ValueError:
# extract_composition({"XyZ": 0.5, "SiO2": 0.5}) # Invalid element
# extract_composition({"NaCl": 0.5, "SiO2": 0.5}) # Not charge-neutral for oxide
Density Estimation¶
get_glass_density_from_model(composition)¶
Calculates room-temperature glass density using Fluegel's empirical polynomial model. This is a widely used regression model trained on a large database of measured glass densities.
Reference: Fluegel, A. "Global Model for Calculating Room-Temperature Glass Density from the Composition", J. Am. Ceram. Soc. 90 [8] 2622–2635 (2007).
The model uses a polynomial expansion in mole percentages with linear, quadratic, cubic, and cross-interaction terms:
where \(x_i\) is the mole percentage of oxide \(i\) and \(b\) values are fitted coefficients.
Supported oxide components:
| Category | Oxides |
|---|---|
| Glass formers | SiO₂, B₂O₃, Al₂O₃ |
| Alkali oxides | Li₂O, Na₂O, K₂O |
| Alkaline earth oxides | MgO, CaO, SrO, BaO |
| Transition metal oxides | ZnO, PbO, TiO₂, ZrO₂, NiO, MnO, FeO |
| Rare earth oxides | La₂O₃, Nd₂O₃, CeO₂ |
| Others | CdO, ThO₂, UO, SbO, SO₃, F, Cl |
from amorphouspy.fabrication import get_glass_density_from_model
density = get_glass_density_from_model({"SiO2": 75, "Na2O": 15, "CaO": 10})
print(f"Predicted density: {density:.4f} g/cm³")
# Typical soda-lime glass: ~2.49 g/cm³
Note: If your composition contains oxides not in the model, they will be treated as "remainder" components with a generic coefficient. For best accuracy, use compositions within the model's training domain.
Structure Generation¶
High-level: get_structure_dict()¶
The main entry point for generating complete structure dictionaries. This function chains all sub-steps automatically: composition parsing → density estimation → box calculation → atom placement.
If specified manually, using (combinations of) an unrealistically high density, too high min_distance, too many target_atoms or n_molecules, or too few max_attempts_per_atom can lead to placement failures.
Internally, the random placement of atoms is controlled by the random_seed parameter. This ensures reproducibility on the one hand (see also comment further below). On the other hand, if statistics are checked and the same system is simulated several times it is recommended to use different seeds for each run to get a better sampling of the configuration space.
from amorphouspy import get_structure_dict
# Mode 1: Specify target atom count
structure_dict = get_structure_dict(
composition={"SiO2": 75, "Na2O": 15, "CaO": 10},
target_atoms=3000, # Target ~3000 atoms (may differ slightly due to stoichiometry)
mode="molar", # Interpret fractions as mol% (default)
density=None, # Auto-calculate density (Fluegel model)
min_distance=1.6, # Minimum inter-atomic distance (Å)
max_attempts_per_atom=10000, # Max placement attempts per atom
random_seed=42, # Seed controlling the random placement of atoms
)
# Mode 2: Specify molecule (formula unit) count
structure_dict = get_structure_dict(
composition={"CaO": 0.25, "Al2O3": 0.25, "SiO2": 0.5},
n_molecules=100, # 100 total formula units
)
# Mode 3: Weight percentages
structure_dict = get_structure_dict(
composition={"SiO2": 79, "B2O3": 13, "Al2O3": 3, "Na2O": 4, "K2O": 1},
target_atoms=5000,
mode="weight", # Interpret as weight%
)
Important: You must specify exactly one of
target_atomsorn_molecules— not both and not neither.
Returns a dictionary with:
| Key | Type | Description |
|---|---|---|
"atoms" |
list[dict] |
List of {"element": str, "position": [x, y, z]} dicts |
"box" |
float |
Cubic box side length in Å |
"formula_units" |
dict[str, int] |
Integer formula units per oxide (e.g. {"SiO2": 250, "Na2O": 50}) |
"total_atoms" |
int |
Actual total atom count (may differ slightly from target) |
"element_counts" |
dict[str, int] |
Total count per element symbol |
"mol_fraction" |
dict[str, float] |
Normalized molar fractions |
Converting to ASE Atoms¶
The get_ase_structure() function converts the structure dictionary into an ASE Atoms object by generating a LAMMPS data file in memory and reading it back with ASE's LAMMPS reader. This ensures proper atom type assignment and charge columns.
from amorphouspy import get_ase_structure
# Basic conversion
atoms = get_ase_structure(structure_dict)
print(f"Number of atoms: {len(atoms)}")
print(f"Cell: {atoms.get_cell()}")
print(f"Chemical symbols: {set(atoms.get_chemical_symbols())}")
# With supercell replication (2×2×2 → 8× more atoms)
atoms_supercell = get_ase_structure(structure_dict, replicate=(2, 2, 2))
print(f"Supercell atoms: {len(atoms_supercell)}") # 8 × original count
Note: The
replicateparameter creates a supercell by tiling the box in each direction. A(2, 2, 2)replication doubles the box in x, y, and z, resulting in 8× the original number of atoms.
Low-level: create_random_atoms()¶
For direct control over atom placement. This function generates random positions in a periodic cubic box, ensuring that no two atoms are closer than min_distance using the minimum image convention.
The placement algorithm:
1. For each element type and count, attempt to place atoms one by one
2. Generate a random position uniformly in [0, box_length]³
3. Check the minimum image distance to all previously placed atoms
4. If distance ≥ min_distance, accept the position; otherwise retry
5. Raise RuntimeError after max_attempts_per_atom consecutive failures
from amorphouspy.fabrication import create_random_atoms
atoms_list, atom_counts = create_random_atoms(
composition={"SiO2": 75, "Na2O": 15, "CaO": 10},
target_atoms=3000,
box_length=35.0, # Å
min_distance=1.6, # Å (typical for oxide glasses)
seed=42, # Reproducible results
)
# atoms_list → [{"element": "Si", "position": [1.2, 3.4, 5.6]}, ...]
# atom_counts → {"Si": 750, "Na": 100, "Ca": 50, "O": 2100}
Tip: If you get a
RuntimeErrorabout placement failure, either increasebox_lengthor decreasemin_distance. Amin_distanceof 1.6 Å works well for most oxide glasses; the melt-quench simulation will relax any remaining stress.Note: The
seedparameter controls the random number generator for the atom placement. Using the same seed will yield the same structure accross different operating systems if the remaining inputs are the same. This should also hold true for different Python or numpy versions (as long as there are no heavy changes in numpy that break backwards-compatibility). For best reproducibility, do not change the Python/numpy version for one project as this can also affect other processes.
System Planning¶
plan_system(composition, target, mode, target_type)¶
Generates a comprehensive plan that converts a composition and target size into concrete integer formula units. This is the core allocation algorithm used internally by get_structure_dict().
The algorithm uses the largest-remainder method to fairly distribute formula units:
- Calculate the ideal (fractional) number of formula units for each oxide based on composition and target
- Assign the integer floor to each oxide
- Distribute remaining units one-by-one to oxides with the largest fractional remainder
- This minimizes the deviation from ideal composition while ensuring integer counts
from amorphouspy.fabrication import plan_system
# Plan for ~3000 atoms
plan = plan_system(
composition={"CaO": 0.25, "Al2O3": 0.25, "SiO2": 0.5},
target=3000,
target_type="atoms", # or "molecules"
mode="molar",
)
print(plan["formula_units"]) # {'CaO': 150, 'Al2O3': 150, 'SiO2': 300}
print(plan["total_atoms"]) # 3000
print(plan["element_counts"]) # {'Ca': 150, 'O': 1500, 'Al': 300, 'Si': 300}
print(plan["mol_fraction"]) # {'CaO': 0.25, 'Al2O3': 0.25, 'SiO2': 0.5}
Box Size Calculation¶
get_box_from_density(composition, n_molecules, target_atoms, mode, density, stoichiometry)¶
Calculates the cubic box side length needed to achieve a target density. If density is not provided, it is estimated using get_glass_density_from_model().
The calculation:
from amorphouspy.fabrication import get_box_from_density
box_length = get_box_from_density(
composition={"SiO2": 75, "Na2O": 15, "CaO": 10},
target_atoms=3000,
n_molecules=None,
density=2.5, # g/cm³ (or None for auto)
)
print(f"Box length: {box_length:.2f} Å")
Utility Functions¶
Formula parsing¶
| Function | Description | Example |
|---|---|---|
parse_formula(formula) |
Parse oxide into element counts | "Al2O3" → {"Al": 2, "O": 3} |
formula_mass_g_per_mol(formula) |
Molar mass of a compound | "SiO2" → 60.08 g/mol |
extract_stoichiometry(composition) |
Stoichiometry of all composition components | See below |
from amorphouspy.fabrication import parse_formula, extract_stoichiometry
# Single formula
parse_formula("Ca3(PO4)2") # {'Ca': 3, 'P': 2, 'O': 8}
# Full composition
extract_stoichiometry({"SiO2": 0.75, "Na2O": 0.15, "CaO": 0.10})
# {'SiO2': {'Si': 1, 'O': 2}, 'Na2O': {'Na': 2, 'O': 1}, 'CaO': {'Ca': 1, 'O': 1}}
Composition conversion¶
| Function | Description |
|---|---|
weight_percent_to_mol_fraction(comp) |
Convert weight% dictionary → molar fraction dictionary |
check_neutral_oxide(oxide) |
Validate an oxide formula is charge-neutral (raises ValueError if not) |
validate_target_mode(n_molecules, target_atoms) |
Ensure exactly one target mode is specified |
Geometry utilities¶
| Function | Description |
|---|---|
minimum_image_distance(pos1, pos2, box_length) |
Minimum image distance in a cubic periodic box |
get_box_from_density(...) |
Calculate box length from composition and density |