diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index 632f51b..debed59 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -14,6 +14,9 @@ jobs: pip install --upgrade pip pip install '.[dev]' --index-url https://gitlab.mpcdf.mpg.de/api/v4/projects/2187/packages/pypi/simple pip install coverage coveralls + - name: mypy + run: | + python -m mypy --ignore-missing-imports --follow-imports=silent --no-strict-optional src/nomad_parser_wannier90 tests - name: Test with pytest run: | python -m coverage run -m pytest -sv diff --git a/src/nomad_parser_wannier90/parser.py b/src/nomad_parser_wannier90/parser.py index bb5a61d..0ff083f 100644 --- a/src/nomad_parser_wannier90/parser.py +++ b/src/nomad_parser_wannier90/parser.py @@ -16,9 +16,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # + import os import logging import numpy as np +from typing import List, Optional +from structlog.stdlib import BoundLogger from nomad.units import ureg from nomad.datamodel import EntryArchive @@ -61,6 +64,7 @@ ) from .utils import get_files +from .win_parser import WInParser, Wannier90WInParser re_n = r'[\n\r]' @@ -228,29 +232,6 @@ def init_quantities(self): ] -class WInParser(TextParser): - def __init__(self): - super().__init__(None) - - def init_quantities(self): - def str_proj_to_list(val_in): - # To avoid inconsistent regex that can contain or not spaces - val_n = [x for x in val_in.split('\n') if x] - return [v.strip('[]').replace(' ', '').split(':') for v in val_n] - - self._quantities = [ - Quantity( - 'energy_fermi', rf'{re_n}fermi_energy\s*=\s*([\d\.\-]+)', repeats=False - ), - Quantity( - 'projections', - rf'[bB]egin [pP]rojections([\s\S]+?)(?:[eE]nd [pP]rojections)', - repeats=False, - str_operation=str_proj_to_list, - ), - ] - - class HrParser(TextParser): def __init__(self): super().__init__(None) @@ -267,7 +248,6 @@ class Wannier90ParserData: def __init__(self): self.wout_parser = WOutParser() - self.win_parser = WInParser() self.band_dat_parser = DataTextParser() self.dos_dat_parser = DataTextParser() self.hr_parser = HrParser() @@ -278,96 +258,105 @@ def __init__(self): # "conv_tol": "convergence_tolerance_max_localization", } - self._input_projection_units = {'Ang': 'angstrom', 'Bohr': 'bohr'} - - # Angular momentum [l, mr] following Wannier90 tables 3.1 and 3.2 # TODO move to normalization or utils in nomad? - self._wannier_orbital_symbols_map = { - 's': ('s', ''), - 'px': ('p', 'x'), - 'py': ('p', 'y'), - 'pz': ('p', 'z'), - 'dz2': ('d', 'z^2'), - 'dxz': ('d', 'xz'), - 'dyz': ('d', 'yz'), - 'dx2-y2': ('d', 'x^2-y^2'), - 'dxy': ('d', 'xy'), - 'fz3': ('f', 'z^3'), - 'fxz2': ('f', 'xz^2'), - 'fyz2': ('f', 'yz^2'), - 'fz(x2-y2)': ('f', 'z(x^2-y^2)'), - 'fxyz': ('f', 'xyz'), - 'fx(x2-3y2)': ('f', 'x(x^2-3y^2)'), - 'fy(3x2-y2)': ('f', 'y(3x^2-y^2)'), - } - self._wannier_orbital_numbers_map = { - (0, 1): ('s', ''), - (1, 1): ('p', 'x'), - (1, 2): ('p', 'y'), - (1, 3): ('p', 'z'), - (2, 1): ('d', 'z^2'), - (2, 2): ('d', 'xz'), - (2, 3): ('d', 'yz'), - (2, 4): ('d', 'x^2-y^2'), - (2, 5): ('d', 'xy'), - (3, 1): ('f', 'z^3'), - (3, 2): ('f', 'xz^2'), - (3, 3): ('f', 'yz^2'), - (3, 4): ('f', 'z(x^2-y^2)'), - (3, 5): ('f', 'xyz'), - (3, 6): ('f', 'x(x^2-3y^2)'), - (3, 7): ('f', 'y(3x^2-y^2)'), - } - def parse_system(self, simulation): - model_system = ModelSystem() - model_system.is_representative = True + def parse_atoms_state(self, labels: List[str]) -> List[AtomsState]: + """ + Parse the `AtomsState` from the labels by storing them as the `chemical_symbols`. - structure = self.wout_parser.get('structure') - if structure is None: - self.logger.error('Error parsing the structure from .wout') - return None + Args: + labels (List[str]): List of chemical element labels. + Returns: + (List[AtomsState]): List of `AtomsState` sections. + """ + atoms_state = [] + for label in labels: + atoms_state.append(AtomsState(chemical_symbol=label)) + return atoms_state + + def parse_atomic_cell(self) -> AtomicCell: + """ + Parse the `AtomicCell` from the `lattice_vectors` and `structure` regex quantities in `WOutParser`. + + Returns: + (AtomicCell): The parsed `AtomicCell` section. + """ atomic_cell = AtomicCell() - model_system.cell.append(atomic_cell) + + # Parsing `lattice_vectors` if self.wout_parser.get('lattice_vectors', []): lattice_vectors = np.vstack( self.wout_parser.get('lattice_vectors', [])[-3:] ) atomic_cell.lattice_vectors = lattice_vectors * ureg.angstrom - + # and `periodic_boundary_conditions` pbc = ( [True, True, True] if lattice_vectors is not None else [False, False, False] ) atomic_cell.periodic_boundary_conditions = pbc - labels = structure.get('labels') - for label in labels: - atoms_state = AtomsState(chemical_symbol=label) - atomic_cell.atoms_state.append(atoms_state) - if structure.get('positions') is not None: - atomic_cell.positions = structure.get('positions') * ureg.angstrom + + # Parsing `atoms_state` from `structure` + labels = self.wout_parser.get('structure', {}).get('labels') + atoms_state = self.parse_atoms_state(labels) + atomic_cell.atoms_state = atoms_state + # and parsing `positions` + if self.wout_parser.get('structure', {}).get('positions') is not None: + atomic_cell.positions = ( + self.wout_parser.get('structure', {}).get('positions') * ureg.angstrom + ) + return atomic_cell + + def parse_model_system(self, logger: BoundLogger) -> Optional[ModelSystem]: + """ + Parse the `ModelSystem` with the `AtomicCell` information. If the `structure` is not recognized in `WOutParser`, then return `None`. + + Returns: + (Optional[ModelSystem]): The parsed `ModelSystem` section. + """ + model_system = ModelSystem() + model_system.is_representative = True + + # If the `structure` is not parsed, return None + if self.wout_parser.get('structure') is None: + logger.error('Error parsing the structure from .wout') + return None + + atomic_cell = self.parse_atomic_cell() + model_system.cell.append(atomic_cell) return model_system - def parse_wannier(self): - sec_wannier = ModelWannier() + def parse_wannier(self) -> ModelWannier: + """ + Parse the `ModelWannier` section from the `WOutParser` quantities. + + Returns: + (ModelWannier): The parsed `ModelWannier` section. + """ + model_wannier = ModelWannier() for key in self._input_projection_mapping.keys(): setattr( - sec_wannier, + model_wannier, self._input_projection_mapping[key], self.wout_parser.get(key), ) if self.wout_parser.get('Niter'): - sec_wannier.is_maximally_localized = self.wout_parser.get('Niter', 0) > 1 - if self.wout_parser.get('energy_windows'): - sec_wannier.energy_window_outer = self.wout_parser.get( - 'energy_windows' - ).outer - sec_wannier.energy_window_inner = self.wout_parser.get( - 'energy_windows' - ).inner - return sec_wannier - - def parse_k_mesh(self): + model_wannier.is_maximally_localized = self.wout_parser.get('Niter', 0) > 1 + model_wannier.energy_window_outer = self.wout_parser.get( + 'energy_windows', {} + ).get('outer') + model_wannier.energy_window_inner = self.wout_parser.get( + 'energy_windows', {} + ).get('inner') + return model_wannier + + def parse_k_mesh(self) -> Optional[ModelKMesh]: + """ + Parse the `ModelKMesh` section from the `WOutParser` quantities. + + Returns: + (Optional[ModelKMesh]): The parsed `ModelKMesh` section. + """ sec_k_mesh = None k_mesh = self.wout_parser.get('k_mesh') if k_mesh: @@ -378,141 +367,22 @@ def parse_k_mesh(self): sec_k_mesh.points = np.complex128(k_mesh.k_points[::2]) return sec_k_mesh - def parse_method(self, simulation): - # Wannier90 section - wannier = self.parse_wannier() - if wannier is None: - self.logger.warning('Could not parse the ModelMethod Wannier section.') - return None - simulation.model_method.append(wannier) - - # KMesh section - k_mesh = self.parse_k_mesh() - if k_mesh: - wannier.numerical_settings.append(k_mesh) - - def parse_winput(self, simulation): - try: - model_system = simulation.model_system[-1] - atomic_cell = model_system.cell[0] - except Exception: - self.logger.warning( - 'Could not extract system.atoms and method sections for parsing win.' - ) - return None - - # Parsing from input - win_files = get_files('*.win', self.filepath, '*.wout') - if not win_files: - self.logger.warning('Input .win file not found.') - return None - if len(win_files) > 1: - self.logger.warning( - 'Multiple win files found. We will parse the first one.' - ) - self.win_parser.mainfile = win_files[0] - - def fract_cart_sites(atomic_cell, units, val): - for pos in atomic_cell.positions.to(units): - if np.array_equal(val, pos.magnitude): - index = atomic_cell.positions.magnitude.tolist().index( - pos.magnitude.tolist() - ) - return atomic_cell.atoms_state[index].chemical_symbol + def parse_model_method(self) -> ModelMethod: + """ + Parse the `ModelWannier(ModelMethod)` section from the `WOutParser` quantities. - # Set units in case these are defined in .win - projections = self.win_parser.get('projections', []) - if projections: - if not isinstance(projections, list): - projections = [projections] - if projections[0][0] in ['Bohr', 'Angstrom']: - x_wannier90_units = self._input_projection_units[projections[0][0]] - projections.pop(0) - else: - x_wannier90_units = 'angstrom' - if projections[0][0] == 'random': - return + Returns: + (ModelMethod): The parsed `ModelWannier(ModelMethod)` section. + """ + # `ModelMethod` section + model_wannier = self.parse_wannier() - def parse_child_atom_indices(atom, model_system_child, atomic_cell): - if atom.startswith('f='): # fractional coordinates - val = [float(x) for x in atom.replace('f=', '').split(',')] - val = np.dot(val, atomic_cell.lattice_vectors.magnitude) - sites = fract_cart_sites(atomic_cell, x_wannier90_units, val) - elif atom.startswith('c='): # cartesian coordinates - val = [float(x) for x in atom.replace('c=', '').split(',')] - sites = fract_cart_sites(atomic_cell, x_wannier90_units, val) - else: # atom label directly specified - sites = atom - # sec_atoms_group.n_entities = len(sites) # always 1 (only one atom per proj) - model_system_child.branch_label = sites - model_system_child.atom_indices = np.where( - [ - atom.chemical_symbol == model_system_child.branch_label - for atom in atomic_cell.atoms_state - ] - )[0] - - def parse_hubbard(model_system_child, atomic_cell): - hubbard = HubbardInteractions(u=3.0 * ureg.eV, j=0.5 * ureg.eV) - atomic_cell.atoms_state[ - model_system_child.atom_indices[0] - ].hubbard_interactions = hubbard - - def parse_orbitals_state(atom, model_system_child, atomic_cell): - for atom_index in model_system_child.atom_indices: - atom_state = atomic_cell.atoms_state[atom_index] - if atom != atom_state.chemical_symbol: - continue - try: - orbitals = projections[nat][1].split(';') - angular_momentum = None - for orb in orbitals: - sec_orbital_state = OrbitalsState() - # sec_orbital_state.n_orbitals = len(orbitals) - if orb.startswith('l='): # using angular momentum numbers - lmom = int( - orb.split(',mr')[0].replace('l=', '').split(',')[0] - ) - mrmom = int( - orb.split(',mr')[-1].replace('=', '').split(',')[0] - ) - angular_momentum = self._wannier_orbital_numbers_map.get( - (lmom, mrmom) - ) - else: # ang mom label directly specified - angular_momentum = self._wannier_orbital_symbols_map.get( - orb - ) - ( - sec_orbital_state.l_quantum_symbol, - sec_orbital_state.ml_quantum_symbol, - ) = angular_momentum - atom_state.orbitals_state.append(sec_orbital_state) - except Exception: - self.logger.warning('Projected orbital labels not found from win.') - return None - - # Populating AtomsGroup for projected atoms - for nat in range(len(projections)): - model_system_child = model_system.m_create(ModelSystem) - model_system_child.type = 'active_atom' - - # atom label always index=0 - atom = projections[nat][0] - try: - parse_child_atom_indices(atom, model_system_child, atomic_cell) - except Exception: - self.logger.warning( - 'Error finding the atom labels for the projection from win.' - ) - return None + # `NumericalSettings` sections + k_mesh = self.parse_k_mesh() + if k_mesh is not None: + model_wannier.numerical_settings.append(k_mesh) - # test hubbard - # parse_hubbard(model_system_child, atomic_cell) - - # orbital angular momentum always index=1 - # suggestion: shift to wout for projection? - parse_orbitals_state(atom, model_system_child, atomic_cell) + return model_wannier def parse_fermi_level(self, output): fermi_level = FermiLevel(variables=[]) @@ -574,12 +444,11 @@ def init_parser(self): self.wout_parser.logger = self.logger self.hr_parser.logger = self.logger - def parse(self, filepath, archive, logger): + def parse(self, filepath: str, archive: EntryArchive, logger: BoundLogger): self.filepath = filepath self.archive = archive self.maindir = os.path.dirname(self.filepath) self.mainfile = os.path.basename(self.filepath) - self.logger = logging.getLogger(__name__) if logger is None else logger self.init_parser() @@ -590,33 +459,27 @@ def parse(self, filepath, archive, logger): version=self.wout_parser.get('version', ''), link='https://wannier.org/', ) - model_system = self.parse_system(simulation) - simulation.model_system.append(model_system) - self.parse_method(simulation) - self.parse_winput(simulation) - self.parse_output(simulation) archive.m_add_sub_section(EntryArchive.data, simulation) - # TEST - # settings = [ - # {"e": 0.15, "li": 3, "ls": "f"}, - # {"state": "initial", "e": 0.3, "li": 3, "ls": "f", "n": 4}, - # {"e": 0.9, "li": 2, "ls": "d", "n": 4, "ml": -2, "mls": "xy"}, - # {"e": 0.25, "li": 3, "ls": "f", "ms": False}, - # {"e": 0.5, "li": 1, "ls": "p", "n": 4, "ml": 0, "mls": "z", "ms": False}, - # ] - # for sett in settings: - # test( - # archive, - # sett.get("i", [0]), - # n_electrons_excited=sett.get("e", 0), - # l_quantum_number=sett.get("li", 0), - # n_quantum_number=sett.get("n"), - # ml_quantum_number=sett.get("ml"), - # ms_quantum_bool=sett.get("ms"), - # j_quantum_number=sett.get("ji", []), - # mj_quantum_number=sett.get("mj", []), - # ) + # `ModelSystem` parsing + model_system = self.parse_model_system(logger) + if model_system is not None: + simulation.model_system.append(model_system) + + # Child `ModelSystem` and `OrbitalsState` parsing + win_files = get_files('*.win', self.filepath, '*.wout') + win_parser = Wannier90WInParser() + child_model_systems = win_parser.parse_child_model_systems( + win_files, model_system, logger + ) + model_system.model_system = child_model_systems + + # `ModelWannier(ModelMethod)` parsing + model_method = self.parse_model_method() + simulation.model_method.append(model_method) + + # `Outputs` parsing + self.parse_output(simulation) class Wannier90Parser: diff --git a/src/nomad_parser_wannier90/win_parser.py b/src/nomad_parser_wannier90/win_parser.py new file mode 100644 index 0000000..d480622 --- /dev/null +++ b/src/nomad_parser_wannier90/win_parser.py @@ -0,0 +1,271 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. +# See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import numpy as np +from typing import List, Optional, Tuple +from structlog.stdlib import BoundLogger + +from nomad.units import ureg +from nomad.parsing.file_parser import TextParser, Quantity + +from nomad_simulations.model_system import ModelSystem, AtomicCell +from nomad_simulations.atoms_state import OrbitalsState + + +class WInParser(TextParser): + def __init__(self): + super().__init__() + + def init_quantities(self): + def str_proj_to_list(val_in): + # To avoid inconsistent regex that can contain or not spaces + val_n = [x for x in val_in.split('\n') if x] + return [v.strip('[]').replace(' ', '').split(':') for v in val_n] + + self._quantities = [ + Quantity( + 'energy_fermi', rf'\n\rfermi_energy\s*=\s*([\d\.\-]+)', repeats=False + ), + Quantity( + 'projections', + rf'[bB]egin [pP]rojections([\s\S]+?)(?:[eE]nd [pP]rojections)', + repeats=False, + str_operation=str_proj_to_list, + ), + ] + + +class Wannier90WInParser: + def __init__(self): + self.win_parser = WInParser() + + self._input_projection_units = {'Ang': 'angstrom', 'Bohr': 'bohr'} + + # Angular momentum [l, mr] following Wannier90 tables 3.1 and 3.2 + self._wannier_orbital_symbols_map = { + 's': ('s', ''), + 'px': ('p', 'x'), + 'py': ('p', 'y'), + 'pz': ('p', 'z'), + 'dz2': ('d', 'z^2'), + 'dxz': ('d', 'xz'), + 'dyz': ('d', 'yz'), + 'dx2-y2': ('d', 'x^2-y^2'), + 'dxy': ('d', 'xy'), + 'fz3': ('f', 'z^3'), + 'fxz2': ('f', 'xz^2'), + 'fyz2': ('f', 'yz^2'), + 'fz(x2-y2)': ('f', 'z(x^2-y^2)'), + 'fxyz': ('f', 'xyz'), + 'fx(x2-3y2)': ('f', 'x(x^2-3y^2)'), + 'fy(3x2-y2)': ('f', 'y(3x^2-y^2)'), + } + self._wannier_orbital_numbers_map = { + (0, 1): ('s', ''), + (1, 1): ('p', 'x'), + (1, 2): ('p', 'y'), + (1, 3): ('p', 'z'), + (2, 1): ('d', 'z^2'), + (2, 2): ('d', 'xz'), + (2, 3): ('d', 'yz'), + (2, 4): ('d', 'x^2-y^2'), + (2, 5): ('d', 'xy'), + (3, 1): ('f', 'z^3'), + (3, 2): ('f', 'xz^2'), + (3, 3): ('f', 'yz^2'), + (3, 4): ('f', 'z(x^2-y^2)'), + (3, 5): ('f', 'xyz'), + (3, 6): ('f', 'x(x^2-3y^2)'), + (3, 7): ('f', 'y(3x^2-y^2)'), + } + + def _convert_positions_to_symbols( + self, atomic_cell: AtomicCell, units: str, positions: List[float] + ) -> Optional[str]: + """ + Convert the atom `positions` in fractional or cartesian coordinates to the atom `chemical_symbols`. + + Args: + atomic_cell (AtomicCell): The `AtomicCell` section to which `positions` are extracted + units (str): The units in which the positions are defined. + positions (List[float]): The positions in fractional or cartesian coordinates to be converted to `chemical_symbols`. + + Returns: + (Optional[str]): The `chemical_symbols` of the atom at the position `val`. + """ + for cell_position in atomic_cell.positions.to(units): + if np.array_equal(positions, cell_position.magnitude): + index = atomic_cell.positions.magnitude.tolist().index( + cell_position.magnitude.tolist() + ) + return atomic_cell.atoms_state[index].chemical_symbol + return None + + def parse_child_atom_indices( + self, + atom: str, + atomic_cell: AtomicCell, + units: str, + ) -> Tuple[str, List[int]]: + """ + Parse the atom indices for the child model system. + + Args: + atom (str): The atom string containing the positions information. + atomic_cell (AtomicCell): The `AtomicCell` section where `positions` are stored + units (str): The units in which the positions are defined. + + Returns: + (Tuple[str, List[int]]): The `branch_label` and `atom_indices` for the child model system. + """ + if atom.startswith('f='): # fractional coordinates + positions = [float(x) for x in atom.replace('f=', '').split(',')] + positions = np.dot(positions, atomic_cell.lattice_vectors.magnitude) + sites = self._convert_positions_to_symbols(atomic_cell, units, positions) + elif atom.startswith('c='): # cartesian coordinates + positions = [float(x) for x in atom.replace('c=', '').split(',')] + sites = self._convert_positions_to_symbols(atomic_cell, units, positions) + else: # atom label directly specified + sites = atom + + # Find the `atom_indices` which coincide with the `sites` + branch_label = sites + atom_indices = np.where( + [atom.chemical_symbol == branch_label for atom in atomic_cell.atoms_state] + )[0].tolist() + return branch_label, atom_indices + + def populate_orbitals_state( + self, + projection: List[str], + model_system_child: ModelSystem, + atomic_cell: AtomicCell, + logger: BoundLogger, + ) -> None: + """ + Populate the `OrbitalsState` sections for the AtomsState relevant for the Wannier projection. + + Args: + projection (List[str]): The projection information for the atom. + model_system_child (ModelSystem): The child model system to get the `atom_indices`. + atomic_cell (AtomicCell): The `AtomicCell` section where `positions` are stored. + logger (BoundLogger): The logger to log messages. + """ + atom = projection[0] + for atom_index in model_system_child.atom_indices: + atom_state = atomic_cell.atoms_state[atom_index] + if atom != atom_state.chemical_symbol: + continue + try: + orbitals = projection[1].split(';') + angular_momentum = None + for orb in orbitals: + orbital_state = OrbitalsState() + if orb.startswith('l='): # using angular momentum numbers + lmom = int(orb.split(',mr')[0].replace('l=', '').split(',')[0]) + mrmom = int(orb.split(',mr')[-1].replace('=', '').split(',')[0]) + angular_momentum = self._wannier_orbital_numbers_map.get( + (lmom, mrmom) + ) + else: # ang mom label directly specified + angular_momentum = self._wannier_orbital_symbols_map.get(orb) + ( + orbital_state.l_quantum_symbol, + orbital_state.ml_quantum_symbol, + ) = angular_momentum + atom_state.orbitals_state.append(orbital_state) + except Exception: + logger.warning('Projected orbital labels not found from win.') + return None + + def parse_child_model_systems( + self, + win_files: Optional[List[str]], + model_system: ModelSystem, + logger: BoundLogger, + ) -> Optional[List[ModelSystem]]: + """ + Parse the child model systems from the `*.win` file to be added as sub-sections of the parent `ModelSystem` section. We + also store the `OrbitalsState` information of the projected atoms. + + Args: + win_files (Optional[List[str]]): The list of `*.win` files. We will only use the first one. + model_system (ModelSystem): The parent `ModelSystem` to which the child model systems will be added. + logger (BoundLogger): The logger to log messages. + + Returns: + (Optional[List[ModelSystem]]): The list of child model systems with the projected atoms information. + """ + # Parsing only the first file `*.win` + if not win_files: + logger.warning('Input `*.win` files not found.') + return None + if len(win_files) > 1: + logger.warning('Multiple `*.win` files found. We will parse the first one.') + self.win_parser.mainfile = win_files[0] + + # Check if `atomic_cell` is present in `model_system`` + if model_system.cell is None or len(model_system.cell) == 0: + logger.warning( + 'Could not extract `model_system.cell` section from mainfile `*.wout`.' + ) + return None + atomic_cell = model_system.cell[0] + + # Set units in case these are defined in .win + projections = self.win_parser.get('projections', []) + if projections: + if not isinstance(projections, list): + projections = [projections] + if projections[0][0] in ['Bohr', 'Angstrom']: + wannier90_units = self._input_projection_units[projections[0][0]] + projections.pop(0) + else: + wannier90_units = 'angstrom' + if projections[0][0] == 'random': + return None + + # Populating AtomsGroup for projected atoms + model_system_childs = [] + for nat in range(len(projections)): + model_system_child = ModelSystem() + model_system_child.type = 'active_atom' + + # atom positions information always index=0 for `projections[nat]` + projection = projections[nat] + atom = projection[0] + try: + branch_label, atom_indices = self.parse_child_atom_indices( + atom, atomic_cell, wannier90_units + ) + model_system_child.branch_label = branch_label + model_system_child.atom_indices = atom_indices + except Exception: + logger.warning( + 'Error finding the atom labels for the projection from win.' + ) + return None + + # orbital angular momentum information always index=1 for `projections[nat]` + self.populate_orbitals_state( + projection, model_system_child, atomic_cell, logger + ) + model_system_childs.append(model_system_child) + + return model_system_childs