From 62600994f2b4dab7e4a99e76d99ef339a0de068a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Jun 2023 09:42:18 +0200 Subject: [PATCH 1/6] Correct basis set scope in _exciting_ parser --- electronicparsers/exciting/parser.py | 2 +- electronicparsers/wien2k/parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electronicparsers/exciting/parser.py b/electronicparsers/exciting/parser.py index c186ed01..d8f658c8 100644 --- a/electronicparsers/exciting/parser.py +++ b/electronicparsers/exciting/parser.py @@ -1614,7 +1614,7 @@ def _set_orbital(source: TextParser, l_quantum_number: int, basis_set=[ BasisSet( type='plane waves', - scope=['valence'], + scope=['valence', 'interstitial'], cutoff_fractional=self.input_xml_parser.get('xs/cutoffapw', 7.), ), ] diff --git a/electronicparsers/wien2k/parser.py b/electronicparsers/wien2k/parser.py index 6b10199a..0d402acb 100644 --- a/electronicparsers/wien2k/parser.py +++ b/electronicparsers/wien2k/parser.py @@ -816,7 +816,7 @@ def parse_method(self): em = BasisSetContainer(scope=['wavefunction']) em.basis_set.append( BasisSet( - scope=['intersitial', 'valence'], + scope=['valence', 'intersitial'], type='plane waves', cutoff_fractional=source.get('rkmax'), ) From 048374f0f62fd828a26825837a1a50baa3e50fc8 Mon Sep 17 00:00:00 2001 From: "nathan.daelman@physik.hu-berlin.de" Date: Thu, 29 Jun 2023 14:37:31 +0200 Subject: [PATCH 2/6] Remove redundant core-level + TODO: fully remove --- electronicparsers/wien2k/parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electronicparsers/wien2k/parser.py b/electronicparsers/wien2k/parser.py index 0d402acb..423850af 100644 --- a/electronicparsers/wien2k/parser.py +++ b/electronicparsers/wien2k/parser.py @@ -828,7 +828,6 @@ def parse_method(self): for l_n in range(source.get('lmax', -1) + 1): orbital = OrbitalAPW() orbital.l_quantum_number = l_n - orbital.core_level = False e_param = mt.get('e_param', source.get('e_ref', .5) - .2) # TODO: check for +.2 case update = False apw_type = mt.get('type') From e7a4bffa33c626100d11d75520945c9bc990d71e Mon Sep 17 00:00:00 2001 From: "nathan.daelman@physik.hu-berlin.de" Date: Thu, 29 Jun 2023 20:37:59 +0200 Subject: [PATCH 3/6] Create universal utils function for storing APW orbitals & apply to exciting parsers + TODO: ascertain meaning of `matchingOrder` in `lo` --- electronicparsers/exciting/parser.py | 138 +++++++++++++++------------ electronicparsers/utils/__init__.py | 2 +- electronicparsers/utils/utils.py | 96 +++++++++++++++++++ 3 files changed, 175 insertions(+), 61 deletions(-) diff --git a/electronicparsers/exciting/parser.py b/electronicparsers/exciting/parser.py index d8f658c8..078d9429 100644 --- a/electronicparsers/exciting/parser.py +++ b/electronicparsers/exciting/parser.py @@ -27,7 +27,7 @@ from nomad.datamodel.metainfo.simulation.method import ( Method, DFT, Electronic, Smearing, XCFunctional, Functional, Scf, BasisSet, KMesh, FrequencyMesh, Screening, GW, Photon, BSE, CoreHole, BasisSetContainer, - OrbitalAPW, AtomParameters, + AtomParameters, ) from nomad.datamodel.metainfo.simulation.system import ( System, Atoms @@ -51,7 +51,7 @@ x_exciting_scrcoul_parameters ) from ..utils import ( - get_files, BeyondDFTWorkflowsParser + get_files, BeyondDFTWorkflowsParser, OrbitalAPWConstructor, ) from typing import Any, Iterable @@ -1550,63 +1550,7 @@ def _parse_band_out(self, sec_scc): sec_k_band_segment.value = band_energies[nb] + energy_fermi def _parse_species(self, sec_method): - def _set_orbital(source: TextParser, l_quantum_number: int, - order: int, type: str = '') -> OrbitalAPW: - if not type: - type = re.sub(r'\+lo', '', source.get('type', 'lapw')).upper() - return OrbitalAPW( - l_quantum_number=l_quantum_number, - type=type, - order=order, - energy_parameter=source['trialEnergy'] * ureg.hartree, - update=source['searchE'], - ) - - type_order_mapping = {' ': 0, 'apw': 1, 'lap': 2} - self.species_parser.parse() - species_data = self.species_parser.to_dict() - - # muffin-tin valence - radius = float(species_data['muffinTin']['radius']) - radialmeshPoints = int(species_data['muffinTin']['radialmeshPoints']) - radial_spacing = radius / radialmeshPoints - bs_val = BasisSet( - scope=['muffin-tin'], - radius=radius * ureg.bohr, - radius_lin_spacing=radial_spacing * ureg.bohr, - ) - lo_samplings = {lo['l']: lo.get('wf', []) for lo in species_data.get('lo', [])} - lmax = self.input_xml_parser.get('xs/lmaxapw', 10) - - for l_n in range(lmax + 1): - source = species_data.get('default', {}) - for custom_settings in species_data.get('custom', []): - if custom_settings['l'] == l_n: - source = custom_settings - break - for order in range(type_order_mapping[source.get('type', 3 * ' ')[:3]]): - bs_val.orbital.append(_set_orbital(source, l_n, order)) - - # Add lo's - if source.get('type', 2 * ' ')[-2:] == 'lo': - wfs = lo_samplings[l_n] if l_n in lo_samplings else [source] - for wf in wfs: - for order in range(wf.get('matchingOrder', 0), 2): - bs_val.orbital.append(_set_orbital(wf, l_n, order, type='LO')) - - # manage atom parameters - if not sec_method.atom_parameters: - sec_method.atom_parameters = [] - sp = species_data.get('sp', {}) - sec_method.atom_parameters.append( - AtomParameters( - atom_number=abs(sp.get('z')), - label=sp.get('chemicalSymbol'), - mass=sp.get('mass') * ureg.amu if sp.get('mass') else None, - ) - ) - bs_val.atom_parameters = sec_method.atom_parameters[-1] - + # process basis set data if not sec_method.electrons_representation: sec_method.electrons_representation = [ BasisSetContainer( @@ -1620,7 +1564,81 @@ def _set_orbital(source: TextParser, l_quantum_number: int, ] ) ] - sec_method.electrons_representation[0].basis_set.append(bs_val) + + # muffin-tin + if not (species_data := self.species_parser.results): + self.logger.warning(f'No species data found in {self.species_parser.filepath}') + return + + def _convert_keyval(key_vals: list[list]) -> dict[str, Any]: + bool_map = {'false': False, 'true': True} + key_vals_converted: dict[str, Any] = {} + for key_val in key_vals: + if len(key_val) != 2: + raise ValueError(f'Invalid key-value pair: {key_val}') + if key_val[1] in bool_map: + key_vals_converted[key_val[0]] = bool_map[key_val[1]] + else: + key_vals_converted[key_val[0]] = key_val[1] + return key_vals_converted + + # read orbitals + def _get_type(line_data: dict[str, Any]) -> str: + return re.sub(r'\+LO', '', line_data.get('type', 'lapw').upper()) + + orb_constr = OrbitalAPWConstructor() + l_max = self.input_xml_parser.get('xs/lmaxapw', 10) + # read default settings + if line_data := _convert_keyval(species_data.get('default', {}).get('key_val', [])): + for l in range(l_max): + orb_constr.add_orbital( + l, line_data['trialEnergy'] * ureg.hartree, + _get_type(line_data), line_data.get('searchE', False), + ) + # read custom settings + if lines_data := species_data.get('custom', []): + for line_data in lines_data: + line_data = _convert_keyval(line_data.get('key_val', [])) + orb_constr.overwrite_orbital( + line_data['l'], line_data['trialEnergy'] * ureg.hartree, + _get_type(line_data), line_data.get('searchE', False), + ) + # read in local orbitals + if lines_data := species_data.get('lo', []): + for line_data in lines_data: + l = line_data['l'] + for wf in line_data.get('wf', []): + wf = _convert_keyval(wf.get('key_val', [])) + orb_constr.add_orbital( + l, wf['trialEnergy'] * ureg.hartree, + 'LO', wf.get('searchE', False), + order=wf.get('matchingOrder', 0), # TODO check if this is correct + ) + # write out the orbitals + bs = BasisSet( + scope=['muffin-tin'], + orbital=orb_constr.get_orbitals(), + ) + if mt := species_data.get('muffinTin', {}): + if radius := mt.get('radius'): + radius = float(radius) * ureg.bohr + bs.radius = radius + if rmp := mt.get('radialmeshPoints'): + bs.radial_spacing = radius / int(rmp) + sec_method.electrons_representation[0].basis_set.append(bs) + + # manage atom parameters + if not sec_method.atom_parameters: + sec_method.atom_parameters = [] + if sp := _convert_keyval(species_data.get('sp', {}).get('key_val', [])): + sec_method.atom_parameters.append( + AtomParameters( + atom_number=abs(sp.get('z')) if sp.get('z') else None, + label=sp.get('chemicalSymbol'), + mass=sp.get('mass') * ureg.amu if sp.get('mass') else None, + ) + ) + sec_method.electrons_representation[0].basis_set[-1].atom_parameters = sec_method.atom_parameters[-1] def parse_file(self, name, section, filepath=None): # TODO add support for info.xml, wannier.out diff --git a/electronicparsers/utils/__init__.py b/electronicparsers/utils/__init__.py index ebeeb1e3..1c44a33d 100644 --- a/electronicparsers/utils/__init__.py +++ b/electronicparsers/utils/__init__.py @@ -17,5 +17,5 @@ # limitations under the License. from .utils import ( - extract_section, get_files, BeyondDFTWorkflowsParser + extract_section, get_files, BeyondDFTWorkflowsParser, OrbitalAPWConstructor, ) diff --git a/electronicparsers/utils/utils.py b/electronicparsers/utils/utils.py index 066816e4..a1f49735 100644 --- a/electronicparsers/utils/utils.py +++ b/electronicparsers/utils/utils.py @@ -28,6 +28,8 @@ ParticleHoleExcitationsMethod, ParticleHoleExcitationsResults, PhotonPolarization, PhotonPolarizationMethod, PhotonPolarizationResults ) +from nomad.datamodel.metainfo.simulation.method import OrbitalAPW +from typing import Any def extract_section(source: EntryArchive, path: str): @@ -283,3 +285,97 @@ def extract_polarization_outputs(): workflow.m_add_sub_section(ParticleHoleExcitations.tasks, task) xs_workflow_archive.workflow2 = workflow + + +class OrbitalAPWConstructor: + ''' + Class for storing and sorting the orbitals in a APW basis set. + ''' + def __init__(self, *args, **kwargs): + ''' + Initializer for the OrbitalAPWConstructor class. Accept: + - args: list of strings defining the input format for the orbitals, should match the quantity names in OrbitalAPW + default: ['l_quantum_number', 'energy_parameter', 'type', 'updated'] + - order: list of strings defining the order in which the orbitals are sorted, in descending order of relevance + - comparsion: list of strings defining the keys used for comparison of orbitals in `overwrite_orbital` + ''' + self.orbitals: list[dict[str, Any]] = [] + self.comparison: list[dict[str, Any]] = [] + self.input_format = ['l_quantum_number', 'energy_parameter', 'type', 'updated'] + if args: + self.input_format = args + self.term_order = kwargs.get( + 'order', + [ + 'l_quantum_number', 'j_quantum_number', 'k_quantum_number', + 'energy_parameter_n', 'type', 'order', 'energy_parameter', 'updated' + ] + ) + self.term_order.reverse() + self.comparison_keys = kwargs.get('comparison', ['l_quantum_number', 'order']) + + def _convert(self, *args, **kwargs) -> dict[str, Any]: + ''' + Convert the input arguments (`args`) to a `dict` as defined by `input_format`. + Keys outside of `input_format` can be added via k`wargs. + ''' + if len(args) != len(self.input_format): + raise ValueError('No. arguments mismatch: check `input_format`') + # sort alphabetically by keys to ensure proper matching in overwrite_orbital + return dict(sorted({**dict(zip(self.input_format, args)), **kwargs}.items())) + + def _extract_comparison(self, orbital: dict[str, Any]) -> dict[str, Any]: + ''' + Extract the keys used for comparison from the orbital, as defined in `comparison_keys`. + ''' + return {k: v for k, v in orbital.items() if k in self.comparison_keys} + + def add_orbital(self, *args, **kwargs): + ''' + Add an orbital to the storage (`orbitals` and `comparison`). + The input arguments (`args`) should match the `input_format`. + Keys outside of `input_format` can be added via `kwargs`. + ''' + if new_orbital := self._convert(*args, **kwargs): + self.orbitals.append(new_orbital) + self.comparison.append(self._extract_comparison(new_orbital)) + + def overwrite_orbital(self, *args, **kwargs): + ''' + If a new orbital matches the comparison keys, overwrite the old orbital with the new one. + The input arguments (`args`) should match the `input_format`. + Keys outside of `input_format` can be added via `kwargs`. + ''' + new_orbital = self._convert(*args, **kwargs) + new_comparison = self._extract_comparison(new_orbital) + try: + index = self.comparison.index(new_comparison) # in case of multiple matching comparison keys, `index` returns the last one + self.orbitals[index] = new_orbital + except ValueError: + pass + + def get_orbitals(self) -> list[OrbitalAPW]: + ''' + Return the stored orbitals as a sorted list of `OrbitalAPW` sections. + ''' + def _sort_func(orbital: dict[str, Any], term: str): + ''' + Helper function to guide the sorting process. + ''' + if term in orbital: + return orbital[term] + else: + return 0 + + # sort out the orbitals by id_keys + orbitals = self.orbitals + for term in self.term_order: + orbitals = sorted(orbitals, key=lambda orbital: _sort_func(orbital, term)) + # convert to NOMAD section + formatted_orbitals: list[OrbitalAPW] = [] + for orbital in orbitals: + formatted_orbital = OrbitalAPW() + for term, val in orbital.items(): + setattr(formatted_orbital, term, val) + formatted_orbitals.append(formatted_orbital) + return formatted_orbitals From e20a2c96669fa28e977a50ecbc8dd16154eb855d Mon Sep 17 00:00:00 2001 From: "nathan.daelman@physik.hu-berlin.de" Date: Thu, 13 Jul 2023 22:23:50 +0200 Subject: [PATCH 4/6] Rewrite FP-APW parsing for exciting + redesign `OrbitalAPWConstructor` interface --- electronicparsers/exciting/parser.py | 81 +++++++++++++------ electronicparsers/utils/utils.py | 116 ++++++++++++++++++--------- 2 files changed, 136 insertions(+), 61 deletions(-) diff --git a/electronicparsers/exciting/parser.py b/electronicparsers/exciting/parser.py index 078d9429..20b46770 100644 --- a/electronicparsers/exciting/parser.py +++ b/electronicparsers/exciting/parser.py @@ -53,7 +53,7 @@ from ..utils import ( get_files, BeyondDFTWorkflowsParser, OrbitalAPWConstructor, ) -from typing import Any, Iterable +from typing import Any, Iterable, Callable re_float = r'[-+]?\d+\.\d*(?:[Ee][-+]\d+)?' @@ -1570,7 +1570,9 @@ def _parse_species(self, sec_method): self.logger.warning(f'No species data found in {self.species_parser.filepath}') return + # helper functions def _convert_keyval(key_vals: list[list]) -> dict[str, Any]: + '''Helper function to convert key-value pairs to a dictionary''' bool_map = {'false': False, 'true': True} key_vals_converted: dict[str, Any] = {} for key_val in key_vals: @@ -1582,38 +1584,71 @@ def _convert_keyval(key_vals: list[list]) -> dict[str, Any]: key_vals_converted[key_val[0]] = key_val[1] return key_vals_converted - # read orbitals - def _get_type(line_data: dict[str, Any]) -> str: - return re.sub(r'\+LO', '', line_data.get('type', 'lapw').upper()) + def map_to_metainfo(settings: dict[str, Any], **kwargs) -> dict[str, Any]: + '''''' + new_settings: dict[str, Any] = {} + mapping = { + 'matchingOrder': 'order', 'n': 'energy_parameter_n', + 'trialEnergy': 'energy_parameter', 'kappa': 'kappa_quantum_number', + 'searchE': 'update', + } + for key, val in settings.items(): + if key in mapping: + new_settings[mapping[key]] = val + if key == 'trialEnergy': + new_settings[mapping[key]] *= ureg.hartree + elif key == 'type': + new_settings[key] = val.upper() + else: + new_settings[key] = val + return {'type': 'LAPW', **new_settings, **kwargs} + + def _unroll_lo(orbital: dict[str, Any]) -> list[dict[str, Any]]: + '''Helper function to unroll local orbitals''' + if (full_type := orbital.get('type').upper()).endswith('+LO'): + # Note: the input variable is modified in-place here + # This shouldn't be a problem when done in the right order + unrolled_lines = [{**orbital, 'type': full_type[:-3]}] + for order in range(2): + unrolled_lines.append({**orbital, 'type': 'LO', 'matchingOrder': order}) + return unrolled_lines + elif full_type is not None: + return [orbital] + else: + return [] orb_constr = OrbitalAPWConstructor() - l_max = self.input_xml_parser.get('xs/lmaxapw', 10) + order_map = {'APW': 1, 'LAPW': 2} # read default settings if line_data := _convert_keyval(species_data.get('default', {}).get('key_val', [])): - for l in range(l_max): - orb_constr.add_orbital( - l, line_data['trialEnergy'] * ureg.hartree, - _get_type(line_data), line_data.get('searchE', False), - ) + for l in range(self.input_xml_parser.get('xs/lmaxapw', 10)): + for orbital in _unroll_lo(line_data): + orb = map_to_metainfo(orbital, l_quantum_number=l) + if (orb_type := orb['type']) in order_map: + orb_constr.unroll_orbital(order_map[orb_type], orb) + orb_constr.append_orbital() + elif orb_type == 'LO': + orb_constr.append_wavefunction(orb) + orb_constr.append_orbital() # read custom settings if lines_data := species_data.get('custom', []): for line_data in lines_data: - line_data = _convert_keyval(line_data.get('key_val', [])) - orb_constr.overwrite_orbital( - line_data['l'], line_data['trialEnergy'] * ureg.hartree, - _get_type(line_data), line_data.get('searchE', False), - ) + for orbital in _unroll_lo(_convert_keyval(line_data.get('key_val', []))): + orb = map_to_metainfo(orbital, l_quantum_number=orbital.get('l')) + if (orb_type := orb['type']) in order_map: + orb_constr.unroll_orbital(order_map[orb_type], orb) + orb_constr.overwrite_orbital() + elif orb_type == 'LO': + orb_constr.append_wavefunction(orb) + orb_constr.append_orbital() # read in local orbitals if lines_data := species_data.get('lo', []): for line_data in lines_data: - l = line_data['l'] - for wf in line_data.get('wf', []): - wf = _convert_keyval(wf.get('key_val', [])) - orb_constr.add_orbital( - l, wf['trialEnergy'] * ureg.hartree, - 'LO', wf.get('searchE', False), - order=wf.get('matchingOrder', 0), # TODO check if this is correct - ) + if (l := line_data.get('l')) is not None: + for wf in line_data.get('wf', []): + wf = map_to_metainfo(_convert_keyval(wf.get('key_val', []))) + orb_constr.append_wavefunction(wf, l_quantum_number=l, type='LO') + orb_constr.append_orbital() # write out the orbitals bs = BasisSet( scope=['muffin-tin'], diff --git a/electronicparsers/utils/utils.py b/electronicparsers/utils/utils.py index a1f49735..2d7ccf39 100644 --- a/electronicparsers/utils/utils.py +++ b/electronicparsers/utils/utils.py @@ -19,6 +19,7 @@ import os from glob import glob +import numpy as np from nomad.datamodel import EntryArchive from nomad.datamodel.metainfo.simulation.run import Run @@ -29,7 +30,7 @@ PhotonPolarization, PhotonPolarizationMethod, PhotonPolarizationResults ) from nomad.datamodel.metainfo.simulation.method import OrbitalAPW -from typing import Any +from typing import Any, Union, Iterable def extract_section(source: EntryArchive, path: str): @@ -297,62 +298,98 @@ def __init__(self, *args, **kwargs): - args: list of strings defining the input format for the orbitals, should match the quantity names in OrbitalAPW default: ['l_quantum_number', 'energy_parameter', 'type', 'updated'] - order: list of strings defining the order in which the orbitals are sorted, in descending order of relevance - - comparsion: list of strings defining the keys used for comparison of orbitals in `overwrite_orbital` + - comparison: list of strings defining the keys used for comparison of orbitals in `overwrite_orbital` ''' self.orbitals: list[dict[str, Any]] = [] - self.comparison: list[dict[str, Any]] = [] - self.input_format = ['l_quantum_number', 'energy_parameter', 'type', 'updated'] + self.wavefunctions: dict[str, Any] = {} if args: self.input_format = args self.term_order = kwargs.get( 'order', [ 'l_quantum_number', 'j_quantum_number', 'k_quantum_number', - 'energy_parameter_n', 'type', 'order', 'energy_parameter', 'updated' + 'energy_parameter_n', 'energy_parameter', 'order', 'updated', ] ) self.term_order.reverse() - self.comparison_keys = kwargs.get('comparison', ['l_quantum_number', 'order']) + self.comparison_keys = kwargs.get( + 'comparison', + ['l_quantum_number', 'j_quantum_number', 'k_quantum_number'] + ) - def _convert(self, *args, **kwargs) -> dict[str, Any]: + def _convert(self, settings: dict[str, Any], **kwargs) -> dict[str, Any]: ''' Convert the input arguments (`args`) to a `dict` as defined by `input_format`. - Keys outside of `input_format` can be added via k`wargs. + Keys outside of `input_format` can be added via kwargs. ''' - if len(args) != len(self.input_format): - raise ValueError('No. arguments mismatch: check `input_format`') - # sort alphabetically by keys to ensure proper matching in overwrite_orbital - return dict(sorted({**dict(zip(self.input_format, args)), **kwargs}.items())) + return dict(sorted({**settings, **kwargs}.items())) - def _extract_comparison(self, orbital: dict[str, Any]) -> dict[str, Any]: + def append_wavefunction(self, settings, **kwargs): ''' - Extract the keys used for comparison from the orbital, as defined in `comparison_keys`. ''' - return {k: v for k, v in orbital.items() if k in self.comparison_keys} + if self.wavefunctions: + for key, val in self.wavefunctions.items(): + converted = self._convert(settings, **kwargs) + if getattr(OrbitalAPW, key, {}).get('shape'): + try: + self.wavefunctions[key].append(converted[key]) + except AttributeError: + self.wavefunctions[key] = np.append(self.wavefunctions[key], converted[key]) + elif val != converted[key]: + raise ValueError(f'Wavefunction {key} does not match previous value') + else: + for key, val in self._convert(settings, **kwargs).items(): + if getattr(OrbitalAPW, key, {}).get('shape'): + if hasattr(val, 'units'): # check if quantity + self.wavefunctions[key] = np.array([val.magnitude]) * val.units + else: + self.wavefunctions[key] = [val] + else: + self.wavefunctions[key] = val + + def unroll_orbital(self, orders: Union[int, list[int]], settings, **kwargs): + ''' + ''' + if isinstance(orders, int): + orders = list(range(orders)) + self.wavefunctions = {} # reset wavefunctions + for order in orders: + new_kwargs = {**kwargs, 'order': order} + self.append_wavefunction(settings, **new_kwargs) + + def append_orbital(self, *settings, **kwargs): + ''' + ''' + if self.wavefunctions: + self.orbitals.append(self.wavefunctions) + self.wavefunctions = {} + return + if len(settings) == 1: + if new_orbital := self._convert(settings[0], **kwargs): + self.orbitals.append(new_orbital) - def add_orbital(self, *args, **kwargs): + def _extract_comparison(self, orbital: dict[str, Any]) -> dict[str, Any]: ''' - Add an orbital to the storage (`orbitals` and `comparison`). - The input arguments (`args`) should match the `input_format`. - Keys outside of `input_format` can be added via `kwargs`. + Extract the keys used for comparison from the orbital, as defined in `comparison_keys`. ''' - if new_orbital := self._convert(*args, **kwargs): - self.orbitals.append(new_orbital) - self.comparison.append(self._extract_comparison(new_orbital)) + return {k: v for k, v in orbital.items() if k in self.comparison_keys} - def overwrite_orbital(self, *args, **kwargs): + def overwrite_orbital(self, *settings, **kwargs): ''' - If a new orbital matches the comparison keys, overwrite the old orbital with the new one. - The input arguments (`args`) should match the `input_format`. - Keys outside of `input_format` can be added via `kwargs`. ''' - new_orbital = self._convert(*args, **kwargs) - new_comparison = self._extract_comparison(new_orbital) - try: - index = self.comparison.index(new_comparison) # in case of multiple matching comparison keys, `index` returns the last one - self.orbitals[index] = new_orbital - except ValueError: - pass + if not(converted := self.wavefunctions): + if len(settings) == 1: + converted = self._convert(settings[0], **kwargs) + else: + return + converted_ids = self._extract_comparison(converted) + new_orbitals = [] + for orbital in self.orbitals: + orbital_ids = self._extract_comparison(orbital) + if orbital_ids != converted_ids: + new_orbitals.append(orbital) + self.orbitals = new_orbitals + self.append_orbital(*settings, **kwargs) def get_orbitals(self) -> list[OrbitalAPW]: ''' @@ -363,17 +400,20 @@ def _sort_func(orbital: dict[str, Any], term: str): Helper function to guide the sorting process. ''' if term in orbital: + if isinstance(val := orbital[term], Iterable): + try: + return sum(val) + except TypeError: + return tuple(val) return orbital[term] - else: - return 0 + return 0 # sort out the orbitals by id_keys - orbitals = self.orbitals for term in self.term_order: - orbitals = sorted(orbitals, key=lambda orbital: _sort_func(orbital, term)) - # convert to NOMAD section + orbitals = sorted(self.orbitals, key=lambda orbital: _sort_func(orbital, term)) formatted_orbitals: list[OrbitalAPW] = [] for orbital in orbitals: + # convert to NOMAD section formatted_orbital = OrbitalAPW() for term, val in orbital.items(): setattr(formatted_orbital, term, val) From 6f13113e164455de116803174a8ffc83dc98838d Mon Sep 17 00:00:00 2001 From: "nathan.daelman@physik.hu-berlin.de" Date: Tue, 1 Aug 2023 21:22:06 +0200 Subject: [PATCH 5/6] Update function names OrbitalAPWConstructor + extend documentation --- electronicparsers/exciting/parser.py | 8 +-- electronicparsers/utils/utils.py | 85 ++++++++++++++++++---------- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/electronicparsers/exciting/parser.py b/electronicparsers/exciting/parser.py index 20b46770..8852ff48 100644 --- a/electronicparsers/exciting/parser.py +++ b/electronicparsers/exciting/parser.py @@ -53,7 +53,7 @@ from ..utils import ( get_files, BeyondDFTWorkflowsParser, OrbitalAPWConstructor, ) -from typing import Any, Iterable, Callable +from typing import Any, Iterable re_float = r'[-+]?\d+\.\d*(?:[Ee][-+]\d+)?' @@ -1628,7 +1628,7 @@ def _unroll_lo(orbital: dict[str, Any]) -> list[dict[str, Any]]: orb_constr.unroll_orbital(order_map[orb_type], orb) orb_constr.append_orbital() elif orb_type == 'LO': - orb_constr.append_wavefunction(orb) + orb_constr.compose_orbital(orb) orb_constr.append_orbital() # read custom settings if lines_data := species_data.get('custom', []): @@ -1639,7 +1639,7 @@ def _unroll_lo(orbital: dict[str, Any]) -> list[dict[str, Any]]: orb_constr.unroll_orbital(order_map[orb_type], orb) orb_constr.overwrite_orbital() elif orb_type == 'LO': - orb_constr.append_wavefunction(orb) + orb_constr.compose_orbital(orb) orb_constr.append_orbital() # read in local orbitals if lines_data := species_data.get('lo', []): @@ -1647,7 +1647,7 @@ def _unroll_lo(orbital: dict[str, Any]) -> list[dict[str, Any]]: if (l := line_data.get('l')) is not None: for wf in line_data.get('wf', []): wf = map_to_metainfo(_convert_keyval(wf.get('key_val', []))) - orb_constr.append_wavefunction(wf, l_quantum_number=l, type='LO') + orb_constr.compose_orbital(wf, l_quantum_number=l, type='LO') orb_constr.append_orbital() # write out the orbitals bs = BasisSet( diff --git a/electronicparsers/utils/utils.py b/electronicparsers/utils/utils.py index 2d7ccf39..eac06731 100644 --- a/electronicparsers/utils/utils.py +++ b/electronicparsers/utils/utils.py @@ -291,31 +291,35 @@ def extract_polarization_outputs(): class OrbitalAPWConstructor: ''' Class for storing and sorting the orbitals in a APW basis set. + Essentially, ''' def __init__(self, *args, **kwargs): ''' - Initializer for the OrbitalAPWConstructor class. Accept: - - args: list of strings defining the input format for the orbitals, should match the quantity names in OrbitalAPW - default: ['l_quantum_number', 'energy_parameter', 'type', 'updated'] - - order: list of strings defining the order in which the orbitals are sorted, in descending order of relevance - - comparison: list of strings defining the keys used for comparison of orbitals in `overwrite_orbital` + Initializes an OrbitalAPWConstructor instance. + + Parameters: + - args: list of strings defining the input format for the orbitals. They should match the attribute names in the OrbitalAPW class. + default: ['l_quantum_number', 'energy_parameter', 'type'] + - kwargs: Optional keyword arguments, which can include: + - 'order': list of strings defining the order in which orbitals should be sorted. + - 'comparison': list of strings defining the keys used for comparing orbitals. ''' - self.orbitals: list[dict[str, Any]] = [] - self.wavefunctions: dict[str, Any] = {} + self.orbitals: list[dict[str, Any]] = [] # retains the orbitals + self.composed_orbital: dict[str, Any] = {} # retains an orbital composed of wavefunctions if args: - self.input_format = args - self.term_order = kwargs.get( + self._input_format = args # the names of the orbital settings being mapped + self._term_order = kwargs.get( 'order', [ 'l_quantum_number', 'j_quantum_number', 'k_quantum_number', 'energy_parameter_n', 'energy_parameter', 'order', 'updated', ] - ) - self.term_order.reverse() - self.comparison_keys = kwargs.get( + ) # order in which the orbitals are to be sorted + self._term_order.reverse() + self._comparison_keys = kwargs.get( 'comparison', ['l_quantum_number', 'j_quantum_number', 'k_quantum_number'] - ) + ) # attributes by which orbitals are compared def _convert(self, settings: dict[str, Any], **kwargs) -> dict[str, Any]: ''' @@ -324,45 +328,65 @@ def _convert(self, settings: dict[str, Any], **kwargs) -> dict[str, Any]: ''' return dict(sorted({**settings, **kwargs}.items())) - def append_wavefunction(self, settings, **kwargs): + def compose_orbital(self, settings: dict[str, Any], **kwargs): ''' + Update the orbital dictionary with the wavefunction attributes. + Non-repeating attributes are added as new keys, + while repeating attributes are appended to a list or numpy array. + Typically used to add local orbitals. + + Parameters: + - settings: a dictionary of the wavefunction's settings + - kwargs: additional settings that are not present in `input_format` but need to be included in the wavefunction ''' - if self.wavefunctions: - for key, val in self.wavefunctions.items(): + if self.composed_orbital: + for key, val in self.composed_orbital.items(): converted = self._convert(settings, **kwargs) - if getattr(OrbitalAPW, key, {}).get('shape'): + if getattr(OrbitalAPW, key, {}).get('shape'): # determine if the quantity is a vector try: - self.wavefunctions[key].append(converted[key]) + self.composed_orbital[key].append(converted[key]) except AttributeError: - self.wavefunctions[key] = np.append(self.wavefunctions[key], converted[key]) + self.composed_orbital[key] = np.append(self.composed_orbital[key], converted[key]) elif val != converted[key]: raise ValueError(f'Wavefunction {key} does not match previous value') else: for key, val in self._convert(settings, **kwargs).items(): - if getattr(OrbitalAPW, key, {}).get('shape'): - if hasattr(val, 'units'): # check if quantity - self.wavefunctions[key] = np.array([val.magnitude]) * val.units + if getattr(OrbitalAPW, key, {}).get('shape'): # determine if the quantity is a vector + if hasattr(val, 'units'): # check if is a quantity + self.composed_orbital[key] = np.array([val.magnitude]) * val.units else: - self.wavefunctions[key] = [val] + self.composed_orbital[key] = [val] else: - self.wavefunctions[key] = val + self.composed_orbital[key] = val def unroll_orbital(self, orders: Union[int, list[int]], settings, **kwargs): ''' + Reset the wavefunctions and unroll a (L)APW orbitals into a list of wavefunctions. + + Parameters: + - orders: an integer or a list of integers representing the orders of the orbital + - settings: settings for the orbital to be appended + - kwargs: additional settings that are not present in `input_format` but need to be included in the orbital ''' if isinstance(orders, int): orders = list(range(orders)) - self.wavefunctions = {} # reset wavefunctions + self.composed_orbital = {} # reset wavefunctions for order in orders: new_kwargs = {**kwargs, 'order': order} - self.append_wavefunction(settings, **new_kwargs) + self.compose_orbital(settings, **new_kwargs) def append_orbital(self, *settings, **kwargs): ''' + Append an orbital to the list of orbitals, + and flush the orbital composed of wavefunctions. + + Parameters: + - settings: attributes for the orbital to be appended + - kwargs: additional settings that are not present in `input_format` but need to be included in the orbital ''' - if self.wavefunctions: - self.orbitals.append(self.wavefunctions) - self.wavefunctions = {} + if self.composed_orbital: + self.orbitals.append(self.composed_orbital) + self.composed_orbital = {} return if len(settings) == 1: if new_orbital := self._convert(settings[0], **kwargs): @@ -376,8 +400,9 @@ def _extract_comparison(self, orbital: dict[str, Any]) -> dict[str, Any]: def overwrite_orbital(self, *settings, **kwargs): ''' + Overwrite a stored orbital matching the comparison_keys with the new settings. ''' - if not(converted := self.wavefunctions): + if not(converted := self.composed_orbital): if len(settings) == 1: converted = self._convert(settings[0], **kwargs) else: From bf5975848475544edfb7297b6e27c1b52b32c157 Mon Sep 17 00:00:00 2001 From: "nathan.daelman@physik.hu-berlin.de" Date: Tue, 1 Aug 2023 21:23:09 +0200 Subject: [PATCH 6/6] First rewrite of the Wien2k parser to use OrbitalAPWConstructor + correction wrongful mapping --- electronicparsers/wien2k/parser.py | 82 +++++++++++++++++------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/electronicparsers/wien2k/parser.py b/electronicparsers/wien2k/parser.py index 423850af..9662ccd7 100644 --- a/electronicparsers/wien2k/parser.py +++ b/electronicparsers/wien2k/parser.py @@ -41,6 +41,7 @@ ) from nomad.datamodel.metainfo.simulation.workflow import SinglePoint from .metainfo.wien2k import x_wien2k_section_equiv_atoms +from ..utils import OrbitalAPWConstructor class In0Parser(TextParser): @@ -76,14 +77,14 @@ def parse(self): self._results[tag] = int(val) except ValueError: self._results[tag] = float(val) - elif re.search(r'K-VECTORS', line): + elif re.search(r'K-VECTORS', line): # typically final line: no. k-vectors after reduction, e-min, e-max, no. bands continue # further specify the species setup elif num_orbitals > 0: line = line.strip().split() orbital_settings = {} for tag, val in zip(['l', 'e_param', 'e_diff', 'diff_search', 'type'], line[:5]): - for converter in (int, float, lambda x: x): + for converter in (int, float, lambda x: x): # cast the orbital keyword to the correct type try: orbital_settings[tag] = converter(val) break @@ -91,13 +92,15 @@ def parse(self): pass self._results['species'][-1]['orbital'].append(orbital_settings) num_orbitals -= 1 - # add a new species setup + # extract the default settings for a new atom + no. amendments elif num_orbitals == 0: - species_settings = {'orbital': []} - line = line.strip().split() - species_settings['e_param'] = float(line[0]) - species_settings['type'] = int(line[2]) - self._results['species'].append(species_settings) + self._results['species'].append( + { + 'e_param': float(line[0]), + 'type': int(line[2]), + 'orbital': [], + } + ) num_orbitals = int(line[1]) @@ -807,11 +810,20 @@ def parse_method(self): sec_k_mesh.points = kpoints[0] sec_k_mesh.weights = kpoints[1] - # basis + # basis set + def _being_updated(energy_step: float, error_handling: str) -> dict[str, bool]: + """Return a dictionary of flags indicating how the energy parameters is being optimized.""" + response = {'update': True, 'updated': False} + if energy_step == 0: + response['update'] = False + elif error_handling == 'STOP': + response['updated'] = True + return response + if self.in1_parser.mainfile: self.in1_parser.parse() if self.in1_parser._results: - type_mapping = ('LAPW', 'APW', 'HDLO', 'LO') + type_mapping = {0: 'LAPW', 1: 'APW', 2: 'HDLO'} # maybe change to function? source = self.in1_parser._results em = BasisSetContainer(scope=['wavefunction']) em.basis_set.append( @@ -821,32 +833,34 @@ def parse_method(self): cutoff_fractional=source.get('rkmax'), ) ) - for mt in source.get('species', []): + for atom in source.get('species', []): # TODO: rename `mt` to `atom`? bs = BasisSet( - scope=['muffin-tin', 'full-electron'], - spherical_harmonics_cutoff=source.get('lmax')) + scope=['muffin-tin'], # TODO: add core specs + spherical_harmonics_cutoff=source.get('lmax') # find default + ) + # Write out the orbitals for each atom for l_n in range(source.get('lmax', -1) + 1): - orbital = OrbitalAPW() - orbital.l_quantum_number = l_n - e_param = mt.get('e_param', source.get('e_ref', .5) - .2) # TODO: check for +.2 case - update = False - apw_type = mt.get('type') - last_orbital_type = None - for orb in mt['orbital']: - if l_n == orb['l']: - e_param = orb.get('e_param', e_param) - update = orb.get('e_diff', 0) is not None - apw_type = orb.get('type', apw_type) - if last_orbital_type == apw_type: - orbital.energy_parameter = e_param - orbital.update = update - orbital.type = type_mapping[3] - continue - last_orbital_type = apw_type - orbital.energy_parameter = e_param - orbital.update = update - orbital.type = type_mapping[apw_type] - bs.orbital.append(orbital) + orbital_constructor = OrbitalAPWConstructor() + ## e_param = atom.get('e_param', source.get('e_ref', .5) - .2) # TODO: check for +.2 case + orbital_constructor.append_orbital( + l_n, atom['e_param'], type_mapping(atom.get('type', 0)), + ) + # Apply amendments and additions + l_spec = None + for orb in atom['orbital']: + l_spec = orb['l'] + if l_spec != l_ref: + orbital_constructor.overwrite_orbital( + l_spec, orb['e_param'], orb.get('type', 0), + **_being_updated(orb.get('e_diff', 0)), + ) + else: + orbital_constructor.append_orbital( + l_spec, orb['e_param'], orb.get('type', 0), + **_being_updated(orb.get('e_diff', 0)), + ) + l_ref = l_spec + bs.orbital = orbital_constructor.get_orbitals() em.basis_set.append(bs) sec_method.electrons_representation.append(em)