Commit be915c0a authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Continued to work on optimade and metainfo.

parent 5e8b67c9
...@@ -17,6 +17,7 @@ import datetime ...@@ -17,6 +17,7 @@ import datetime
from elasticsearch_dsl import Keyword from elasticsearch_dsl import Keyword
from nomad import utils, config from nomad import utils, config
from nomad.metainfo import MObject
class UploadWithMetadata(): class UploadWithMetadata():
...@@ -106,10 +107,17 @@ class CalcWithMetadata(): ...@@ -106,10 +107,17 @@ class CalcWithMetadata():
self.update(**kwargs) self.update(**kwargs)
def to_dict(self): def to_dict(self):
return { def items():
key: value for key, value in self.__dict__.items() for key, value in self.__dict__.items():
if value is not None and key not in ['backend'] if value is None or key in ['backend']:
} continue
if isinstance(value, MObject):
value = value.m_to_dict()
yield key, value
return {key: value for key, value in items()}
def update(self, **kwargs): def update(self, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
......
...@@ -92,7 +92,7 @@ class DFTCalcWithMetadata(CalcWithMetadata): ...@@ -92,7 +92,7 @@ class DFTCalcWithMetadata(CalcWithMetadata):
self.geometries = [] self.geometries = []
self.group_hash: str = None self.group_hash: str = None
self.optimade: optimade.StructureEntry = None self.optimade: optimade.OptimadeStructureEntry = None
super().__init__(**kwargs) super().__init__(**kwargs)
...@@ -176,6 +176,8 @@ class DFTCalcWithMetadata(CalcWithMetadata): ...@@ -176,6 +176,8 @@ class DFTCalcWithMetadata(CalcWithMetadata):
self.n_total_energies = n_total_energies self.n_total_energies = n_total_energies
self.n_geometries = n_geometries self.n_geometries = n_geometries
self.optimade = backend.get_mi2_section(optimade.OptimadeStructureEntry.m_section)
def only_atoms(atoms): def only_atoms(atoms):
numbers = [ase.data.atomic_numbers[atom] for atom in atoms] numbers = [ase.data.atomic_numbers[atom] for atom in atoms]
...@@ -230,8 +232,8 @@ Domain( ...@@ -230,8 +232,8 @@ Domain(
elastic_mapping=Integer()), elastic_mapping=Integer()),
optimade=DomainQuantity( optimade=DomainQuantity(
'Data for the optimade API', 'Data for the optimade API',
elastic_mapping=Object(optimade.ESStructureEntry), elastic_mapping=Object(optimade.ESOptimadeEntry),
elastic_value=lambda entry: optimade.elastic_obj(entry, optimade.ESStructureEntry) elastic_value=lambda entry: optimade.elastic_obj(entry, optimade.ESOptimadeEntry)
)), )),
metrics=dict( metrics=dict(
total_energies=('n_total_energies', 'sum'), total_energies=('n_total_energies', 'sum'),
......
...@@ -145,6 +145,7 @@ import sys ...@@ -145,6 +145,7 @@ import sys
import inspect import inspect
import re import re
import numpy as np
from pint.unit import _Unit from pint.unit import _Unit
from pint import UnitRegistry from pint import UnitRegistry
import inflection import inflection
...@@ -364,17 +365,22 @@ class MObject(metaclass=MObjectMeta): ...@@ -364,17 +365,22 @@ class MObject(metaclass=MObjectMeta):
if shape is None or len(shape) == 0 or check_item: if shape is None or len(shape) == 0 or check_item:
check_value(value) check_value(value)
elif len(shape) == 1: else:
if not isinstance(value, list): if type(definition.type) == np.dtype:
raise TypeError('Wrong shape') if len(shape) != len(value.shape):
raise TypeError('Wrong shape')
else:
if len(shape) == 1:
if not isinstance(value, list):
raise TypeError('Wrong shape')
for item in value: for item in value:
check_value(item) check_value(item)
else: else:
# TODO # TODO
# raise Exception('Higher shapes not implemented') # raise Exception('Higher shapes not implemented')
pass pass
# TODO check dimension # TODO check dimension
...@@ -525,7 +531,29 @@ class MObject(metaclass=MObjectMeta): ...@@ -525,7 +531,29 @@ class MObject(metaclass=MObjectMeta):
def m_to_dict(self) -> Dict[str, Any]: def m_to_dict(self) -> Dict[str, Any]:
"""Returns the data of this section as a json serializeable dictionary. """ """Returns the data of this section as a json serializeable dictionary. """
pass
def items() -> Iterable[Tuple[str, Any]]:
yield 'm_section', self.m_section.name
if self.m_parent_index != -1:
yield 'm_parnet_index', self.m_parent_index
for name, sub_section in self.m_section.sub_sections.items():
if name not in self.m_data:
continue
if sub_section.repeats:
yield name, [item.m_to_dict() for item in self.m_data[name]]
else:
yield name, self.m_data[name].m_to_dict()
for name in self.m_section.quantities:
if name in self.m_data:
value = getattr(self, name)
if hasattr(value, 'tolist'):
value = value.tolist()
yield name, value
return {key: value for key, value in items()}
def m_to_json(self): def m_to_json(self):
"""Returns the data of this section as a json string. """ """Returns the data of this section as a json string. """
...@@ -620,6 +648,15 @@ class Quantity(Definition): ...@@ -620,6 +648,15 @@ class Quantity(Definition):
raise KeyError('Cannot overwrite quantity definition. Only values can be set.') raise KeyError('Cannot overwrite quantity definition. Only values can be set.')
# object (instance) case # object (instance) case
if type(self.type) == np.dtype:
if type(value) != np.ndarray:
value = np.array(value, dtype=self.type)
elif self.type != value.dtype:
value = np.array(value, dtype=self.type)
elif type(value) == np.ndarray:
value = value.tolist()
MObject.m_type_check(self, value) MObject.m_type_check(self, value)
obj.m_data[self.__name] = value obj.m_data[self.__name] = value
...@@ -792,7 +829,7 @@ Section.parent = Quantity( ...@@ -792,7 +829,7 @@ Section.parent = Quantity(
Quantity.m_section.section_cls = Quantity Quantity.m_section.section_cls = Quantity
Quantity.type = Quantity( Quantity.type = Quantity(
type=Union[type, Enum, Section], name='type', _bs=True, description=''' type=Union[type, Enum, Section, np.dtype], name='type', _bs=True, description='''
The type of the quantity. The type of the quantity.
Can be one of the following: Can be one of the following:
...@@ -802,6 +839,12 @@ Quantity.type = Quantity( ...@@ -802,6 +839,12 @@ Quantity.type = Quantity(
- an instance of :class:`Enum`, e.g. ``Enum(['one', 'two', 'three']) - an instance of :class:`Enum`, e.g. ``Enum(['one', 'two', 'three'])
- a instance of Section, i.e. a section definition. This will define a reference - a instance of Section, i.e. a section definition. This will define a reference
- a custom meta-info DataType - a custom meta-info DataType
- a numpy dtype,
If set to a dtype, this quantity will use a numpy array to store values. It will use
the given dtype. If not set, this quantity will use (nested) Python lists to store values.
If values are set to the property, they will be converted to the respective
representation.
In the NOMAD CoE meta-info this was basically the ``dTypeStr``. In the NOMAD CoE meta-info this was basically the ``dTypeStr``.
''') ''')
......
from ase.data import chemical_symbols from ase.data import chemical_symbols
from elasticsearch_dsl import Keyword, Integer, Float, Text, InnerDoc, Nested from elasticsearch_dsl import Keyword, Integer, Float, Text, InnerDoc, Nested
import numpy as np
from nomad.metainfo import MObject, Section, Quantity, Enum, units from nomad.metainfo import MObject, Section, Quantity, Enum, units
...@@ -15,7 +16,7 @@ class ElementRatio(InnerDoc): ...@@ -15,7 +16,7 @@ class ElementRatio(InnerDoc):
ratio = Float() ratio = Float()
@staticmethod @staticmethod
def from_structure_entry(entry: 'StructureEntry'): def from_structure_entry(entry: 'OptimadeStructureEntry'):
return [ return [
ElementRatio(element=entry.elements[i], ratio=entry.elements_ratios[i]) ElementRatio(element=entry.elements[i], ratio=entry.elements_ratios[i])
for i in range(0, entry.nelements)] for i in range(0, entry.nelements)]
...@@ -26,7 +27,7 @@ class Optimade(): ...@@ -26,7 +27,7 @@ class Optimade():
pass pass
class StructureEntry(MObject): class OptimadeStructureEntry(MObject):
m_section = Section( m_section = Section(
links=optimade_links('h.6.2'), links=optimade_links('h.6.2'),
a_flask=dict(skip_none=True), a_flask=dict(skip_none=True),
...@@ -115,7 +116,7 @@ class StructureEntry(MObject): ...@@ -115,7 +116,7 @@ class StructureEntry(MObject):
''') ''')
lattice_vectors = Quantity( lattice_vectors = Quantity(
type=float, shape=[3, 3], unit=units.angstrom, type=np.dtype('f8'), shape=[3, 3], unit=units.angstrom,
links=optimade_links('h.6.2.9'), links=optimade_links('h.6.2.9'),
a_optimade=Optimade(query=False, entry=True), a_optimade=Optimade(query=False, entry=True),
description=''' description='''
...@@ -123,7 +124,7 @@ class StructureEntry(MObject): ...@@ -123,7 +124,7 @@ class StructureEntry(MObject):
''') ''')
cartesian_site_positions = Quantity( cartesian_site_positions = Quantity(
type=float, shape=['nsites', 3], unit=units.angstrom, type=np.dtype('f8'), shape=['nsites', 3], unit=units.angstrom,
links=optimade_links('h.6.2.10'), links=optimade_links('h.6.2.10'),
a_optimade=Optimade(query=False, entry=True), description=''' a_optimade=Optimade(query=False, entry=True), description='''
Cartesian positions of each site. A site is an atom, a site potentially occupied by Cartesian positions of each site. A site is an atom, a site potentially occupied by
...@@ -173,7 +174,7 @@ class Species(MObject): ...@@ -173,7 +174,7 @@ class Species(MObject):
""" """
m_section = Section( m_section = Section(
repeats=True, parent=StructureEntry.m_section, repeats=True, parent=OptimadeStructureEntry.m_section,
links=optimade_links('h.6.2.13')) links=optimade_links('h.6.2.13'))
name = Quantity( name = Quantity(
...@@ -264,4 +265,4 @@ def elastic_obj(source: MObject, target_cls: type): ...@@ -264,4 +265,4 @@ def elastic_obj(source: MObject, target_cls: type):
return target return target
ESStructureEntry = elastic_mapping(StructureEntry.m_section, InnerDoc) ESOptimadeEntry = elastic_mapping(OptimadeStructureEntry.m_section, InnerDoc)
...@@ -18,7 +18,8 @@ import numpy as np ...@@ -18,7 +18,8 @@ import numpy as np
from nomad import config from nomad import config
from nomad.parsing import LocalBackend from nomad.parsing import LocalBackend
from nomad.normalizing.normalizer import SystemBasedNormalizer from nomad.normalizing.normalizer import SystemBasedNormalizer
from nomad.metainfo.optimade import StructureEntry as Optimade from nomad.metainfo import units
from nomad.metainfo.optimade import OptimadeStructureEntry
class OptimadeNormalizer(SystemBasedNormalizer): class OptimadeNormalizer(SystemBasedNormalizer):
...@@ -30,26 +31,29 @@ class OptimadeNormalizer(SystemBasedNormalizer): ...@@ -30,26 +31,29 @@ class OptimadeNormalizer(SystemBasedNormalizer):
def __init__(self, backend): def __init__(self, backend):
super().__init__(backend, all_sections=config.normalize.all_systems) super().__init__(backend, all_sections=config.normalize.all_systems)
def get_optimade_data(self, index) -> Optimade: def get_optimade_data(self, index) -> OptimadeStructureEntry:
""" """
The 'main' method of this :class:`SystemBasedNormalizer`. The 'main' method of this :class:`SystemBasedNormalizer`.
Normalizes the section with the given `index`. Normalizes the section with the given `index`.
Normalizes geometry, classifies, system_type, and runs symmetry analysis. Normalizes geometry, classifies, system_type, and runs symmetry analysis.
""" """
optimade = Optimade() optimade = OptimadeStructureEntry()
def get_value(key: str, default: Any = None, nonp: bool = False) -> Any: def get_value(key: str, default: Any = None, numpy: bool = False) -> Any:
try: try:
value = self._backend.get_value(key, index) value = self._backend.get_value(key, index)
if nonp and type(value).__module__ == np.__name__: if type(value) == np.ndarray and not numpy:
value = value.tolist() return value.tolist()
if isinstance(value, list) and numpy:
return np.array(value)
return value return value
except KeyError: except KeyError:
return default return default
from nomad.normalizing.system import normalized_atom_labels from nomad.normalizing.system import normalized_atom_labels
nomad_species = get_value('atom_labels', nonp=True) nomad_species = get_value('atom_labels')
# elements # elements
atoms = normalized_atom_labels(nomad_species) atoms = normalized_atom_labels(nomad_species)
...@@ -63,7 +67,7 @@ class OptimadeNormalizer(SystemBasedNormalizer): ...@@ -63,7 +67,7 @@ class OptimadeNormalizer(SystemBasedNormalizer):
optimade.elements.sort() optimade.elements.sort()
optimade.nelements = len(optimade.elements) optimade.nelements = len(optimade.elements)
optimade.elements_ratios = [ optimade.elements_ratios = [
optimade.nelements / atom_counts[element] atom_counts[element] / optimade.nelements
for element in optimade.elements] for element in optimade.elements]
# formulas # formulas
...@@ -77,11 +81,11 @@ class OptimadeNormalizer(SystemBasedNormalizer): ...@@ -77,11 +81,11 @@ class OptimadeNormalizer(SystemBasedNormalizer):
# sites # sites
optimade.nsites = len(nomad_species) optimade.nsites = len(nomad_species)
optimade.species_at_sites = nomad_species optimade.species_at_sites = nomad_species
optimade.lattice_vectors = get_value('lattice_vectors', nonp=True) optimade.lattice_vectors = (get_value('lattice_vectors', numpy=True) * units.m).to(units.angstrom).magnitude
optimade.cartesian_site_positions = get_value('atom_positions', nonp=True) optimade.cartesian_site_positions = (get_value('atom_positions', numpy=True) * units.m).to(units.angstrom).magnitude
optimade.dimension_types = [ optimade.dimension_types = [
1 if value else 0 1 if value else 0
for value in get_value('configuration_periodic_dimensions', nonp=True)] for value in get_value('configuration_periodic_dimensions')]
# TODO subsections with species def # TODO subsections with species def
# TODO optimade.structure_features # TODO optimade.structure_features
...@@ -93,4 +97,6 @@ class OptimadeNormalizer(SystemBasedNormalizer): ...@@ -93,4 +97,6 @@ class OptimadeNormalizer(SystemBasedNormalizer):
optimade = self.get_optimade_data(index) optimade = self.get_optimade_data(index)
self._backend.add_mi2_section(optimade) self._backend.add_mi2_section(optimade)
except Exception as e: except Exception as e:
import traceback
traceback.print_exc()
self.logger.warn('could not acquire optimade data', exc_info=e) self.logger.warn('could not acquire optimade data', exc_info=e)
...@@ -355,7 +355,7 @@ class LocalBackend(LegacyParserBackend): ...@@ -355,7 +355,7 @@ class LocalBackend(LegacyParserBackend):
def get_mi2_section(self, section_def: MI2Section): def get_mi2_section(self, section_def: MI2Section):
""" Allows to mix a metainfo2 style section into backend. """ """ Allows to mix a metainfo2 style section into backend. """
self.mi2_data.get(section_def.name, None) return self.mi2_data.get(section_def.name, None)
def __getattr__(self, name): def __getattr__(self, name):
""" Support for unimplemented and unexpected methods. """ """ Support for unimplemented and unexpected methods. """
......
import json
from nomad.processing import Upload
from nomad.search import SearchRequest
def test_get_entry(published: Upload):
calc_id = list(published.calcs)[0].calc_id
with published.upload_files.archive_file(calc_id) as f:
data = json.load(f)
assert 'OptimadeStructureEntry' in data
search_result = SearchRequest().search_parameter('calc_id', calc_id).execute_paginated()['results'][0]
assert 'optimade' in search_result
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import numpy as np
from nomad.metainfo.metainfo import MObject, Section, Quantity, Definition, sub_section from nomad.metainfo.metainfo import MObject, Section, Quantity, Definition, sub_section
...@@ -92,6 +93,7 @@ class System(MObject): ...@@ -92,6 +93,7 @@ class System(MObject):
m_section = Section(repeats=True, parent=Run.m_section) m_section = Section(repeats=True, parent=Run.m_section)
n_atoms = Quantity(type=int, default=0) n_atoms = Quantity(type=int, default=0)
atom_label = Quantity(type=str, shape=['n_atoms']) atom_label = Quantity(type=str, shape=['n_atoms'])
atom_positions = Quantity(type=np.dtype('f8'), shape=['n_atoms', 3])
class Parsing(MObject): class Parsing(MObject):
...@@ -251,3 +253,8 @@ class TestM1: ...@@ -251,3 +253,8 @@ class TestM1:
pass pass
else: else:
assert False, 'Expected TypeError' assert False, 'Expected TypeError'
def test_np(self):
system = System()
system.atom_positions = [[1, 2, 3]]
assert type(system.atom_positions) == np.ndarray
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment