Source code for cideMOD.cell.components

#
# Copyright (c) 2023 CIDETEC Energy Storage.
#
# This file is part of cideMOD.
#
# cideMOD is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
cell_components creates and initializes the corresponding battery components
attributes. This also includes the functions for the weak formulation.
"""
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing_extensions import Self

from cideMOD.helpers.logging import VerbosityLevel, _print
from cideMOD.cell._factory import get_cell_component_class
from cideMOD.cell.parser import BaseComponentParser

# FIXME: In python version greater or equal 3.11, Self is implemented in typing


[docs] class BaseCellComponent(ABC): """ Base class for cell component creation. Parameters ---------- root: Optional[BaseCellComponent] Component to which it belongs. If it does not the case, then it should be None. config: BaseComponentParser Object where the cell component parameters are parsed. tag: str Tag to identify the component between the available tags. """ @classmethod @property @abstractmethod def _name_(cls): """Name of the component""" # raise NotImplementedError @classmethod @property def _root_name_(cls): """ Name of the root component. It should be None if there is no root component. This property could be also a list of strings. """ return None # NOTE: This attribute will be replaced during the registration process _allowed_components_: list = [] @classmethod @property def _tags_(cls): """ Available tags for this component. Override this property to define the available tags. If there are an undetermined number of tags, then this attribute should be None. Examples -------- >>> _tags_ = { 'anode': { 'label': 'a' } 'cathode': { 'label': 'c' } } Notes ----- Every component type must be unique, not only the tags but also the label and dictionary entries. """ return None _is_recursive_ = False @classmethod @property def name(cls): return cls._name_ @property def tag(self): return self._tag_ @property def label(self): return self._label_ @property def parser(self) -> BaseComponentParser: return self._parser_ @property def complete_tag(self): return self._parser_.complete_tag def __init__(self, root, parser: BaseComponentParser, tag: str = None, verbose=VerbosityLevel.NO_INFO): # Parse tag # NOTE: In this cases there should be only one instance of the component if self._tags_ is None: tag = tag or self._name_ elif tag is None: if len(self._tags_.keys()) > 1: raise ValueError(f"A tag must be provided to create the component '{self._name_}'") else: tag = list(self._tags_.keys())[0] # NOTE: In this cases there could be more than one instance of the component elif tag not in self._tags_.keys(): if not self._is_recursive_: raise ValueError(f"Unrecognized tag '{tag}' for the component '{self._name_}'. " + "Available options '" + "' '".join(self._tags_.keys()) + "'") if tag != parser._tag_ or self._name_ != parser._name_: raise ValueError( f"Component '{self._name_}' must be created with the corresponding parser class") self._tag_ = tag self._parser_ = parser self._root_component_ = root self._label_ = parser._label_ self._components_ = OrderedDict() self.cell = root.cell if root is not None and root._name_ != 'cell' else root self.verbose = verbose self.N = parser.N self._set_components()
[docs] def get_component(self, name) -> Self: """This method return the specified component""" if '.' in name: name = name[name.rfind('.') + 1:] if name not in self._components_.keys(): raise ValueError(f"Unrecognized component '{name}' of {self.complete_tag}. " + "Available options: '" + "' '".join(self._components_.keys()) + "'") return self._components_[name]
def _setup(self, models, problem): """ This method sets up both itself and the components it contains. """ if self.verbose >= VerbosityLevel.DETAILED_PROGRESS_INFO: _print(f" Setting up '{self.complete_tag}' parameters", comm=problem._comm) # Setup this component models.set_component_parameters(self, problem) # Setup each component for component in self._components_.values(): component._setup(models, problem) def _set_component(self, component): """ This method initialize and sets the given component as a component of this component. Parameters ---------- component: Union[BaseCellComponent,str] Object (or name of the object) where component parameters are preprocessed and stored. """ # NOTE: Assume that the BaseCellComponent has been initialized correctly # Initialize the new component if isinstance(component, str): component_cls: BaseCellComponent = get_cell_component_class(component) parser = self._parser_._components_[component] component = component_cls(self, parser, tag=component, verbose=self.verbose) # Set the new component to this component tag = component._tag_ if tag in self._components_.keys(): raise RuntimeError(f"Component '{tag}' already added to component '{self._name_}'") else: self._components_[tag] = component return component def _set_components(self): """ This method initialize and sets the components following the parser class components. """ for name in self._parser_._components_.keys(): component = self._set_component(name) setattr(self, component._tag_, component)
[docs] class ElectrodeParameters(BaseCellComponent): _name_ = 'electrode' _root_name_ = 'cell' def _set_components(self): super()._set_components() # Set each active material self.active_materials = [] for am_parser in self.parser.active_materials: am = self._components_[am_parser._tag_] self.active_materials.append(am) self.n_mat = len(self.active_materials)
[docs] class ActiveMaterialParameters(BaseCellComponent): _name_ = 'active_material' _root_name_ = ('electrode', 'active_material') @property def electrode_tag(self): return self._parser_.electrode_tag def __init__(self, root, parser, tag=None, verbose=VerbosityLevel.NO_INFO): super().__init__(root, parser, tag, verbose) self.electrode = root # NOTE: It could be electrode or active_material self.index = parser.index def _set_components(self): super()._set_components() # Set each active material inclusion self.inclusions = [] for inc_parser in self.parser.inclusions: inc = self._components_[inc_parser._tag_] self.inclusions.append(inc) self.n_inc = len(self.inclusions)
[docs] class BatteryCell(BaseCellComponent): """ Class that preprocesses and stores the cell parameters. Parameters ---------- problem: Problem Object tha handles the battery cell simulation. """ _name_ = 'cell' def __init__(self, problem): super().__init__(None, problem.cell_parser, verbose=problem.model_options.verbose) self.problem = problem self._comm = problem._comm self.verbose = problem.verbose self.structure = problem.cell_parser.structure # Set cell parameters self._setup(problem._models, problem) # Cell properties problem._models.compute_cell_properties(self)