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):
parsing = SubSection(sub_section=Parsing.m_def)
class VaspRun(MSection):
""" All VASP specific quantities for section Run. """
m_def = Section(extends=Run.m_def)
# class VaspRun(MSection):
# """ All VASP specific quantities for section Run. """
# m_def = Section(extends=Run.m_def)
x_vasp_raw_format = Quantity(
type=Enum(['xml', 'outcar']),
description='The file format of the parsed VASP mainfile.')
# x_vasp_raw_format = Quantity(
# type=Enum(['xml', 'outcar']),
# description='The file format of the parsed VASP mainfile.')
if __name__ == '__main__':
......
......@@ -136,11 +136,12 @@ See the reference of classes :class:`Section` and :class:`Quantities` for detail
# 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 inspect
import re
import json
import itertools
import numpy as np
from pint.unit import _Unit
......@@ -148,6 +149,7 @@ from pint import UnitRegistry
is_bootstrapping = True
MSectionBound = TypeVar('MSectionBound', bound='MSection')
T = TypeVar('T')
# Reflection
......@@ -176,7 +178,7 @@ class DataType:
class Dimension(DataType):
def type_check(self, value):
def type_check(self, section, value):
if isinstance(value, int):
return value
......@@ -207,28 +209,6 @@ class Reference(DataType):
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 Datetime(DataType)
......@@ -325,11 +305,11 @@ class MSection(metaclass=MObjectMeta):
self.m_data[key] = value
else:
# self.m_data = {}
# self.m_update(**rest)
self.m_data = {}
for key, value in rest.items():
self.m_data[key] = value
self.m_update(**rest)
# self.m_data = {}
# for key, value in rest.items():
# self.m_data[key] = value
@classmethod
def __init_cls__(cls):
......@@ -386,7 +366,7 @@ class MSection(metaclass=MObjectMeta):
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
return
return value
def check_value(value):
if isinstance(definition.type, Enum):
......@@ -395,11 +375,15 @@ class MSection(metaclass=MObjectMeta):
elif isinstance(definition.type, 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):
if not isinstance(value, MSection) or value.m_def != definition.type:
raise TypeError('The value is not a section of wrong section definition')
if not isinstance(value, MSection) or not value.m_follows(definition.type):
raise TypeError(
'The section %s is not of section definition %s, required by quantity %s.' %
(value, definition.type, definition))
elif isinstance(definition.type, DataType):
value = definition.type.type_check(self, value)
......@@ -606,7 +590,7 @@ class MSection(metaclass=MObjectMeta):
for name, value in kwargs.items():
prop = self.m_def.all_properties.get(name, 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 prop.repeats:
......@@ -621,6 +605,9 @@ class MSection(metaclass=MObjectMeta):
else:
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]:
"""Returns the data of this section as a json serializeable dictionary. """
......@@ -851,14 +838,14 @@ class Quantity(Property):
__synonym_for = property(lambda self: self.m_data.get('synonym_for', 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:
# class (def) attribute case
return self
# object (instance) attribute case
if self.__synonym_for is not None:
return getattr(obj, self.__synonym_for.name)
return getattr(obj, self.__synonym_for)
try:
return obj.m_data[self.__name]
......@@ -872,7 +859,7 @@ class Quantity(Property):
# object (instance) case
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(value) != np.ndarray:
......@@ -884,6 +871,7 @@ class Quantity(Property):
value = value.tolist()
value = obj.m_type_check(self, value)
obj.m_data[self.__name] = value
def __delete__(self, obj):
......@@ -904,7 +892,7 @@ class SubSection(Property):
sub_section: '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:
# the class attribute case
return self
......@@ -919,7 +907,7 @@ class SubSection(Property):
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.')
def __delete__(self, obj):
......@@ -948,6 +936,15 @@ class Section(Definition):
base_sections: 'Quantity' = None
# TODO extends = Quantity(type=bool), denotes this section as a container for
# 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
def all_properties(self) -> Dict[str, Union['SubSection', Quantity]]:
......@@ -962,7 +959,7 @@ class Section(Definition):
""" All quantity definition in the given section definition. """
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', []):
all_quantities[quantity.name] = quantity
......@@ -1031,6 +1028,8 @@ Definition.m_def = Section(name='Definition')
Property.m_def = Section(name='Property')
Quantity.m_def = Section(name='Quantity')
SubSection.m_def = Section(name='SubSection')
Category.m_def = Section(name='Category')
Package.m_def = Section(name='Package')
Definition.name = Quantity(
type=str, name='name', description='''
......@@ -1097,7 +1096,7 @@ Quantity.type = Quantity(
In the NOMAD CoE meta-info this was basically the ``dTypeStr``.
''')
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.
A shape is a list, where each item defines a dimension. Each dimension can be:
......@@ -1121,15 +1120,12 @@ Quantity.default = Quantity(
The default value for this 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
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(
sub_section=Section.m_def, name='section_definitions', repeats=True,
description=''' The sections defined in this package. ''')
......
......@@ -2,7 +2,7 @@ from ase.data import chemical_symbols
from elasticsearch_dsl import Keyword, Integer, Float, InnerDoc, Nested
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):
......@@ -27,6 +27,71 @@ class Optimade():
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):
m_def = Section(
links=optimade_links('h.6.2'),
......@@ -165,72 +230,7 @@ class OptimadeEntry(MSection):
- assemblies: This flag MUST be present if the assemblies list is present.
''')
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).
''')
species = SubSection(sub_section=Species.m_def, repeats=True)
def elastic_mapping(section: Section, base_cls: type) -> type:
......
......@@ -47,12 +47,12 @@ class TestM3:
assert Section.name.m_def == Quantity.m_def
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_quantities
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_sub_sections
assert sub_section.sub_section in Section.m_def.all_sub_sections_by_section
......@@ -141,11 +141,11 @@ class TestM2:
def test_package(self):
assert example_package.name == 'nomad.metainfo.example'
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
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)
assert 'name' in Section.m_def.all_quantities
assert 'name' in Quantity.m_def.all_quantities
......@@ -248,7 +248,7 @@ class TestM1:
def test_synonym(self):
system = System()
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')
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