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
CELLPY_FILE_VERSION = 8
MINIMUM_CELLPY_FILE_VERSION = 4
STEP_TABLE_VERSION = 5
RAW_TABLE_VERSION = 5
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:
PICKLE_PROTOCOL = 4
# For creating the sqlite database from Excel:
TABLE_NAME_SQLITE = "cells"
COLUMNS_EXCEL_PK = "id"
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",
}
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",
]
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",
]
OTHERPATHS = ["rawdatadir", "cellpydatadir"]
[docs]@dataclass
class CellpyMeta:
[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
[docs]@dataclass
class CellpyMetaCommon(CellpyMeta):
# about test
cell_name: Optional[str] = None # used as property
start_datetime: Optional[str] = None
time_zone: Optional[str] = None
comment: Optional[prms.CellPyDataConfig] = prms.CellInfo.comment
file_errors: Optional[str] = None # not in use at the moment
raw_id: Optional[str] = None # used as property
cellpy_file_version: int = CELLPY_FILE_VERSION
# about tester
tester_ID: Optional[prms.CellPyDataConfig] = None
tester_server_software_version: Optional[prms.CellPyDataConfig] = None
tester_client_software_version: Optional[prms.CellPyDataConfig] = None
tester_calibration_date: Optional[prms.CellPyDataConfig] = None
# about cell
material: Optional[prms.CellPyDataConfig] = prms.Materials.default_material
# TODO @jepe: Maybe we should use values with units here instead (pint)?
mass: Optional[
prms.CellPyDataConfig
] = prms.Materials.default_mass # active material
tot_mass: Optional[
prms.CellPyDataConfig
] = prms.Materials.default_mass # total material
nom_cap: Optional[
prms.CellPyDataConfig
] = prms.Materials.default_nom_cap # nominal capacity # used as property
nom_cap_specifics: Optional[
prms.CellPyDataConfig
] = (
prms.Materials.default_nom_cap_specifics
) # nominal capacity type # used as property
active_electrode_area: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.active_electrode_area
active_electrode_thickness: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.active_electrode_thickness
electrolyte_volume: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.electrolyte_volume
electrolyte_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.electrolyte_type
active_electrode_type: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.active_electrode_type
counter_electrode_type: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.counter_electrode_type
reference_electrode_type: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.reference_electrode_type
experiment_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.experiment_type
cell_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.cell_type
separator_type: Optional[prms.CellPyDataConfig] = prms.CellInfo.separator_type
active_electrode_current_collector: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.active_electrode_current_collector
reference_electrode_current_collector: Optional[
prms.CellPyDataConfig
] = prms.CellInfo.reference_electrode_current_collector
[docs]@dataclass
class CellpyMetaIndividualTest(CellpyMeta):
# ---------------- test dependent -------------------------------
channel_index: Optional[prms.CellPyDataConfig] = None
creator: Optional[str] = None
schedule_file_name = None
test_type: Optional[
prms.CellPyDataConfig
] = None # Not used (and might be put inside test_ID)
voltage_lim_low: Optional[prms.CellPyDataConfig] = prms.CellInfo.voltage_lim_low
voltage_lim_high: Optional[prms.CellPyDataConfig] = prms.CellInfo.voltage_lim_high
cycle_mode: Optional[prms.CellPyDataConfig] = prms.Reader.cycle_mode
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):
"""Sub-classing 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
[docs]@dataclass
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]@dataclass
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
[docs]@dataclass
class BaseHeaders(BaseSettings):
"""Extending BaseSetting so that it's allowed to add postfixes.
Example:
>>> header["key_postfix"] # returns "value_postfix"
"""
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}")
[docs]@dataclass
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.
"""
...
[docs]@dataclass
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"
"""
current: str = "A"
charge: str = "mAh"
voltage: str = "V"
time: str = "sec"
resistance: str = "ohm"
power: str = "W"
energy: str = "Wh"
frequency: str = "hz"
mass: str = "mg" # for mass
nominal_capacity: str = "mAh/g"
specific_gravimetric: str = "g" # g in specific capacity etc
specific_areal: str = "cm**2" # m2 in specific capacity etc
specific_volumetric: str = "cm**3" # m3 in specific capacity etc
length: str = "cm"
area: str = "cm**2"
volume: str = "cm**3"
temperature: str = "C"
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]
[docs]@dataclass
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.
"""
current_hard: float = 1e-13
current_soft: float = 1e-05
stable_current_hard: float = 2.0
stable_current_soft: float = 4.0
stable_voltage_hard: float = 2.0
stable_voltage_soft: float = 4.0
stable_charge_hard: float = 0.9
stable_charge_soft: float = 5.0
ir_change: float = 1e-05
[docs]@dataclass
class HeadersNormal(BaseHeaders):
aci_phase_angle_txt: str = "aci_phase_angle"
ref_aci_phase_angle_txt: str = "ref_aci_phase_angle"
ac_impedance_txt: str = "ac_impedance"
ref_ac_impedance_txt: str = "ref_ac_impedance"
charge_capacity_txt: str = "charge_capacity"
charge_energy_txt: str = "charge_energy"
current_txt: str = "current"
cycle_index_txt: str = "cycle_index"
data_point_txt: str = "data_point"
datetime_txt: str = "date_time"
discharge_capacity_txt: str = "discharge_capacity"
discharge_energy_txt: str = "discharge_energy"
internal_resistance_txt: str = "internal_resistance"
power_txt: str = "power"
is_fc_data_txt: str = "is_fc_data"
step_index_txt: str = "step_index"
sub_step_index_txt: str = "sub_step_index"
step_time_txt: str = "step_time"
sub_step_time_txt: str = "sub_step_time"
test_id_txt: str = "test_id"
test_time_txt: str = "test_time"
voltage_txt: str = "voltage"
ref_voltage_txt: str = "reference_voltage"
dv_dt_txt: str = "dv_dt"
frequency_txt: str = "frequency"
amplitude_txt: str = "amplitude"
channel_id_txt: str = "channel_id"
data_flag_txt: str = "data_flag"
test_name_txt: str = "test_name"
[docs]@dataclass
class HeadersSummary(BaseHeaders):
"""In addition to the headers defined here, the summary might also contain
specific headers (ending in _gravimetric or _areal).
"""
postfixes = ["gravimetric", "areal"]
cycle_index: str = "cycle_index"
data_point: str = "data_point"
test_time: str = "test_time"
datetime: str = "date_time"
discharge_capacity_raw: str = "discharge_capacity"
charge_capacity_raw: str = "charge_capacity"
test_name: str = "test_name"
data_flag: str = "data_flag"
channel_id: str = "channel_id"
coulombic_efficiency: str = "coulombic_efficiency"
cumulated_coulombic_efficiency: str = "cumulated_coulombic_efficiency"
discharge_capacity: str = "discharge_capacity"
charge_capacity: str = "charge_capacity"
cumulated_charge_capacity: str = "cumulated_charge_capacity"
cumulated_discharge_capacity: str = "cumulated_discharge_capacity"
coulombic_difference: str = "coulombic_difference"
cumulated_coulombic_difference: str = "cumulated_coulombic_difference"
discharge_capacity_loss: str = "discharge_capacity_loss"
charge_capacity_loss: str = "charge_capacity_loss"
cumulated_discharge_capacity_loss: str = "cumulated_discharge_capacity_loss"
cumulated_charge_capacity_loss: str = "cumulated_charge_capacity_loss"
normalized_charge_capacity: str = "normalized_charge_capacity"
normalized_discharge_capacity: str = "normalized_discharge_capacity"
shifted_charge_capacity: str = "shifted_charge_capacity"
shifted_discharge_capacity: str = "shifted_discharge_capacity"
ir_discharge: str = "ir_discharge"
ir_charge: str = "ir_charge"
ocv_first_min: str = "ocv_first_min"
ocv_second_min: str = "ocv_second_min"
ocv_first_max: str = "ocv_first_max"
ocv_second_max: str = "ocv_second_max"
end_voltage_discharge: str = "end_voltage_discharge"
end_voltage_charge: str = "end_voltage_charge"
cumulated_ric_disconnect: str = "cumulated_ric_disconnect"
cumulated_ric_sei: str = "cumulated_ric_sei"
cumulated_ric: str = "cumulated_ric"
normalized_cycle_index: str = "normalized_cycle_index"
low_level: str = "low_level"
high_level: str = "high_level"
temperature_last: str = "temperature_last"
temperature_mean: str = "temperature_mean"
charge_c_rate: str = "charge_c_rate"
discharge_c_rate: str = "discharge_c_rate"
pre_aux: str = "aux_"
@property
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
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
def specific_columns(self) -> List[str]:
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,
]
[docs]@dataclass
class HeadersStepTable(BaseHeaders):
test: str = "test"
ustep: str = "ustep"
cycle: str = "cycle"
step: str = "step"
test_time: str = "test_time"
step_time: str = "step_time"
sub_step: str = "sub_step"
type: str = "type"
sub_type: str = "sub_type"
info: str = "info"
voltage: str = "voltage"
current: str = "current"
charge: str = "charge"
discharge: str = "discharge"
point: str = "point"
internal_resistance: str = "ir"
internal_resistance_change: str = "ir_pct_change"
rate_avr: str = "rate_avr"
[docs]@dataclass
class HeadersJournal(BaseHeaders):
filename: str = "filename"
mass: str = "mass"
total_mass: str = "total_mass"
loading: str = "loading"
area: str = "area"
nom_cap: str = "nom_cap"
experiment: str = "experiment"
fixed: str = "fixed"
label: str = "label"
cell_type: str = "cell_type"
instrument: str = "instrument"
raw_file_names: str = "raw_file_names"
cellpy_file_name: str = "cellpy_file_name"
group: str = "group"
sub_group: str = "sub_group"
comment: str = "comment"
argument: str = "argument"
keys_journal_session = ["starred", "bad_cells", "bad_cycles", "notes"]
headers_step_table = HeadersStepTable()
headers_journal = HeadersJournal()
headers_summary = HeadersSummary()
headers_normal = HeadersNormal()
cellpy_units = CellpyUnits()
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,
]
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()