diff --git a/nomad/app/optimade/filterparser.py b/nomad/app/optimade/filterparser.py index 0147802d27f47a5d840c5d9fe18ddc0146c457a5..48246a07130d6cb29e3c6352281fa5545be2ba13 100644 --- a/nomad/app/optimade/filterparser.py +++ b/nomad/app/optimade/filterparser.py @@ -46,7 +46,6 @@ def _get_transformer(nomad_properties, without_prefix): if 'search' in q.m_annotations} quantities['elements'].length_quantity = quantities['nelements'] - quantities['dimension_types'].length_quantity = quantities['dimension_types'] quantities['elements'].has_only_quantity = Quantity(name='only_atoms') quantities['elements'].nested_quantity = quantities['elements_ratios'] quantities['elements_ratios'].nested_quantity = quantities['elements_ratios'] diff --git a/nomad/app/optimade/infolinks.py b/nomad/app/optimade/infolinks.py index 0c0d52a6f2f45f957330f47ecfbd5918fa1db77c..dfd82af6562d6aff898b0133d54a3bc70f7285f9 100644 --- a/nomad/app/optimade/infolinks.py +++ b/nomad/app/optimade/infolinks.py @@ -48,7 +48,7 @@ class Info(Resource): }], 'formats': ['json'], 'entry_types_by_format': { - 'json': ['structures', 'calculations', 'info'] + 'json': ['structures', 'calculations'] }, 'available_endpoints': ['structures', 'calculations', 'info'], 'is_index': False @@ -73,11 +73,12 @@ class Links(Resource): result = [ { - "type": "parent", + "type": "links", "id": "index", "attributes": { "name": config.meta.name, "description": config.meta.description, + "link_type": "root", "base_url": { "href": url(version=None, prefix='index'), }, diff --git a/nomad/app/optimade/models.py b/nomad/app/optimade/models.py index b34a034b497937044a01796095c736b6d3591955..6516b185112c393c222fa6075538fd8a9bea2edc 100644 --- a/nomad/app/optimade/models.py +++ b/nomad/app/optimade/models.py @@ -25,9 +25,12 @@ from flask_restplus import fields import datetime import math from cachetools import cached +import numpy as np from nomad import config, datamodel, files from nomad.app.common import RFC3339DateTime +from nomad.normalizing.optimade import optimade_chemical_formula_reduced +from nomad.metainfo import Datetime, MEnum from nomad.datamodel import EntryMetadata from nomad.datamodel.dft import DFTMetadata from nomad.datamodel.optimade import OptimadeEntry @@ -248,7 +251,28 @@ def get_entry_properties(include_optimade: bool = True): def add_nmd_properties(prefix, section_cls): for quantity in section_cls.m_def.all_quantities.values(): name = prefix + quantity.name - properties[name] = dict(description=quantity.description) + if quantity.is_scalar: + if quantity.type == int: + prop_type = 'integer' + elif quantity.type == float: + prop_type = 'float' + elif quantity.type == str: + prop_type = 'string' + elif quantity.type == bool: + prop_type = 'boolean' + elif quantity.type == Datetime: + prop_type = 'timestamp' + elif isinstance(quantity.type, MEnum): + prop_type = 'string' + elif isinstance(quantity.type, np.dtype): + prop_type = 'float' + else: + prop_type = 'unknown' + else: + prop_type = 'list' + properties[name] = dict( + description=quantity.description if quantity.description else quantity.name, + type=prop_type) add_nmd_properties('_nmd_', EntryMetadata) add_nmd_properties('_nmd_dft_', DFTMetadata) @@ -276,6 +300,21 @@ class EntryDataObject: if response_fields is not None: for request_field in response_fields: + if request_field == 'chemical_formula_reduced': + # TODO remove when this is fixed in the index + # ensure correct order of elements in formula + attrs[request_field] = optimade_chemical_formula_reduced(attrs[request_field]) + + if request_field == 'dimension_types': + # TODO remove when this is fixed in the index + # ensure correct order of elements in formula + dts = attrs[request_field] + if isinstance(dts, int): + attrs[request_field] = [1] * dts + [0] * (3 - dts) + attrs['nperiodic_dimensions'] = dts + elif isinstance(dts, list): + attrs['nperiodic_dimensions'] = sum(dts) + if not request_field.startswith('_nmd_'): continue diff --git a/nomad/datamodel/optimade.py b/nomad/datamodel/optimade.py index d29e6762dd0a78073b53cd7b8032d21f3b984b0f..df596b9ecc6c4ee22cc91f9ebb18bb29671701ca 100644 --- a/nomad/datamodel/optimade.py +++ b/nomad/datamodel/optimade.py @@ -193,8 +193,7 @@ class OptimadeEntry(MSection): dimension_types = Quantity( type=int, shape=[3], default=[0, 0, 0], links=optimade_links('h.6.2.8'), - a_search=Search(value=lambda a: sum(a.dimension_types), mapping=Integer()), - a_optimade=Optimade(query=True, entry=True, sortable=False, type='list'), + a_optimade=Optimade(query=False, entry=True, sortable=False, type='list'), description=''' List of three integers. For each of the three directions indicated by the three lattice vectors (see property lattice_vectors). This list indicates if the direction is @@ -203,6 +202,16 @@ class OptimadeEntry(MSection): the Cartesian x, y, z directions. ''') + nperiodic_dimensions = Quantity( + type=int, derived=lambda a: sum(a.dimension_types), + links=optimade_links('h.6.2.8'), + a_search=Search(mapping=Integer()), + a_optimade=Optimade(query=True, entry=True, sortable=True, type='integer'), + description=''' + An integer specifying the number of periodic dimensions in the structure, equivalent + to the number of non-zero entries in dimension_types. + ''') + lattice_vectors = Quantity( type=np.dtype('f8'), shape=[3, 3], unit=ureg.angstrom, links=optimade_links('h.6.2.9'), @@ -240,7 +249,7 @@ class OptimadeEntry(MSection): # TODO assemblies structure_features = Quantity( - type=MEnum(['disorder', 'unknown_positions', 'assemblies']), shape=['1..*'], + type=MEnum(['disorder', 'unknown_positions', 'assemblies']), shape=['1..*'], default=[], links=optimade_links('h.6.2.15'), a_search=Search(), a_optimade=Optimade(query=True, entry=True, sortable=False, type='list'), description=''' diff --git a/nomad/normalizing/optimade.py b/nomad/normalizing/optimade.py index a3fea0e3f207ea333f978a7509ddd888365cb4ad..201dcc1dbd9fcb106062cec5b5c6defd7c8d0282 100644 --- a/nomad/normalizing/optimade.py +++ b/nomad/normalizing/optimade.py @@ -20,6 +20,7 @@ from typing import Any, Dict import numpy as np import re import ase.data +import ase.formula from string import ascii_uppercase import pint.quantity @@ -31,6 +32,24 @@ from nomad.datamodel.metainfo.public import section_system species_re = re.compile(r'^([A-Z][a-z]?)(\d*)$') +def optimade_chemical_formula_reduced(formula: str): + if formula is None: + return formula + + try: + ase_formula = ase.formula.Formula(formula).count() + result_formula = '' + for element in sorted(ase_formula.keys()): + result_formula += element + element_count = ase_formula[element] + if element_count > 1: + result_formula += str(element_count) + + return result_formula + except Exception: + return formula + + class OptimadeNormalizer(SystemBasedNormalizer): ''' @@ -92,7 +111,8 @@ class OptimadeNormalizer(SystemBasedNormalizer): for element in optimade.elements] # formulas - optimade.chemical_formula_reduced = get_value(section_system.chemical_composition_reduced) + optimade.chemical_formula_reduced = optimade_chemical_formula_reduced( + get_value(section_system.chemical_composition_reduced)) optimade.chemical_formula_hill = get_value(section_system.chemical_composition_bulk_reduced) optimade.chemical_formula_descriptive = optimade.chemical_formula_hill optimade.chemical_formula_anonymous = '' diff --git a/tests/app/test_optimade.py b/tests/app/test_optimade.py index 09d9c417dc9684e6a0813273e83719070cfe1341..e0f186a55fc148fbc9c1589217fe69c5a864ee56 100644 --- a/tests/app/test_optimade.py +++ b/tests/app/test_optimade.py @@ -122,10 +122,10 @@ def example_structures(elastic_infra, mongo_infra, raw_files_infra): ('chemical_formula_reduced CONTAINS "H2"', 3), ('chemical_formula_reduced CONTAINS "H"', 4), ('chemical_formula_reduced CONTAINS "C"', 1), - ('chemical_formula_reduced STARTS "H2"', 3), - ('chemical_formula_reduced STARTS WITH "H2"', 3), - ('chemical_formula_reduced ENDS WITH "C"', 1), - ('chemical_formula_reduced ENDS "C"', 1), + ('chemical_formula_reduced STARTS "H2"', 2), + ('chemical_formula_reduced STARTS WITH "H2"', 2), + ('chemical_formula_reduced ENDS WITH "O"', 4), + ('chemical_formula_reduced ENDS "C"', 0), ('chemical_formula_hill CONTAINS "1"', 0), ('chemical_formula_hill STARTS WITH "H" AND chemical_formula_hill ENDS WITH "O"', 3), ('NOT chemical_formula_descriptive ENDS WITH "1"', 4), @@ -136,14 +136,14 @@ def example_structures(elastic_infra, mongo_infra, raw_files_infra): ('elements LENGTH = 2', 3), ('elements LENGTH 2', 3), ('elements LENGTH = 3', 1), - ('dimension_types LENGTH = 0', 3), - ('dimension_types LENGTH = 1', 1), - ('nelements = 2 AND dimension_types LENGTH = 1', 1), - ('nelements = 3 AND dimension_types LENGTH = 1', 0), - ('nelements = 3 OR dimension_types LENGTH = 1', 2), - ('nelements > 1 OR dimension_types LENGTH = 1 AND nelements = 2', 4), - ('(nelements > 1 OR dimension_types LENGTH = 1) AND nelements = 2', 3), - ('NOT dimension_types LENGTH 1', 3), + ('nperiodic_dimensions = 0', 3), + ('nperiodic_dimensions = 1', 1), + ('nelements = 2 AND nperiodic_dimensions = 1', 1), + ('nelements = 3 AND nperiodic_dimensions = 1', 0), + ('nelements = 3 OR nperiodic_dimensions = 1', 2), + ('nelements > 1 OR nperiodic_dimensions = 1 AND nelements = 2', 4), + ('(nelements > 1 OR nperiodic_dimensions = 1) AND nelements = 2', 3), + ('NOT nperiodic_dimensions = 1', 3), ('nelements LENGTH = 1', -1), ('LENGTH nelements = 1', -1), ('chemical_formula_anonymous starts with "A"', -1),