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

Added synonyms, m_follows, all_base_sections.

parent dfe8fa82
...@@ -56,13 +56,13 @@ class Run(MSection): ...@@ -56,13 +56,13 @@ class Run(MSection):
parsing = SubSection(sub_section=Parsing.m_def) parsing = SubSection(sub_section=Parsing.m_def)
class VaspRun(MSection): # class VaspRun(MSection):
""" All VASP specific quantities for section Run. """ # """ All VASP specific quantities for section Run. """
m_def = Section(extends=Run.m_def) # m_def = Section(extends=Run.m_def)
x_vasp_raw_format = Quantity( # x_vasp_raw_format = Quantity(
type=Enum(['xml', 'outcar']), # type=Enum(['xml', 'outcar']),
description='The file format of the parsed VASP mainfile.') # description='The file format of the parsed VASP mainfile.')
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -136,11 +136,12 @@ See the reference of classes :class:`Section` and :class:`Quantities` for detail ...@@ -136,11 +136,12 @@ See the reference of classes :class:`Section` and :class:`Quantities` for detail
# TODO validation # TODO validation
from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, cast from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, Set, cast
import sys import sys
import inspect import inspect
import re import re
import json import json
import itertools
import numpy as np import numpy as np
from pint.unit import _Unit from pint.unit import _Unit
...@@ -148,6 +149,7 @@ from pint import UnitRegistry ...@@ -148,6 +149,7 @@ from pint import UnitRegistry
is_bootstrapping = True is_bootstrapping = True
MSectionBound = TypeVar('MSectionBound', bound='MSection') MSectionBound = TypeVar('MSectionBound', bound='MSection')
T = TypeVar('T')
# Reflection # Reflection
...@@ -176,7 +178,7 @@ class DataType: ...@@ -176,7 +178,7 @@ class DataType:
class Dimension(DataType): class Dimension(DataType):
def type_check(self, value): def type_check(self, section, value):
if isinstance(value, int): if isinstance(value, int):
return value return value
...@@ -207,28 +209,6 @@ class Reference(DataType): ...@@ -207,28 +209,6 @@ class Reference(DataType):
self.section = section self.section = section
class QuantityReference(Reference):
""" Instances represent a special reference type to reference other Quantities.
It will allow quantity names as values and resolve them to the actual quantitiy
definition. Only works for quantities defined within the same section.
"""
def __init__(self):
super().__init__(Quantity.m_def)
def normalize(self, section: 'MSection', value: Union[str, 'Quantity']):
if isinstance(value, Quantity):
if value.m_parent != section.m_def:
raise TypeError('Must be a quantity of the same section.')
return value
value = section.m_def.all_quantities[value]
if value is not None:
raise TypeError('Must be the name of a quantity in the same section.')
return value
# TODO class Unit(DataType) # TODO class Unit(DataType)
# TODO class Datetime(DataType) # TODO class Datetime(DataType)
...@@ -325,11 +305,11 @@ class MSection(metaclass=MObjectMeta): ...@@ -325,11 +305,11 @@ class MSection(metaclass=MObjectMeta):
self.m_data[key] = value self.m_data[key] = value
else: else:
# self.m_data = {}
# self.m_update(**rest)
self.m_data = {} self.m_data = {}
for key, value in rest.items(): self.m_update(**rest)
self.m_data[key] = value # self.m_data = {}
# for key, value in rest.items():
# self.m_data[key] = value
@classmethod @classmethod
def __init_cls__(cls): def __init_cls__(cls):
...@@ -386,7 +366,7 @@ class MSection(metaclass=MObjectMeta): ...@@ -386,7 +366,7 @@ class MSection(metaclass=MObjectMeta):
if value is None and not check_item and definition.default is None: if value is None and not check_item and definition.default is None:
# Allow the default None value even if it would violate the type # Allow the default None value even if it would violate the type
return return value
def check_value(value): def check_value(value):
if isinstance(definition.type, Enum): if isinstance(definition.type, Enum):
...@@ -395,11 +375,15 @@ class MSection(metaclass=MObjectMeta): ...@@ -395,11 +375,15 @@ class MSection(metaclass=MObjectMeta):
elif isinstance(definition.type, type): elif isinstance(definition.type, type):
if not isinstance(value, definition.type): if not isinstance(value, definition.type):
raise TypeError('Value has wrong type.') raise TypeError(
'Value %s is not of type %s, required by quantity %s.' %
(value, definition.type, definition))
elif isinstance(definition.type, Section): elif isinstance(definition.type, Section):
if not isinstance(value, MSection) or value.m_def != definition.type: if not isinstance(value, MSection) or not value.m_follows(definition.type):
raise TypeError('The value is not a section of wrong section definition') raise TypeError(
'The section %s is not of section definition %s, required by quantity %s.' %
(value, definition.type, definition))
elif isinstance(definition.type, DataType): elif isinstance(definition.type, DataType):
value = definition.type.type_check(self, value) value = definition.type.type_check(self, value)
...@@ -606,7 +590,7 @@ class MSection(metaclass=MObjectMeta): ...@@ -606,7 +590,7 @@ class MSection(metaclass=MObjectMeta):
for name, value in kwargs.items(): for name, value in kwargs.items():
prop = self.m_def.all_properties.get(name, None) prop = self.m_def.all_properties.get(name, None)
if prop is None: if prop is None:
raise KeyError('%s is not an attribute of this section' % name) raise KeyError('%s is not an attribute of this section %s' % (name, self))
if isinstance(prop, SubSection): if isinstance(prop, SubSection):
if prop.repeats: if prop.repeats:
...@@ -621,6 +605,9 @@ class MSection(metaclass=MObjectMeta): ...@@ -621,6 +605,9 @@ class MSection(metaclass=MObjectMeta):
else: else:
setattr(self, name, value) setattr(self, name, value)
def m_follows(self, definition: 'Section') -> bool:
return self.m_def == definition or self.m_def in definition.all_base_sections
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. """
...@@ -851,14 +838,14 @@ class Quantity(Property): ...@@ -851,14 +838,14 @@ class Quantity(Property):
__synonym_for = property(lambda self: self.m_data.get('synonym_for', None)) __synonym_for = property(lambda self: self.m_data.get('synonym_for', None))
__default = property(lambda self: self.m_data.get('default', None)) __default = property(lambda self: self.m_data.get('default', None))
def __get__(self, obj, type=None): def __get__(self, obj, cls):
if obj is None: if obj is None:
# class (def) attribute case # class (def) attribute case
return self return self
# object (instance) attribute case # object (instance) attribute case
if self.__synonym_for is not None: if self.__synonym_for is not None:
return getattr(obj, self.__synonym_for.name) return getattr(obj, self.__synonym_for)
try: try:
return obj.m_data[self.__name] return obj.m_data[self.__name]
...@@ -872,7 +859,7 @@ class Quantity(Property): ...@@ -872,7 +859,7 @@ class Quantity(Property):
# object (instance) case # object (instance) case
if self.__synonym_for is not None: if self.__synonym_for is not None:
return setattr(obj, self.__synonym_for.name, value) return setattr(obj, self.__synonym_for, value)
if type(self.type) == np.dtype: if type(self.type) == np.dtype:
if type(value) != np.ndarray: if type(value) != np.ndarray:
...@@ -884,6 +871,7 @@ class Quantity(Property): ...@@ -884,6 +871,7 @@ class Quantity(Property):
value = value.tolist() value = value.tolist()
value = obj.m_type_check(self, value) value = obj.m_type_check(self, value)
obj.m_data[self.__name] = value obj.m_data[self.__name] = value
def __delete__(self, obj): def __delete__(self, obj):
...@@ -904,7 +892,7 @@ class SubSection(Property): ...@@ -904,7 +892,7 @@ class SubSection(Property):
sub_section: 'Quantity' = None sub_section: 'Quantity' = None
repeats: 'Quantity' = None repeats: 'Quantity' = None
def __get__(self, obj: MSection, type=None) -> Union[MSection, 'Section']: def __get__(self, obj, type=None):
if obj is None: if obj is None:
# the class attribute case # the class attribute case
return self return self
...@@ -919,7 +907,7 @@ class SubSection(Property): ...@@ -919,7 +907,7 @@ class SubSection(Property):
return m_data_value return m_data_value
def __set__(self, obj: MSection, value: Union[MSection, List[MSection]]): def __set__(self, obj, value):
raise NotImplementedError('Sub sections cannot be set directly. Use m_create.') raise NotImplementedError('Sub sections cannot be set directly. Use m_create.')
def __delete__(self, obj): def __delete__(self, obj):
...@@ -948,6 +936,15 @@ class Section(Definition): ...@@ -948,6 +936,15 @@ class Section(Definition):
base_sections: 'Quantity' = None base_sections: 'Quantity' = None
# TODO extends = Quantity(type=bool), denotes this section as a container for # TODO extends = Quantity(type=bool), denotes this section as a container for
# new quantities that belong to the base-class section definitions # new quantities that belong to the base-class section definitions
@cached_property
def all_base_sections(self) -> Set['Section']:
all_base_sections: Set['Section'] = set()
for base_section in self.base_sections: # pylint: disable=not-an-iterable
for base_base_section in base_section.all_base_sections:
all_base_sections.add(base_base_section)
all_base_sections.add(base_section)
return all_base_sections
@cached_property @cached_property
def all_properties(self) -> Dict[str, Union['SubSection', Quantity]]: def all_properties(self) -> Dict[str, Union['SubSection', Quantity]]:
...@@ -962,7 +959,7 @@ class Section(Definition): ...@@ -962,7 +959,7 @@ class Section(Definition):
""" All quantity definition in the given section definition. """ """ All quantity definition in the given section definition. """
all_quantities: Dict[str, Quantity] = {} all_quantities: Dict[str, Quantity] = {}
for section in self.base_sections + [self]: for section in itertools.chain(self.all_base_sections, [self]):
for quantity in section.m_data.get('quantities', []): for quantity in section.m_data.get('quantities', []):
all_quantities[quantity.name] = quantity all_quantities[quantity.name] = quantity
...@@ -1031,6 +1028,8 @@ Definition.m_def = Section(name='Definition') ...@@ -1031,6 +1028,8 @@ Definition.m_def = Section(name='Definition')
Property.m_def = Section(name='Property') Property.m_def = Section(name='Property')
Quantity.m_def = Section(name='Quantity') Quantity.m_def = Section(name='Quantity')
SubSection.m_def = Section(name='SubSection') SubSection.m_def = Section(name='SubSection')
Category.m_def = Section(name='Category')
Package.m_def = Section(name='Package')
Definition.name = Quantity( Definition.name = Quantity(
type=str, name='name', description=''' type=str, name='name', description='''
...@@ -1097,7 +1096,7 @@ Quantity.type = Quantity( ...@@ -1097,7 +1096,7 @@ Quantity.type = Quantity(
In the NOMAD CoE meta-info this was basically the ``dTypeStr``. In the NOMAD CoE meta-info this was basically the ``dTypeStr``.
''') ''')
Quantity.shape = Quantity( Quantity.shape = Quantity(
type=Dimension, shape=['0..*'], name='shape', description=''' type=Dimension(), shape=['0..*'], name='shape', description='''
The shape of the quantity that defines its dimensionality. The shape of the quantity that defines its dimensionality.
A shape is a list, where each item defines a dimension. Each dimension can be: A shape is a list, where each item defines a dimension. Each dimension can be:
...@@ -1121,15 +1120,12 @@ Quantity.default = Quantity( ...@@ -1121,15 +1120,12 @@ Quantity.default = Quantity(
The default value for this quantity. The default value for this quantity.
''') ''')
Quantity.synonym_for = Quantity( Quantity.synonym_for = Quantity(
type=QuantityReference(), description=''' type=str, description='''
With this set, the quantitiy will become a virtual quantity and its data is not stored With this set, the quantitiy will become a virtual quantity and its data is not stored
directly. Setting and getting quantity, will change the *synonym* quantity instead. directly. Setting and getting quantity, will change the *synonym* quantity instead. Use
the name of the quantity as value.
''') ''')
Package.m_def = Section(name='Package')
Category.m_def = Section(name='Category')
Package.section_definitions = SubSection( Package.section_definitions = SubSection(
sub_section=Section.m_def, name='section_definitions', repeats=True, sub_section=Section.m_def, name='section_definitions', repeats=True,
description=''' The sections defined in this package. ''') description=''' The sections defined in this package. ''')
......
...@@ -2,7 +2,7 @@ from ase.data import chemical_symbols ...@@ -2,7 +2,7 @@ from ase.data import chemical_symbols
from elasticsearch_dsl import Keyword, Integer, Float, InnerDoc, Nested from elasticsearch_dsl import Keyword, Integer, Float, InnerDoc, Nested
import numpy as np import numpy as np
from nomad.metainfo import MSection, Section, Quantity, Enum, units from nomad.metainfo import MSection, Section, Quantity, SubSection, Enum, units
def optimade_links(section: str): def optimade_links(section: str):
...@@ -27,6 +27,71 @@ class Optimade(): ...@@ -27,6 +27,71 @@ class Optimade():
pass pass
class Species(MSection):
"""
Used to describe the species of the sites of this structure. Species can be pure
chemical elements, or virtual-crystal atoms representing a statistical occupation of a
given site by multiple chemical elements.
"""
m_def = Section(links=optimade_links('h.6.2.13'))
name = Quantity(
type=str, a_optimade=Optimade(entry=True), description='''
The name of the species; the name value MUST be unique in the species list.
''')
chemical_symbols = Quantity(
type=Enum(chemical_symbols + ['x', 'vacancy']), shape=['1..*'],
a_optimade=Optimade(entry=True), description='''
A list of strings of all chemical elements composing this species.
It MUST be one of the following:
- a valid chemical-element name, or
- the special value "X" to represent a non-chemical element, or
- the special value "vacancy" to represent that this site has a non-zero probability
of having a vacancy (the respective probability is indicated in the concentration
list, see below).
If any one entry in the species list has a chemical_symbols list that is longer than 1
element, the correct flag MUST be set in the list structure_features (see
structure_features)
''')
concentration = Quantity(
type=float, shape=['1..*'],
a_optimade=Optimade(entry=True), description='''
A list of floats, with same length as chemical_symbols. The numbers represent the
relative concentration of the corresponding chemical symbol in this species. The
numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall
only in the following two categories:
- Numerical errors when representing float numbers in fixed precision, e.g. for two
chemical symbols with concentrations 1/3 and 2/3, the concentration might look
something like [0.33333333333, 0.66666666666]. If the client is aware that the sum
is not one because of numerical precision, it can renormalize the values so that the
sum is exactly one.
- Experimental errors in the data present in the database. In this case, it is the
responsibility of the client to decide how to process the data.
Note that concentrations are uncorrelated between different sites (even of the same
species).
''')
mass = Quantity(type=float, unit=units.amu, a_optimade=dict(entry='optional'))
original_name = Quantity(type=str, a_optimade=dict(entry='optional'), description='''
Can be any valid Unicode string, and SHOULD contain (if specified) the name of the
species that is used internally in the source database.
Note: With regards to "source database", we refer to the immediate source being
queried via the OPTiMaDe API implementation. The main use of this field is for source
databases that use species names, containing characters that are not allowed (see
description of the species_at_sites list).
''')
class OptimadeEntry(MSection): class OptimadeEntry(MSection):
m_def = Section( m_def = Section(
links=optimade_links('h.6.2'), links=optimade_links('h.6.2'),
...@@ -165,72 +230,7 @@ class OptimadeEntry(MSection): ...@@ -165,72 +230,7 @@ class OptimadeEntry(MSection):
- assemblies: This flag MUST be present if the assemblies list is present. - assemblies: This flag MUST be present if the assemblies list is present.
''') ''')
species = SubSection(sub_section=Species.m_def, repeats=True)
class Species(MSection):
"""
Used to describe the species of the sites of this structure. Species can be pure
chemical elements, or virtual-crystal atoms representing a statistical occupation of a
given site by multiple chemical elements.
"""
m_def = Section(
repeats=True, parent=OptimadeEntry.m_def,
links=optimade_links('h.6.2.13'))
name = Quantity(
type=str, a_optimade=Optimade(entry=True), description='''
The name of the species; the name value MUST be unique in the species list.
''')
chemical_symbols = Quantity(
type=Enum(chemical_symbols + ['x', 'vacancy']), shape=['1..*'],
a_optimade=Optimade(entry=True), description='''
A list of strings of all chemical elements composing this species.
It MUST be one of the following:
- a valid chemical-element name, or
- the special value "X" to represent a non-chemical element, or
- the special value "vacancy" to represent that this site has a non-zero probability
of having a vacancy (the respective probability is indicated in the concentration
list, see below).
If any one entry in the species list has a chemical_symbols list that is longer than 1
element, the correct flag MUST be set in the list structure_features (see
structure_features)
''')
concentration = Quantity(
type=float, shape=['1..*'],
a_optimade=Optimade(entry=True), description='''
A list of floats, with same length as chemical_symbols. The numbers represent the
relative concentration of the corresponding chemical symbol in this species. The
numbers SHOULD sum to one. Cases in which the numbers do not sum to one typically fall
only in the following two categories:
- Numerical errors when representing float numbers in fixed precision, e.g. for two
chemical symbols with concentrations 1/3 and 2/3, the concentration might look
something like [0.33333333333, 0.66666666666]. If the client is aware that the sum
is not one because of numerical precision, it can renormalize the values so that the
sum is exactly one.
- Experimental errors in the data present in the database. In this case, it is the
responsibility of the client to decide how to process the data.
Note that concentrations are uncorrelated between different sites (even of the same
species).
''')
mass = Quantity(type=float, unit=units.amu, a_optimade=dict(entry='optional'))
original_name = Quantity(type=str, a_optimade=dict(entry='optional'), description='''
Can be any valid Unicode string, and SHOULD contain (if specified) the name of the
species that is used internally in the source database.
Note: With regards to "source database", we refer to the immediate source being
queried via the OPTiMaDe API implementation. The main use of this field is for source
databases that use species names, containing characters that are not allowed (see
description of the species_at_sites list).
''')
def elastic_mapping(section: Section, base_cls: type) -> type: def elastic_mapping(section: Section, base_cls: type) -> type:
......
...@@ -47,12 +47,12 @@ class TestM3: ...@@ -47,12 +47,12 @@ class TestM3:
assert Section.name.m_def == Quantity.m_def assert Section.name.m_def == Quantity.m_def
assert Section.description.description is not None assert Section.description.description is not None
for quantity in Section.m_def.quantities: for quantity in iter(Section.m_def.quantities):
assert quantity.name in Section.m_def.all_properties assert quantity.name in Section.m_def.all_properties
assert quantity.name in Section.m_def.all_quantities assert quantity.name in Section.m_def.all_quantities
assert quantity.m_parent == Section.m_def assert quantity.m_parent == Section.m_def
for sub_section in Section.m_def.sub_sections: for sub_section in iter(Section.m_def.sub_sections):
assert sub_section.name in Section.m_def.all_properties assert sub_section.name in Section.m_def.all_properties
assert sub_section.name in Section.m_def.all_sub_sections assert sub_section.name in Section.m_def.all_sub_sections
assert sub_section.sub_section in Section.m_def.all_sub_sections_by_section assert sub_section.sub_section in Section.m_def.all_sub_sections_by_section
...@@ -141,11 +141,11 @@ class TestM2: ...@@ -141,11 +141,11 @@ class TestM2:
def test_package(self): def test_package(self):
assert example_package.name == 'nomad.metainfo.example' assert example_package.name == 'nomad.metainfo.example'
assert example_package.description == 'An example metainfo package.' assert example_package.description == 'An example metainfo package.'
assert len(example_package.m_sub_sections(Section)) == 4 assert len(example_package.m_sub_sections(Section)) == 3
assert len(example_package.m_sub_sections(Category)) == 1 assert len(example_package.m_sub_sections(Category)) == 1
def test_base_sections(self): def test_base_sections(self):
assert Definition.m_def in Section.m_def.base_sections assert Definition.m_def in iter(Section.m_def.base_sections)
print(Section.m_def.base_sections) print(Section.m_def.base_sections)
assert 'name' in Section.m_def.all_quantities assert 'name' in Section.m_def.all_quantities
assert 'name' in Quantity.m_def.all_quantities assert 'name' in Quantity.m_def.all_quantities
...@@ -248,7 +248,7 @@ class TestM1: ...@@ -248,7 +248,7 @@ class TestM1:
def test_synonym(self): def test_synonym(self):
system = System() system = System()
system.lattice_vectors = [[1.2e-10, 0, 0], [0, 1.2e-10, 0], [0, 0, 1.2e-10]] system.lattice_vectors = [[1.2e-10, 0, 0], [0, 1.2e-10, 0], [0, 0, 1.2e-10]]
assert system.unit_cell == system.lattice_vectors assert np.array_equal(system.unit_cell, system.lattice_vectors)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def example_data(self): def example_data(self):
......
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