diff --git a/nomad/metainfo/__init__.py b/nomad/metainfo/__init__.py index 34206203fdd15c80f172b9f9b4af1fa1ab1fca08..fbde5985a3ad7e9b974bf2c89ced0eb2e5bb61bf 100644 --- a/nomad/metainfo/__init__.py +++ b/nomad/metainfo/__init__.py @@ -1 +1 @@ -from .metainfo import MSection, Section, Quantity, Enum, units +from .metainfo import MSection, MCategory, Definition, Section, Quantity, Category, Package, Enum, units diff --git a/nomad/metainfo/example.py b/nomad/metainfo/example.py new file mode 100644 index 0000000000000000000000000000000000000000..b13cee7f08a08bfb336a939e6c4c73ef1b5f2a8b --- /dev/null +++ b/nomad/metainfo/example.py @@ -0,0 +1,96 @@ +""" An example metainfo package. """ + +import numpy as np + +from nomad.metainfo import MSection, MCategory, Section, Quantity, Enum, Package, units + +m_package = Package(links=['http://metainfo.nomad-coe.eu']) + + +class SystemHash(MCategory): + """ All quantities that contribute to what makes a system unique. """ + + +class Run(MSection): + """ All data that belongs to a single code run. """ + + code_name = Quantity(type=str, description='The name of the code that was run.') + code_version = Quantity(type=str, description='The version of the code that was run.') + + +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.') + + +class Parsing(MSection): + """ All data that describes the NOMAD parsing of this run. """ + m_def = Section(parent=Run.m_def) + + parser_name = Quantity(type=str) + parser_version = Quantity(type=str) + nomad_version = Quantity(type=str) + warnings = Quantity(type=str, shape=['0..*']) + + +class System(MSection): + """ All data that describes a simulated system. """ + m_def = Section(repeats=True, parent=Run.m_def) + + n_atoms = Quantity( + type=int, default=0, + description='Number of atoms in the simulated system.') + + atom_labels = Quantity( + type=str, shape=['n_atoms'], categories=[SystemHash.m_def], + description='The atoms in the simulated systems.') + + atom_positions = Quantity( + type=np.dtype('f'), shape=['n_atoms', 3], unit=units.m, categories=[SystemHash.m_def], + description='The atom positions in the simulated system.') + + lattice_vectors = Quantity( + type=np.dtype('f'), shape=[3, 3], unit=units.m, categories=[SystemHash.m_def], + description='The lattice vectors of the simulated unit cell.') + + periodic_dimensions = Quantity( + type=bool, shape=[3], categories=[SystemHash.m_def], + description='A vector of booleans indicating in which dimensions the unit cell is repeated.') + + +if __name__ == '__main__': + # Demonstration of how to reflect on the definitions + + # All definitions are metainfo data themselves, and they can be accessed like any other + # metainfo data. E.g. all section definitions are sections themselves. + + # To get quantities of a given section + print(Run.m_def.m_sub_sections(Quantity)) + + # Or all Sections in the package + print(m_package.m_sub_sections(Section)) # type: ignore, pylint: disable=undefined-variable + + # There are also some definition specific helper methods. + # For example to get all attributes (Quantities and possible sub-sections) of a section. + print(Run.m_def.attributes) + + # Demonstration on how to use the definitions, e.g. to create a run with system: + run = Run() + run.code_name = 'VASP' + run.code_version = '1.0.0' + + system = run.m_create(System) + system.n_atoms = 3 + system.atom_labels = ['H', 'H', 'O'] + + # Or to read data from existing metainfo data: + print(system.atom_labels) + + # To serialize the data: + print(run.m_to_json(indent=2)) + + # print(m_package.m_to_json(indent=2)) # type: ignore, pylint: disable=undefined-variable diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index bc919e86b1396fc8984019fbb91ecf250d5a5780..7d8c3aa3b7db1c56856975e4c3c8e08df30afe88 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -137,13 +137,12 @@ See the reference of classes :class:`Section` and :class:`Quantities` for detail """ # TODO validation -# TODO serialization/deserialization -# TODO packages from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, cast import sys import inspect import re +import json import numpy as np from pint.unit import _Unit @@ -301,7 +300,7 @@ class MSection(metaclass=MObjectMeta): # transfer name and description to m_def m_def.name = cls.__name__ if cls.__doc__ is not None: - m_def.description = inspect.cleandoc(cls.__doc__) + m_def.description = inspect.cleandoc(cls.__doc__).strip() m_def.section_cls = cls # add sub_section to parent section @@ -314,7 +313,7 @@ class MSection(metaclass=MObjectMeta): if isinstance(attr, Quantity): attr.name = name if attr.description is not None: - attr.description = inspect.cleandoc(attr.description) + attr.description = inspect.cleandoc(attr.description).strip() attr.__doc__ = attr.description # manual manipulation of m_data due to bootstrapping m_def.m_data.setdefault('Quantity', []).append(attr) @@ -476,7 +475,8 @@ class MSection(metaclass=MObjectMeta): return sub_section - def m_create(self, definition: SectionDef, **kwargs) -> 'MSection': + # TODO this should work with the section constructor + def m_create(self, definition: Type[MSectionBound], **kwargs) -> MSectionBound: """Creates a subsection and adds it this this section Args: @@ -495,7 +495,7 @@ class MSection(metaclass=MObjectMeta): section_cls = section_def.section_cls section_instance = section_cls(m_def=section_def, m_parent=self, **kwargs) - return self.m_add_sub_section(section_instance) + return cast(MSectionBound, self.m_add_sub_section(section_instance)) def __resolve_quantity(self, definition: Union[str, 'Quantity']) -> 'Quantity': """Resolves and checks the given quantity definition. """ @@ -568,11 +568,25 @@ class MSection(metaclass=MObjectMeta): else: yield name, self.m_data[name].m_to_dict() - for name in self.m_def.quantities: + for name, quantity in self.m_def.quantities.items(): if name in self.m_data: value = getattr(self, name) if hasattr(value, 'tolist'): value = value.tolist() + + # TODO + if isinstance(quantity.type, Section): + value = str(value) + # TODO + if isinstance(value, type): + value = str(value) + # TODO + if isinstance(value, np.dtype): + value = str(value) + # TODO + if isinstance(value, _Unit): + value = str(value) + yield name, value return {key: value for key, value in items()} @@ -604,9 +618,9 @@ class MSection(metaclass=MObjectMeta): section_instance.m_update(**dct) return section_instance - def m_to_json(self): + def m_to_json(self, **kwargs): """Returns the data of this section as a json string. """ - pass + return json.dumps(self.m_to_dict(), **kwargs) def m_all_contents(self) -> Iterable[Content]: """Returns an iterable over all sub and sub subs sections. """ @@ -651,7 +665,7 @@ class MCategory(metaclass=MObjectMeta): # transfer name and description to m_def m_def.name = cls.__name__ if cls.__doc__ is not None: - m_def.description = inspect.cleandoc(cls.__doc__) + m_def.description = inspect.cleandoc(cls.__doc__).strip() # add section cls' section to the module's package module_name = cls.__module__ @@ -894,7 +908,7 @@ class Package(Definition): pkg.name = module_name if pkg.description is None and module.__doc__ is not None: - pkg.description = inspect.cleandoc(module.__doc__) + pkg.description = inspect.cleandoc(module.__doc__).strip() return pkg diff --git a/tests/test_metainfo.py b/tests/test_metainfo.py index 4b7f8cbae673915da74ed54d649a439cf42aa62d..98a4f938f951d80d93ac78f847730e7867a47419 100644 --- a/tests/test_metainfo.py +++ b/tests/test_metainfo.py @@ -15,7 +15,8 @@ import pytest import numpy as np -from nomad.metainfo.metainfo import MSection, MCategory, Section, Quantity, Definition, Category, Package, sub_section +from nomad.metainfo.metainfo import MSection, MCategory, Section, Quantity, Definition, Category, sub_section +from nomad.metainfo.example import Run, System, SystemHash, Parsing, m_package as example_package def assert_section_def(section_def: Section): @@ -79,37 +80,11 @@ class TestPureReflection: assert getattr(obj, 'test_quantity') == 'test_value' -m_package = Package(description='package doc') - - class MaterialDefining(MCategory): """Quantities that add to what constitutes a different material.""" pass -class Run(MSection): - """ This is the description. - - And some more description. - """ - - code_name = Quantity( - type=str, description=''' - The code_name description. - ''') - - -class System(MSection): - m_def = Section(repeats=True, parent=Run.m_def) - n_atoms = Quantity(type=int, default=0, categories=[MaterialDefining.m_def]) - atom_label = Quantity(type=str, shape=['n_atoms'], categories=[MaterialDefining.m_def]) - atom_positions = Quantity(type=np.dtype('f8'), shape=['n_atoms', 3]) - - -class Parsing(MSection): - m_def = Section(parent=Run.m_def) - - class TestM2: """ Test for meta-info definitions. """ @@ -125,7 +100,7 @@ class TestM2: assert Run.m_def.parent is None def test_quantities(self): - assert len(Run.m_def.quantities) == 1 + assert len(Run.m_def.quantities) == 2 assert Run.m_def.quantities['code_name'] == Run.__dict__['code_name'] def test_sub_sections(self): @@ -133,7 +108,7 @@ class TestM2: assert Run.m_def.sub_sections['System'] == System.m_def def test_attributes(self): - assert len(Run.m_def.attributes) == 3 + assert len(Run.m_def.attributes) == 4 assert Run.m_def.attributes['System'] == System.m_def assert Run.m_def.attributes['code_name'] == Run.__dict__['code_name'] @@ -162,19 +137,19 @@ class TestM2: def test_quantity_description(self): assert Run.code_name.description is not None - assert Run.code_name.description == 'The code_name description.' + assert Run.code_name.description == 'The name of the code that was run.' assert Run.code_name.description.strip() == Run.code_name.description.strip() def test_direct_category(self): - assert len(System.atom_label.categories) - assert MaterialDefining.m_def in System.atom_label.categories - assert System.atom_label in MaterialDefining.m_def.definitions + assert len(System.atom_labels.categories) == 1 + assert SystemHash.m_def in System.atom_labels.categories + assert System.atom_labels in SystemHash.m_def.definitions def test_package(self): - assert m_package.name == __name__ - assert m_package.description is not None - assert len(m_package.m_sub_sections(Section)) == 3 - assert len(m_package.m_sub_sections(Category)) == 1 + 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(Category)) == 1 class TestM1: @@ -206,7 +181,7 @@ class TestM1: def test_defaults(self): assert System().n_atoms == 0 - assert System().atom_label is None + assert System().atom_labels is None try: System().does_not_exist assert False, 'Supposed unreachable' @@ -268,7 +243,7 @@ class TestM1: def test_wrong_shape_2(self): try: - System().atom_label = 'label' + System().atom_labels = 'label' assert False, 'Supposed unreachable' except TypeError: pass @@ -286,7 +261,7 @@ class TestM1: run.code_name = 'test code name' system: System = run.m_create(System) system.n_atoms = 3 - system.atom_label = ['H', 'H', 'O'] + system.atom_labels = ['H', 'H', 'O'] system.atom_positions = np.array([[1.2e-10, 0, 0], [0, 1.2e-10, 0], [0, 0, 1.2e-10]]) return run @@ -299,7 +274,7 @@ class TestM1: assert_section_instance(system) assert system.m_def == System.m_def assert system.n_atoms == 3 - assert system.atom_label == ['H', 'H', 'O'] + assert system.atom_labels == ['H', 'H', 'O'] assert type(system.atom_positions) == np.ndarray def test_to_dict(self, example_data):