Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make structure adapters infer species from species_at_sites when missing #1103

Merged
merged 1 commit into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions optimade/adapters/structures/aiida.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
This conversion function relies on the [`aiida-core`](https://github.com/aiidateam/aiida-core) package.
"""
from warnings import warn
from typing import List, Optional

from optimade.models import StructureResource as OptimadeStructure
from optimade.models import Species as OptimadeStructureSpecies

from optimade.adapters.warnings import AdapterPackageNotFound, ConversionWarning
from optimade.adapters.structures.utils import pad_cell
from optimade.adapters.structures.utils import pad_cell, species_from_species_at_sites

try:
from aiida.orm.nodes.data.structure import StructureData, Kind, Site
Expand Down Expand Up @@ -46,8 +48,13 @@ def get_aiida_structure_data(optimade_structure: OptimadeStructure) -> Structure
lattice_vectors, adjust_cell = pad_cell(attributes.lattice_vectors)
structure = StructureData(cell=lattice_vectors)

# If species not provided, infer data from species_at_sites
species: Optional[List[OptimadeStructureSpecies]] = attributes.species
if not species:
species = species_from_species_at_sites(attributes.species_at_sites)

# Add Kinds
for kind in attributes.species:
for kind in species:
symbols = []
concentration = []
mass = 0.0
Expand Down
14 changes: 9 additions & 5 deletions optimade/adapters/structures/ase.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from optimade.models import StructureFeatures

from optimade.adapters.exceptions import ConversionError
from optimade.adapters.structures.utils import species_from_species_at_sites

try:
from ase import Atoms, Atom
Expand Down Expand Up @@ -53,13 +54,16 @@ def get_ase_atoms(optimade_structure: OptimadeStructure) -> Atoms:
"ASE cannot handle structures with partial occupancies, sorry."
)

species: Dict[str, OptimadeStructureSpecies] = {
species.name: species for species in attributes.species
}
species = attributes.species
# If species is missing, infer data from species_at_sites
if not species:
species = species_from_species_at_sites(attributes.species_at_sites)

optimade_species: Dict[str, OptimadeStructureSpecies] = {_.name: _ for _ in species}

# Since we've made sure there are no species with more than 1 chemical symbol,
# asking for index 0 will always work.
if "X" in [specie.chemical_symbols[0] for specie in species.values()]:
if "X" in [specie.chemical_symbols[0] for specie in optimade_species.values()]:
raise ConversionError(
"ASE cannot handle structures with unknown ('X') chemical symbols, sorry."
)
Expand All @@ -69,7 +73,7 @@ def get_ase_atoms(optimade_structure: OptimadeStructure) -> Atoms:
species_name = attributes.species_at_sites[site_number]
site = attributes.cartesian_site_positions[site_number]

current_species = species[species_name]
current_species = optimade_species[species_name]

# Argument above about chemical symbols also holds here
mass = None
Expand Down
14 changes: 11 additions & 3 deletions optimade/adapters/structures/pymatgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

For more information on the pymatgen code see [their documentation](https://pymatgen.org).
"""
from typing import Union, Dict, List
from typing import Union, Dict, List, Optional

from optimade.models import Species as OptimadeStructureSpecies
from optimade.models import StructureResource as OptimadeStructure
from optimade.adapters.structures.utils import species_from_species_at_sites

try:
from pymatgen.core import Structure, Molecule
Expand Down Expand Up @@ -51,7 +52,9 @@ def get_pymatgen(optimade_structure: OptimadeStructure) -> Union[Structure, Mole
warn(PYMATGEN_NOT_FOUND, AdapterPackageNotFound)
return None

if all(optimade_structure.attributes.dimension_types):
if optimade_structure.attributes.nperiodic_dimensions == 3 or all(
optimade_structure.attributes.dimension_types
):
return _get_structure(optimade_structure)

return _get_molecule(optimade_structure)
Expand Down Expand Up @@ -90,12 +93,17 @@ def _get_molecule(optimade_structure: OptimadeStructure) -> Molecule:


def _pymatgen_species(
nsites: int, species: List[OptimadeStructureSpecies], species_at_sites: List[str]
nsites: int,
species: Optional[List[OptimadeStructureSpecies]],
species_at_sites: List[str],
) -> List[Dict[str, float]]:
"""
Create list of {"symbol": "concentration"} per site for values to pymatgen species parameters.
Remove vacancies, if they are present.
"""
if not species:
# If species is missing, infer data from species_at_sites
species = species_from_species_at_sites(species_at_sites)

optimade_species = {_.name: _ for _ in species}

Expand Down
26 changes: 26 additions & 0 deletions optimade/adapters/structures/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import List, Tuple, Iterable

from optimade.models.structures import Vector3D
from optimade.models.structures import Species as OptimadeStructureSpecies

try:
import numpy as np
Expand Down Expand Up @@ -315,3 +316,28 @@ def pad_cell(
outer=tuple,
inner=tuple,
)


def species_from_species_at_sites(
species_at_sites: List[str],
) -> List[OptimadeStructureSpecies]:
"""When a list of species dictionaries is not provided, this function
can be used to infer the species from the provided species_at_sites.

In this use case, species_at_sites is assumed to provide a list of
element symbols, and refers to situations with no mixed occupancy, i.e.,
the constructed species list will contain all unique species with
concentration equal to 1 and the species_at_site tag will be used as
the chemical symbol.

Parameters:
species_at_sites: The list found under the species_at_sites field.

Returns:
An OPTIMADE species list.

"""
return [
OptimadeStructureSpecies(name=_, concentration=[1.0], chemical_symbols=[_])
for _ in set(species_at_sites)
]
6 changes: 6 additions & 0 deletions tests/adapters/structures/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ def null_lattice_vector_structure(raw_structure) -> Structure:
raw_structure["attributes"]["dimension_types"][0] = 0
raw_structure["attributes"]["nperiodic_dimensions"] = 2
return Structure(raw_structure)


@pytest.fixture
def null_species_structure(raw_structure) -> Structure:
raw_structure["attributes"]["species"] = None
return Structure(raw_structure)
5 changes: 5 additions & 0 deletions tests/adapters/structures/test_aiida.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ def test_special_species(SPECIAL_SPECIES_STRUCTURES):
)
else:
assert aiida_structure.kinds[0].mass == 1.0


def test_null_species(null_species_structure):
"""Make sure null species are handled"""
assert isinstance(get_aiida_structure_data(null_species_structure), StructureData)
5 changes: 5 additions & 0 deletions tests/adapters/structures/test_ase.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ def test_special_species(SPECIAL_SPECIES_STRUCTURES):
r"(ASE cannot handle structures with unknown \('X'\) chemical symbols)",
):
get_ase_atoms(structure)


def test_null_species(null_species_structure):
"""Make sure null species are handled"""
assert isinstance(get_ase_atoms(null_species_structure), Atoms)
5 changes: 5 additions & 0 deletions tests/adapters/structures/test_pymatgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ def test_special_species(SPECIAL_SPECIES_STRUCTURES):
for special_structure in SPECIAL_SPECIES_STRUCTURES:
structure = Structure(special_structure)
assert isinstance(get_pymatgen(structure), PymatgenStructure)


def test_null_species(null_species_structure):
"""Make sure null species are handled"""
assert isinstance(get_pymatgen(null_species_structure), PymatgenStructure)
45 changes: 45 additions & 0 deletions tests/adapters/structures/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
fractional_coordinates,
pad_cell,
scaled_cell,
species_from_species_at_sites,
)


Expand Down Expand Up @@ -115,3 +116,47 @@ def test_scaled_cell_consistency(structure):
volume_from_scale = 1 / numpy.linalg.det(scale)

assert volume_from_scale == pytest.approx(volume_from_cellpar)


def test_species_from_species_at_sites(structure):
"""Test that species can be inferred from species_at_sites"""
species_at_sites = ["Si"]
assert [d.dict() for d in species_from_species_at_sites(species_at_sites)] == [
{
"name": "Si",
"concentration": [1.0],
"chemical_symbols": ["Si"],
"attached": None,
"mass": None,
"original_name": None,
"nattached": None,
},
]

species_at_sites = ["Si", "Si", "O", "O", "O", "O"]
assert sorted(
[d.dict() for d in species_from_species_at_sites(species_at_sites)],
key=lambda _: _["name"],
) == sorted(
[
{
"name": "O",
"concentration": [1.0],
"chemical_symbols": ["O"],
"attached": None,
"mass": None,
"original_name": None,
"nattached": None,
},
{
"name": "Si",
"concentration": [1.0],
"chemical_symbols": ["Si"],
"attached": None,
"mass": None,
"original_name": None,
"nattached": None,
},
],
key=lambda _: _["name"],
)