Source code for cellpy.parameters.internal_settings

"""Internal settings and definitions and functions for getting them."""

import logging
import warnings
from collections import UserDict
from dataclasses import dataclass, fields, asdict
from typing import List, Optional

import pandas as pd

from cellpy import prms

[docs] CELLPY_FILE_VERSION = 8
[docs] MINIMUM_CELLPY_FILE_VERSION = 4
[docs] STEP_TABLE_VERSION = 5
[docs] RAW_TABLE_VERSION = 5
[docs] SUMMARY_TABLE_VERSION = 7
# if you change this, remember that both loading and saving uses this # constant at the moment, and check that loading old files still works # - and possibly refactor so that the old-file loaders contain the # appropriate pickle protocol:
[docs] PICKLE_PROTOCOL = 4
# For creating the sqlite database from Excel:
[docs] TABLE_NAME_SQLITE = "cells"
[docs] COLUMNS_EXCEL_PK = "id"
[docs] COLUMNS_RENAMER = { COLUMNS_EXCEL_PK: "pk", "batch": "comment_history", "cell_name": "name", "exists": "cell_exists", "group": "cell_group", "raw_file_names": "raw_data", "argument": "cell_spec", "nom_cap": "nominal_capacity", "freeze": "frozen", }
[docs] ATTRS_TO_IMPORT_FROM_EXCEL_SQLITE = [ "name", "label", "project", "cell_group", "cellpy_file_name", "instrument", "cell_type", "cell_design", "channel", "experiment_type", "mass_active", "area", "mass_total", "loading_active", "nominal_capacity", "comment_slurry", "comment_cell", "comment_general", "comment_history", "selected", "freeze", "cell_exists", ]
[docs] BATCH_ATTRS_TO_IMPORT_FROM_EXCEL_SQLITE = [ "comment_history", "sub_batch_01", "sub_batch_02", "sub_batch_03", "sub_batch_04", "sub_batch_05", "sub_batch_06", "sub_batch_07", ]
[docs] OTHERPATHS = ["rawdatadir", "cellpydatadir"]
@dataclass
[docs] class CellpyMeta: """Base class for meta-data in cellpy."""
[docs] def update(self, as_list: bool = False, **kwargs): """Updates from dictionary of form {key: [values]} Args: as_list (bool): pick only first scalar if True. **kwargs (dict): key word attributes to update. Returns: None """ for k, v in kwargs.items(): if not as_list: v = v[0] if hasattr(self, k): logging.debug(f"{k} -> {v}") setattr(self, k, v) else: logging.debug(f"[NOT-VALID]{k}:{v}")
[docs] def digest(self, as_list: bool = False, **kwargs): """Pops from dictionary of form {key: [values]} Args: as_list (bool): pick only first scalar if True. **kwargs (dict): key word attributes to pick. Returns: Dictionary containing the non-digested part. """ not_digested = {} for k, v in kwargs.items(): if not as_list: v = v[0] if hasattr(self, k): logging.debug(f"{k} -> {v}") setattr(self, k, v) else: logging.debug(f"{k}:{v} ->") not_digested[k] = v return not_digested
[docs] def to_frame(self): """Converts to pandas dataframe""" df = pd.DataFrame.from_dict(asdict(self), orient="index") df.index.name = "key" n_rows, n_cols = df.shape if n_cols == 1: columns = ["value"] else: columns = [f"value_{i:02}" for i in range(n_cols)] df.columns = columns return df
@dataclass
[docs] class CellpyMetaCommon(CellpyMeta): """Common (not test-dependent) meta-data for cellpy.""" # about test
[docs] cell_name: Optional[str] = None # used as property
[docs] start_datetime: Optional[str] = None
[docs] time_zone: Optional[str] = None
[docs] comment: Optional[prms.CellPyDataConfig] = prms.CellInfo.comment
[docs] file_errors: Optional[str] = None # not in use at the moment
[docs] raw_id: Optional[str] = None # used as property
[docs] cellpy_file_version: int = CELLPY_FILE_VERSION
# about tester
[docs] tester_ID: Optional[prms.CellPyDataConfig] = None
[docs] tester_server_software_version: Optional[prms.CellPyDataConfig] = None
[docs] tester_client_software_version: Optional[prms.CellPyDataConfig] = None
[docs] tester_calibration_date: Optional[prms.CellPyDataConfig] = None
# about cell
[docs] material: Optional[prms.CellPyDataConfig] = prms.Materials.default_material
# TODO @jepe: Maybe we should use values with units here instead (pint)?
[docs] mass: Optional[prms.CellPyDataConfig] = ( prms.Materials.default_mass ) # active material
[docs] tot_mass: Optional[prms.CellPyDataConfig] = ( prms.Materials.default_mass ) # total material
# volume: Optional[prms.CellPyDataConfig] = prms.CellInfo.volume
[docs] nom_cap: Optional[prms.CellPyDataConfig] = ( prms.Materials.default_nom_cap ) # nominal capacity # used as property
[docs] nom_cap_specifics: Optional[prms.CellPyDataConfig] = ( prms.Materials.default_nom_cap_specifics ) # nominal capacity type # used as property
[docs] active_electrode_area: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.active_electrode_area )
[docs] active_electrode_thickness: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.active_electrode_thickness )
[docs] electrolyte_volume: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.electrolyte_volume )
[docs] electrolyte_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.electrolyte_type
[docs] active_electrode_type: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.active_electrode_type )
[docs] counter_electrode_type: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.counter_electrode_type )
[docs] reference_electrode_type: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.reference_electrode_type )
[docs] experiment_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.experiment_type
[docs] cell_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.cell_type
[docs] separator_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.separator_type
[docs] active_electrode_current_collector: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.active_electrode_current_collector )
[docs] reference_electrode_current_collector: Optional[prms.CellPyDataConfig] = ( prms.CellInfo.reference_electrode_current_collector )
@dataclass
[docs] class CellpyMetaIndividualTest(CellpyMeta): """Test-dependent meta-data for cellpy.""" # ---------------- test dependent -------------------------------
[docs] channel_index: Optional[prms.CellPyDataConfig] = None
[docs] creator: Optional[str] = None
[docs] schedule_file_name = None
[docs] test_type: Optional[prms.CellPyDataConfig] = ( None # Not used (and might be put inside test_ID) )
[docs] voltage_lim_low: Optional[prms.CellPyDataConfig] = prms.CellInfo.voltage_lim_low
[docs] voltage_lim_high: Optional[prms.CellPyDataConfig] = prms.CellInfo.voltage_lim_high
[docs] cycle_mode: Optional[prms.CellPyDataConfig] = prms.Reader.cycle_mode
[docs] test_ID: Optional[prms.CellPyDataConfig] = ( None # id for the test - currently just a number; could become a list or more in the future )
# TODO: remove import of this
[docs] class HeaderDict(UserDict): """A Sub-class of dict to allow for tab-completion.""" def __setitem__(self, key: str, value: str) -> None: if key == "data": raise KeyError("protected key") super().__setitem__(key, value) self.__dict__[key] = value
@dataclass
[docs] class DictLikeClass: """Add some dunder-methods so that it does not break old code that used dictionaries for storing settings Remarks: it is not a complete dictionary experience - for example, setting new attributes (new keys) is not supported (raises ``KeyError`` if using the typical dict setting method) since it uses the ``dataclasses.fields`` method to find its members. """ def __getitem__(self, key): if key not in self._field_names: logging.debug(f"{key} not in fields") try: return getattr(self, key) except AttributeError: raise KeyError(f"missing key: {key}") def __setitem__(self, key, value): if key not in self._field_names: raise KeyError(f"creating new key not allowed: {key}") setattr(self, key, value) def __missing__(self, key): raise KeyError @property def _field_names(self): return [field.name for field in fields(self)] def __iter__(self): for field in self._field_names: yield field def _value_iter(self): for field in self._field_names: yield getattr(self, field)
[docs] def keys(self): return [key for key in self.__iter__()]
[docs] def values(self): return [v for v in self._value_iter()]
[docs] def items(self): return zip(self.keys(), self.values())
@dataclass
[docs] class BaseSettings(DictLikeClass): """Base class for internal cellpy settings. Usage:: @dataclass class MyCoolCellpySetting(BaseSetting): var1: str = "first var" var2: int = 12 """
[docs] def get(self, key): """Get the value (postfixes not supported).""" if key not in self.keys(): logging.critical(f"the column header '{key}' not found") return else: return self[key]
[docs] def to_frame(self): """Converts to pandas dataframe""" df = pd.DataFrame.from_dict(asdict(self), orient="index") df.index.name = "key" n_rows, n_cols = df.shape if n_cols == 1: columns = ["value"] else: columns = [f"value_{i:02}" for i in range(n_cols)] df.columns = columns return df
@dataclass
[docs] class BaseHeaders(BaseSettings): """Subclass of BaseSetting including option to add postfixes. Example: >>> header["key_postfix"] # returns "value_postfix" """
[docs] postfixes = []
def __getitem__(self, key): postfix = "" if key not in self._field_names: # check postfix: subs = key.split("_") _key = "_".join(subs[:-1]) _postfix = subs[-1] if _postfix in self.postfixes: postfix = f"_{_postfix}" key = _key try: v = getattr(self, key) return f"{v}{postfix}" except AttributeError: raise KeyError(f"missing key: {key}")
@dataclass
[docs] class InstrumentSettings(DictLikeClass): """Base class for instrument settings. Usage:: @dataclass class MyCoolInstrumentSetting(InstrumentSettings): var1: str = "first var" var2: int = 12 Remark! Try to use it as you would use a normal dataclass. """ ...
@dataclass
[docs] class CellpyUnits(BaseSettings): """These are the units used inside Cellpy. At least two sets of units needs to be defined; `cellpy_units` and `raw_units`. The `data.raw` dataframe is given in `raw_units` where the units are defined inside the instrument loader used. Since the `data.steps` dataframe is a summary of the step statistics from the `data.raw` dataframe, this also uses the `raw_units`. The `data.summary` dataframe contains columns with values directly from the `data.raw` dataframe given in `raw_units` as well as calculated columns given in `cellpy_units`. Remark that all input to cellpy through user interaction (or utils) should be in `cellpy_units`. This is also true for meta-data collected from the raw files. The instrument loader needs to take care of the translation from its raw units to `cellpy_units` during loading the raw data file for the meta-data (remark that this is not necessary and not recommended for the actual "raw" data that is going to be stored in the `data.raw` dataframe). As of 2022.09.29, cellpy does not automatically ensure unit conversion for input of meta-data, but has an internal method (`CellPyData.to_cellpy_units`) that can be used. These are the different attributes currently supported for data in the dataframes:: current: str = "A" charge: str = "mAh" voltage: str = "V" time: str = "sec" resistance: str = "Ohms" power: str = "W" energy: str = "Wh" frequency: str = "hz" And here are the different attributes currently supported for meta-data:: # output-units for specific capacity etc. specific_gravimetric: str = "g" specific_areal: str = "cm**2" # used for calculating specific capacity etc. specific_volumetric: str = "cm**3" # used for calculating specific capacity etc. # other meta-data nominal_capacity: str = "mAh/g" # used for calculating rates etc. mass: str = "mg" length: str = "cm" area: str = "cm**2" volume: str = "cm**3" temperature: str = "C" """
[docs] current: str = "A"
[docs] charge: str = "mAh"
[docs] voltage: str = "V"
[docs] time: str = "sec"
[docs] resistance: str = "ohm"
[docs] power: str = "W"
[docs] energy: str = "Wh"
[docs] frequency: str = "hz"
[docs] mass: str = "mg" # for mass
[docs] nominal_capacity: str = "mAh/g"
[docs] specific_gravimetric: str = "g" # g in specific capacity etc
[docs] specific_areal: str = "cm**2" # m2 in specific capacity etc
[docs] specific_volumetric: str = "cm**3" # m3 in specific capacity etc
[docs] length: str = "cm"
[docs] area: str = "cm**2"
[docs] volume: str = "cm**3"
[docs] temperature: str = "C"
[docs] pressure: str = "bar"
[docs] def update(self, new_units: dict): """Update the units.""" logging.debug(f"{new_units=}") for k in new_units: if k in self.keys(): self[k] = new_units[k]
@dataclass
[docs] class CellpyLimits(BaseSettings): """These are the limits used inside ``cellpy`` for finding step types. Since all instruments have an inherent inaccuracy, it is naive to assume that for example the voltage within a constant voltage step does not change at all. Therefore, we need to define some limits for what we consider to be a constant and what we assume to be zero. """
[docs] current_hard: float = 1e-13
[docs] current_soft: float = 1e-05
[docs] stable_current_hard: float = 2.0
[docs] stable_current_soft: float = 4.0
[docs] stable_voltage_hard: float = 2.0
[docs] stable_voltage_soft: float = 4.0
[docs] stable_charge_hard: float = 0.9
[docs] stable_charge_soft: float = 5.0
[docs] ir_change: float = 1e-05
@dataclass
[docs] class HeadersNormal(BaseHeaders): """Headers used for the normal (raw) data (used as column headers for the main data pandas DataFrames)"""
[docs] aci_phase_angle_txt: str = "aci_phase_angle"
[docs] ref_aci_phase_angle_txt: str = "ref_aci_phase_angle"
[docs] ac_impedance_txt: str = "ac_impedance"
[docs] ref_ac_impedance_txt: str = "ref_ac_impedance"
[docs] charge_capacity_txt: str = "charge_capacity"
[docs] charge_energy_txt: str = "charge_energy"
[docs] current_txt: str = "current"
[docs] cycle_index_txt: str = "cycle_index"
[docs] data_point_txt: str = "data_point"
[docs] datetime_txt: str = "date_time"
[docs] discharge_capacity_txt: str = "discharge_capacity"
[docs] discharge_energy_txt: str = "discharge_energy"
[docs] internal_resistance_txt: str = "internal_resistance"
[docs] power_txt: str = "power"
[docs] is_fc_data_txt: str = "is_fc_data"
[docs] step_index_txt: str = "step_index"
[docs] sub_step_index_txt: str = "sub_step_index"
[docs] step_time_txt: str = "step_time"
[docs] sub_step_time_txt: str = "sub_step_time"
[docs] test_id_txt: str = "test_id"
[docs] test_time_txt: str = "test_time"
[docs] voltage_txt: str = "voltage"
[docs] ref_voltage_txt: str = "reference_voltage"
[docs] dv_dt_txt: str = "dv_dt"
[docs] frequency_txt: str = "frequency"
[docs] amplitude_txt: str = "amplitude"
[docs] channel_id_txt: str = "channel_id"
[docs] data_flag_txt: str = "data_flag"
[docs] test_name_txt: str = "test_name"
@dataclass
[docs] class HeadersSummary(BaseHeaders): """Headers used for the summary data (used as column headers for the main data pandas DataFrames) In addition to the headers defined here, the summary might also contain specific headers (ending in _gravimetric or _areal). """
[docs] postfixes = ["gravimetric", "areal"]
[docs] cycle_index: str = "cycle_index"
[docs] data_point: str = "data_point"
[docs] test_time: str = "test_time"
[docs] datetime: str = "date_time"
[docs] discharge_capacity_raw: str = "discharge_capacity"
[docs] charge_capacity_raw: str = "charge_capacity"
[docs] test_name: str = "test_name"
[docs] data_flag: str = "data_flag"
[docs] channel_id: str = "channel_id"
[docs] coulombic_efficiency: str = "coulombic_efficiency"
[docs] cumulated_coulombic_efficiency: str = "cumulated_coulombic_efficiency"
[docs] discharge_capacity: str = "discharge_capacity"
[docs] charge_capacity: str = "charge_capacity"
[docs] cumulated_charge_capacity: str = "cumulated_charge_capacity"
[docs] cumulated_discharge_capacity: str = "cumulated_discharge_capacity"
[docs] coulombic_difference: str = "coulombic_difference"
[docs] cumulated_coulombic_difference: str = "cumulated_coulombic_difference"
[docs] discharge_capacity_loss: str = "discharge_capacity_loss"
[docs] charge_capacity_loss: str = "charge_capacity_loss"
[docs] cumulated_discharge_capacity_loss: str = "cumulated_discharge_capacity_loss"
[docs] cumulated_charge_capacity_loss: str = "cumulated_charge_capacity_loss"
[docs] normalized_charge_capacity: str = "normalized_charge_capacity"
[docs] normalized_discharge_capacity: str = "normalized_discharge_capacity"
[docs] shifted_charge_capacity: str = "shifted_charge_capacity"
[docs] shifted_discharge_capacity: str = "shifted_discharge_capacity"
[docs] ir_discharge: str = "ir_discharge"
[docs] ir_charge: str = "ir_charge"
[docs] ocv_first_min: str = "ocv_first_min"
[docs] ocv_second_min: str = "ocv_second_min"
[docs] ocv_first_max: str = "ocv_first_max"
[docs] ocv_second_max: str = "ocv_second_max"
[docs] end_voltage_discharge: str = "end_voltage_discharge"
[docs] end_voltage_charge: str = "end_voltage_charge"
[docs] cumulated_ric_disconnect: str = "cumulated_ric_disconnect"
[docs] cumulated_ric_sei: str = "cumulated_ric_sei"
[docs] cumulated_ric: str = "cumulated_ric"
[docs] normalized_cycle_index: str = "normalized_cycle_index"
[docs] low_level: str = "low_level"
[docs] high_level: str = "high_level"
[docs] temperature_last: str = "temperature_last"
[docs] temperature_mean: str = "temperature_mean"
[docs] charge_c_rate: str = "charge_c_rate"
[docs] discharge_c_rate: str = "discharge_c_rate"
[docs] pre_aux: str = "aux_"
@property
[docs] def areal_charge_capacity(self) -> str: warnings.warn( "using old-type look-up (areal_charge_capacity) -> will be deprecated soon", DeprecationWarning, stacklevel=2, ) return f"{self.charge_capacity}_areal"
@property
[docs] def areal_discharge_capacity(self) -> str: warnings.warn( "using old-type look-up (areal_discharge_capacity) -> will be deprecated soon", DeprecationWarning, stacklevel=2, ) return f"{self.discharge_capacity}_areal"
@property
[docs] def specific_columns(self) -> List[str]: """Returns a list of the columns that can be "specific" (e.g. pr. mass or pr. area) for the summary table.""" return [ self.discharge_capacity, self.charge_capacity, self.cumulated_charge_capacity, self.cumulated_discharge_capacity, self.coulombic_difference, self.cumulated_coulombic_difference, self.discharge_capacity_loss, self.charge_capacity_loss, self.cumulated_discharge_capacity_loss, self.cumulated_charge_capacity_loss, self.shifted_charge_capacity, self.shifted_discharge_capacity, # self.cumulated_ric_disconnect, # self.cumulated_ric_sei, # self.cumulated_ric, # self.normalized_cycle_index, ]
@dataclass
[docs] class HeadersStepTable(BaseHeaders): """Headers used for the steps table (used as column headers for the steps pandas DataFrames)"""
[docs] test: str = "test"
[docs] ustep: str = "ustep"
[docs] cycle: str = "cycle"
[docs] step: str = "step"
[docs] test_time: str = "test_time"
[docs] step_time: str = "step_time"
[docs] sub_step: str = "sub_step"
[docs] type: str = "type"
[docs] sub_type: str = "sub_type"
[docs] info: str = "info"
[docs] voltage: str = "voltage"
[docs] current: str = "current"
[docs] charge: str = "charge"
[docs] discharge: str = "discharge"
[docs] point: str = "point"
[docs] internal_resistance: str = "ir"
[docs] internal_resistance_change: str = "ir_pct_change"
[docs] rate_avr: str = "rate_avr"
@dataclass
[docs] class HeadersJournal(BaseHeaders): """Headers used for the journal (batch) (used as column headers for the journal pandas DataFrames)"""
[docs] filename: str = "filename"
[docs] mass: str = "mass"
[docs] total_mass: str = "total_mass"
[docs] loading: str = "loading"
[docs] area: str = "area"
[docs] nom_cap: str = "nom_cap"
[docs] experiment: str = "experiment"
[docs] fixed: str = "fixed"
[docs] label: str = "label"
[docs] cell_type: str = "cell_type"
[docs] instrument: str = "instrument"
[docs] raw_file_names: str = "raw_file_names"
[docs] cellpy_file_name: str = "cellpy_file_name"
[docs] group: str = "group"
[docs] sub_group: str = "sub_group"
[docs] comment: str = "comment"
[docs] argument: str = "argument"
[docs] keys_journal_session = ["starred", "bad_cells", "bad_cycles", "notes"]
[docs] headers_step_table = HeadersStepTable()
[docs] headers_journal = HeadersJournal()
[docs] headers_summary = HeadersSummary()
[docs] headers_normal = HeadersNormal()
[docs] cellpy_units = CellpyUnits()
[docs] base_columns_float = [ headers_normal.test_time_txt, headers_normal.step_time_txt, headers_normal.current_txt, headers_normal.voltage_txt, headers_normal.ref_voltage_txt, headers_normal.charge_capacity_txt, headers_normal.discharge_capacity_txt, headers_normal.internal_resistance_txt, ]
[docs] base_columns_int = [ headers_normal.data_point_txt, headers_normal.step_index_txt, headers_normal.cycle_index_txt, ]
[docs] def get_cellpy_units(*args, **kwargs) -> CellpyUnits: """Returns an augmented global dictionary with units""" return cellpy_units
[docs] def get_default_output_units(*args, **kwargs) -> CellpyUnits: """Returns an augmented dictionary with units to use as default.""" return CellpyUnits()
[docs] def get_default_cellpy_file_raw_units(*args, **kwargs) -> CellpyUnits: """Returns a dictionary with units to use as default for old versions of cellpy files""" return CellpyUnits( charge="Ah", mass="mg", )
[docs] def get_default_raw_units(*args, **kwargs) -> CellpyUnits: """Returns a dictionary with units as default for raw data""" return CellpyUnits( charge="Ah", mass="mg", )
[docs] def get_default_raw_limits() -> CellpyLimits: """Returns an augmented dictionary with units as default for raw data""" return CellpyLimits()
[docs] def get_headers_normal() -> HeadersNormal: """Returns an augmented global dictionary containing the header-strings for the normal data (used as column headers for the main data pandas DataFrames)""" return headers_normal
[docs] def get_headers_step_table() -> HeadersStepTable: """Returns an augmented global dictionary containing the header-strings for the steps table (used as column headers for the steps pandas DataFrames)""" return headers_step_table
[docs] def get_headers_journal() -> HeadersJournal: """Returns an augmented global dictionary containing the header-strings for the journal (batch) (used as column headers for the journal pandas DataFrames)""" return headers_journal
[docs] def get_headers_summary() -> HeadersSummary: """Returns an augmented global dictionary containing the header-strings for the summary (used as column headers for the summary pandas DataFrames)""" return headers_summary
[docs] def get_default_custom_headers_summary() -> HeadersSummary: """Returns an augmented dictionary that can be used to create custom header-strings for the summary (used as column headers for the summary pandas DataFrames) This function is mainly implemented to provide an example. """ # maybe I can do some tricks in here so that tab completion works in pycharm? # solution: ctrl + space works return HeadersSummary()