diff --git a/gui/src/metainfo.json b/gui/src/metainfo.json index 4a4aab724272bc529b62de5dbe857e3b7534b0a0..5168d8421a44e770320e559c41d71b243212c9d6 100644 --- a/gui/src/metainfo.json +++ b/gui/src/metainfo.json @@ -12874,7 +12874,9 @@ }, "shape": [ "0..*" - ] + ], + "default": [], + "virtual": true } ], "sub_sections": [ diff --git a/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py b/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py index 8cbc09b3acba85bcfd03b65c550e2f309bbed3f3..24177cf64aad08d4e9d9c040271c67ff6ecf8eea 100644 --- a/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py +++ b/nomad/datamodel/metainfo/eln/perovskite_solar_cell_database/__init__.py @@ -37,11 +37,6 @@ def add_band_gap(archive, band_gap): band_structure = BandStructureElectronic(band_gap=[band_gap]) electronic = ElectronicProperties(band_structure_electronic=[band_structure]) archive.results.properties.electronic = electronic - props = archive.results.properties.available_properties - if not props: - props = [] - props.append('electronic.band_structure_electronic.band_gap') - archive.results.properties.available_properties = props def add_solar_cell(archive): @@ -54,12 +49,6 @@ def add_solar_cell(archive): archive.results.properties.optoelectronic = OptoelectronicProperties() if not archive.results.properties.optoelectronic.solar_cell: archive.results.properties.optoelectronic.solar_cell = SolarCell() - props = archive.results.properties.available_properties - if not props: - props = [] - if 'solar_cell' not in props: - props.append('solar_cell') - archive.results.properties.available_properties = props class Ref(MSection): diff --git a/nomad/datamodel/results.py b/nomad/datamodel/results.py index 2fe228ced265f7343d252098fad5bcaeb6c81622..a38792b18e35dcc3d1ead01c0dd1b762fe514b59 100644 --- a/nomad/datamodel/results.py +++ b/nomad/datamodel/results.py @@ -16,12 +16,14 @@ # limitations under the License. # +from typing import List import numpy as np from elasticsearch_dsl import Text from ase.data import chemical_symbols from nomad import config +from nomad.utils import traverse_reversed from nomad.atomutils import Formula from nomad.datamodel.metainfo.measurements import Spectrum from nomad.datamodel.metainfo.simulation.system import Atoms @@ -190,6 +192,43 @@ def get_formula_iupac(formula: str) -> str: return None if formula is None else Formula(formula).format('iupac') +def available_properties(root: MSection) -> List[str]: + '''Returns a list of property names that are available in results.properties. + + Args: + root: The metainfo section containing the properties + + Returns: + List of property names that are present + ''' + available_property_names = { + 'electronic.band_structure_electronic.band_gap': 'electronic.band_structure_electronic.band_gap', + 'electronic.band_structure_electronic': 'band_structure_electronic', + 'electronic.dos_electronic': 'dos_electronic', + 'electronic.greens_functions_electronic': 'greens_functions_electronic', + 'vibrational.dos_phonon': 'dos_phonon', + 'vibrational.band_structure_phonon': 'band_structure_phonon', + 'vibrational.energy_free_helmholtz': 'energy_free_helmholtz', + 'vibrational.heat_capacity_constant_volume': 'heat_capacity_constant_volume', + 'thermodynamic.trajectory': 'trajectory', + 'structural.radial_distribution_function': 'radial_distribution_function', + 'dynamical.mean_squared_displacement': 'mean_squared_displacement', + 'structural.radius_of_gyration': 'radius_of_gyration', + 'geometry_optimization': 'geometry_optimization', + 'mechanical.bulk_modulus': 'bulk_modulus', + 'mechanical.shear_modulus': 'shear_modulus', + 'mechanical.energy_volume_curve': 'energy_volume_curve', + 'spectroscopy.eels': 'eels', + 'optoelectronic.solar_cell': 'solar_cell', + } + available_properties: List[str] = [] + for path, shortcut in available_property_names.items(): + for _ in traverse_reversed(root, path.split('.')): + available_properties.append(shortcut) + break + return sorted(available_properties) + + tokenizer_formula = get_tokenizer(r'[A-Z][a-z]?\d*') @@ -2512,6 +2551,8 @@ class Properties(MSection): ) available_properties = Quantity( type=str, + default=[], + derived=lambda a: available_properties(a), shape=['0..*'], description='Subset of the property names that are present in this entry.', a_elasticsearch=Elasticsearch(material_entry_type), diff --git a/nomad/normalizing/results.py b/nomad/normalizing/results.py index 22d1a93073c326203b468180c1997e46c7c0c8e2..7c785bac6bbc3d9990183403eac6e5952a2372e8 100644 --- a/nomad/normalizing/results.py +++ b/nomad/normalizing/results.py @@ -26,6 +26,7 @@ import matid.geometry # pylint: disable=import-error from nomad import config from nomad import atomutils +from nomad.utils import traverse_reversed from nomad.atomutils import Formula from nomad.normalizing.normalizer import Normalizer from nomad.normalizing.method import MethodNormalizer @@ -114,34 +115,6 @@ class ResultsNormalizer(Normalizer): for measurement in self.entry_archive.measurement: self.normalize_measurement(measurement) - # Add the list of available_properties: it is a selected subset of the - # stored properties. - available_property_names = { - "results.properties.electronic.band_structure_electronic.band_gap": "electronic.band_structure_electronic.band_gap", - "results.properties.electronic.band_structure_electronic": "band_structure_electronic", - "results.properties.electronic.dos_electronic": "dos_electronic", - "results.properties.electronic.greens_functions_electronic": "greens_functions_electronic", - "results.properties.vibrational.dos_phonon": "dos_phonon", - "results.properties.vibrational.band_structure_phonon": "band_structure_phonon", - "results.properties.vibrational.energy_free_helmholtz": "energy_free_helmholtz", - "results.properties.vibrational.heat_capacity_constant_volume": "heat_capacity_constant_volume", - "results.properties.thermodynamic.trajectory": "trajectory", - "results.properties.structural.radial_distribution_function": "radial_distribution_function", - "results.properties.dynamical.mean_squared_displacement": "mean_squared_displacement", - "results.properties.structural.radius_of_gyration": "radius_of_gyration", - "results.properties.geometry_optimization": "geometry_optimization", - "results.properties.mechanical.bulk_modulus": "bulk_modulus", - "results.properties.mechanical.shear_modulus": "shear_modulus", - "results.properties.mechanical.energy_volume_curve": "energy_volume_curve", - "results.properties.spectroscopy.eels": "eels", - } - available_properties: List[str] = [] - for path, shortcut in available_property_names.items(): - for _ in self.traverse_reversed(path.split('.')): - available_properties.append(shortcut) - break - results.properties.available_properties = sorted(available_properties) - def normalize_sample(self, sample) -> None: results = self.entry_archive.results @@ -260,7 +233,7 @@ class ResultsNormalizer(Normalizer): - There is a non-empty array of energies. """ def resolve_band_structure(path): - for bs in self.traverse_reversed(path): + for bs in traverse_reversed(self.entry_archive, path): if not bs.segment: continue valid = True @@ -319,7 +292,7 @@ class ResultsNormalizer(Normalizer): """ def resolve_dos(path): - for dos in self.traverse_reversed(path): + for dos in traverse_reversed(self.entry_archive, path): energies = dos.energies values = np.array([d.value.magnitude for d in dos.total]) if valid_array(energies) and valid_array(values): @@ -366,7 +339,7 @@ class ResultsNormalizer(Normalizer): """ def resolve_greens_functions(path): - for gfs in self.traverse_reversed(path): + for gfs in traverse_reversed(self.entry_archive, path): tau = gfs.tau iw = gfs.matsubara_freq values_gtau = np.array([np.absolute(gtau) for gtau in gfs.greens_function_tau.real]) @@ -403,7 +376,7 @@ class ResultsNormalizer(Normalizer): - There is a non-empty array of energies. """ path = ["run", "calculation", "band_structure_phonon"] - for bs in self.traverse_reversed(path): + for bs in traverse_reversed(self.entry_archive, path): if not bs.segment: continue valid = True @@ -430,7 +403,7 @@ class ResultsNormalizer(Normalizer): - There is a non-empty array of energies. """ path = ["run", "calculation", "dos_phonon"] - for dos in self.traverse_reversed(path): + for dos in traverse_reversed(self.entry_archive, path): energies = dos.energies values = np.array([d.value.magnitude for d in dos.total]) if valid_array(energies) and valid_array(values): @@ -450,7 +423,7 @@ class ResultsNormalizer(Normalizer): - There is a non-empty array of energies. """ path = ["workflow", "thermodynamics"] - for thermo_prop in self.traverse_reversed(path): + for thermo_prop in traverse_reversed(self.entry_archive, path): temperatures = thermo_prop.temperature energies = thermo_prop.vibrational_free_energy_at_constant_volume if valid_array(temperatures) and valid_array(energies): @@ -470,7 +443,7 @@ class ResultsNormalizer(Normalizer): - There is a non-empty array of energies. """ path = ["workflow", "thermodynamics"] - for thermo_prop in self.traverse_reversed(path): + for thermo_prop in traverse_reversed(self.entry_archive, path): temperatures = thermo_prop.temperature heat_capacities = thermo_prop.heat_capacity_c_v if valid_array(temperatures) and valid_array(heat_capacities): @@ -486,7 +459,7 @@ class ResultsNormalizer(Normalizer): properties based on the first found geometry optimization workflow. """ path = ["workflow"] - for workflow in self.traverse_reversed(path): + for workflow in traverse_reversed(self.entry_archive, path): # Check validity if workflow.type == "geometry_optimization" and workflow.calculations_ref: @@ -529,7 +502,7 @@ class ResultsNormalizer(Normalizer): """ path = ["workflow"] trajs = [] - for workflow in self.traverse_reversed(path): + for workflow in traverse_reversed(self.entry_archive, path): # Check validity if workflow.type == "molecular_dynamics": traj = Trajectory() @@ -590,7 +563,7 @@ class ResultsNormalizer(Normalizer): """ path = ["workflow", "molecular_dynamics", "results", "radial_distribution_functions"] rdfs = [] - for rdf_workflow in self.traverse_reversed(path): + for rdf_workflow in traverse_reversed(self.entry_archive, path): rdf_values = rdf_workflow.radial_distribution_function_values if rdf_values is not None: for rdf_value in rdf_values or []: @@ -620,7 +593,7 @@ class ResultsNormalizer(Normalizer): """ path_workflow = ["workflow"] rgs: List[RadiusOfGyration] = [] - for workflow in self.traverse_reversed(path_workflow): + for workflow in traverse_reversed(self.entry_archive, path_workflow): # Check validity if workflow.type == "molecular_dynamics": @@ -654,7 +627,7 @@ class ResultsNormalizer(Normalizer): """ path = ["workflow", "molecular_dynamics", "results", "mean_squared_displacements"] msds = [] - for msd_workflow in self.traverse_reversed(path): + for msd_workflow in traverse_reversed(self.entry_archive, path): msd_values = msd_workflow.mean_squared_displacement_values if msd_values is not None: for msd_value in msd_values or []: @@ -1011,28 +984,3 @@ class ResultsNormalizer(Normalizer): )) return shear_modulus - - def traverse_reversed(self, path: List[str]) -> Any: - """Traverses the given metainfo path in reverse order. Useful in - finding the latest reported section or value. - """ - def traverse(root, path, i): - if not root: - return - sections = getattr(root, path[i]) - if isinstance(sections, list): - for section in reversed(sections): - if i == len(path) - 1: - yield section - else: - for s in traverse(section, path, i + 1): - yield s - else: - if i == len(path) - 1: - yield sections - else: - for s in traverse(sections, path, i + 1): - yield s - for t in traverse(self.entry_archive, path, 0): - if t is not None: - yield t diff --git a/nomad/utils/__init__.py b/nomad/utils/__init__.py index d23b6bb0ef323aaa9c49f524951caa4c48befdea..0c793cb797ddec14d020a031393ab9bd5aba1018 100644 --- a/nomad/utils/__init__.py +++ b/nomad/utils/__init__.py @@ -589,3 +589,37 @@ def query_list_to_dict(path_list: List[Union[str, int]], value: Any) -> Dict[str current = current[key] i += 1 return returned + + +def traverse_reversed(archive: Any, path: List[str]) -> Any: + '''Traverses the given metainfo path in reverse order. Useful in finding the + latest reported section or value. + + Args: + archive: The root section to traverse + path: List of path names to traverse + + Returns: + Returns the last metainfo section or quantity in the given path or None + if not found. + ''' + def traverse(root, path, i): + if not root: + return + sections = getattr(root, path[i]) + if isinstance(sections, list): + for section in reversed(sections): + if i == len(path) - 1: + yield section + else: + for s in traverse(section, path, i + 1): + yield s + else: + if i == len(path) - 1: + yield sections + else: + for s in traverse(sections, path, i + 1): + yield s + for t in traverse(archive, path, 0): + if t is not None: + yield t