class DeprecatedFeature(Error):
    """Raised when the feature is recently deprecated"""

    pass class NullData(Error):
    """Raised when required data is missing (e.g. voltage = None or summary_frames are missing)"""

    pass class WrongFileVersion(Error):
    """Raised when the file version is wrong"""

    pass class CellPyConfig:
class PathsClass(CellPyConfig):
class FileNamesClass(CellPyConfig):
class ReaderClass(CellPyConfig):
class DataSetClass(CellPyConfig):
class DbClass(CellPyConfig):
class DbColsClass(CellPyConfig):
class MaterialsClass(CellPyConfig):
class BatchClass(CellPyConfig):
class InstrumentsClass(CellPyConfig): class HeaderDict(UserDict):
class DictLikeClass:
class BaseSettings(DictLikeClass):
class InstrumentSettings(DictLikeClass):
class CellpyUnits(BaseSettings):
class CellpyLimits(BaseSettings):
class HeadersNormal(BaseSettings):
class HeadersSummary(BaseSettings):
class HeadersStepTable(BaseSettings):
class HeadersJournal(BaseSettings):
    def __setitem__(self, key: str, value: str) -> None:
    def __getitem__(self, key):
    def __setitem__(self, key, value):
    def __missing__(self, key):
    def _field_names(self):
    def __iter__(self):
    def _value_iter(self):
    def keys(self):
    def values(self):
    def items(self):
def get_headers_summary() -> BaseSettings:
def get_cellpy_units() -> CellpyUnits:
def get_headers_normal() -> HeadersNormal:
def get_headers_step_table() -> HeadersStepTable:
def get_headers_journal() -> HeadersJournal:
ATTRS_CELLPYFILE = [
    "mass",
    "channel_index",
    "channel_number",
    "creator",
    "cycle_mode",
    "schedule_file_name",
    "start_datetime",
    "test_ID",
    "cell_no",
    "name",
    "nom_cap",
    "material",
    "item_ID",
    "active_electrode_area",
    "active_electrode_thickness",
    "electrolyte_type",
    "electrolyte_volume",
    "active_electrode_type",
    "counter_electrode_type",
    "reference_electrode_type",
    "experiment_type",
    "active_electrode_current_collector",
    "reference_electrode_current_collector",
    "comment",
]
ATTRS_CELLPYDATA = [
    "auto_dirs",
    "capacity_modifiers",
    "cellpy_datadir",
    "daniel_number",
    "ensure_step_table",
    "file_names",
    "filestatuschecker",
    "force_all",
    "force_step_table_creation",
    "forced_errors",
    "limit_loaded_cycles",
    "load_only_summary",
    "minimum_selection",
    "name",
    "number_of_datasets",
    "profile",
    "raw_datadir",
    "raw_limits",
    "raw_units",
    "select_minimal",
    "selected_cell_number",
    "selected_scans",
    "sep",
    "status_datasets",
    "summary_exists",
    "table_names",
    "tester",
]
ATTRS_DATASET = [
    "cellpy_file_version",
    "channel_index",
    "channel_number",
    "charge_steps",
    "creator",
    "cycle_mode",
    # "data",
    "discharge_steps",
    "file_errors",
    "ir_steps",
    "item_ID",
    "loaded_from",
    "mass",
    "mass_given",
    "material",
    "merged",
    "name",
    "no_cycles",
    "nom_cap",
    "ocv_steps",
    "raw_data_files_length",
    "raw_limits",
    "raw_units",
    "schedule_file_name",
    "start_datetime",
    # "summary",
    "test_ID",
    "cell_no",
    "tot_mass",
]
ATTRS_DATASET_DEEP = ["raw_data_files"] HEADERS_NORMAL = get_headers_normal()
HEADERS_SUMMARY = get_headers_summary()
HEADERS_STEP_TABLE = get_headers_step_table()
HEADERS_KEYS_STEP_TABLE_EXTENDED = [
    "point",
    "test_time",
    "step_time",
    "current",
    "voltage",
    "charge",
    "discharge",
    "internal_resistance",
]
HEADERS_STEP_TABLE_EXTENSIONS = ["min", "max", "avr", "first", "last", "delta", "std"]
def translate_headers(data_sets, cellpy_file_version):
def check(data_sets): CELLPY_FILE_VERSION = 6 MINIMUM_CELLPY_FILE_VERSION = 4 PICKLE_PROTOCOL = 4 class Cell(object):
    """Object to store data for a test.

    This class is used for storing all the relevant data for a 'run', i.e. all
    the data collected by the tester as stored in the raw-files.

    Attributes:
        test_no (int): test number.
        mass (float): mass of electrode [mg].
        dfdata (pandas.DataFrame): contains the experimental data points.
        dfsummary (pandas.DataFrame): contains summary of the data pr. cycle.
        step_table (pandas.DataFrame): information for each step, used for
            defining type of step (charge, discharge, etc.)

    """

    def _repr_html_(self):
        obj = f"<b>Cell-object</b> id={hex(id(self))}"
        txt = "<p>"
        for p in dir(self):
            if not p.startswith("_"):
                if p not in ["raw", "summary", "steps", "logger"]:
                    value = self.__getattribute__(p)
                    txt += f"<b>{p}</b>: {value}<br>"
        txt += "</p>"
        try:
            raw_txt = f"<p><b>raw data-frame (summary)</b><br>{self.raw.describe()._repr_html_()}</p>"
            raw_txt += f"<p><b>raw data-frame (head)</b><br>{self.raw.head()._repr_html_()}</p>"
        except AttributeError:
            raw_txt = "<p><b>raw data-frame </b><br> not found!</p>"
        except ValueError:
            raw_txt = "<p><b>raw data-frame </b><br> does not contain any columns!</p>"

        try:
            summary_txt = f"<p><b>summary data-frame (summary)</b><br>{self.summary.describe()._repr_html_()}</p>"
            summary_txt += f"<p><b>summary data-frame (head)</b><br>{self.summary.head()._repr_html_()}</p>"
        except AttributeError:
            summary_txt = "<p><b>summary data-frame </b><br> not found!</p>"
        except ValueError:
            summary_txt = (
                "<p><b>summary data-frame </b><br> does not contain any columns!</p>"
            )

        try:
            steps_txt = f"<p><b>steps data-frame (summary)</b><br>{self.steps.describe()._repr_html_()}</p>"
            steps_txt += f"<p><b>steps data-frame (head)</b><br>{self.steps.head()._repr_html_()}</p>"
        except AttributeError:
            steps_txt = "<p><b>steps data-frame </b><br> not found!</p>"
        except ValueError:
            steps_txt = (
                "<p><b>steps data-frame </b><br> does not contain any columns!</p>"
            )

        return obj + txt + summary_txt + steps_txt + raw_txt

    def __init__(self, **kwargs):
        self.logger = logging.getLogger(__name__)
        self.logger.debug("created DataSet instance")

        # meta-data
        self.cell_no = None
        self.mass = prms.Materials.default_mass  # active material (in mg)
        self.tot_mass = prms.Materials.default_mass  # total material (in mg)
        self.no_cycles = 0.0
        self.charge_steps = None
        self.discharge_steps = None
        self.ir_steps = None
        self.ocv_steps = None
        self.nom_cap = prms.DataSet.nom_cap  # mAh/g (for finding c-rates)
        self.mass_given = False
        self.material = prms.Materials.default_material
        self.merged = False
        self.file_errors = None  # not in use at the moment
        self.loaded_from = None  # loaded from (can be list if merged)
        self.channel_index = None
        self.channel_number = None
        self.creator = None
        self.item_ID = None
        self.schedule_file_name = None
        self.start_datetime = None
        self.test_ID = None
        self.name = None

        # new meta data
        self.cycle_mode = prms.Reader.cycle_mode
        self.active_electrode_area = None  # [cm2]
        self.active_electrode_thickness = None  # [micron]
        self.electrolyte_type = None  #
        self.electrolyte_volume = None  # [micro-liter]
        self.active_electrode_type = None
        self.counter_electrode_type = None
        self.reference_electrode_type = None
        self.experiment_type = None
        self.cell_type = None
        self.separator_type = None
        self.active_electrode_current_collector = None
        self.reference_electrode_current_collector = None
        self.comment = None

        # custom meta-data
        for k in kwargs:
            if hasattr(self, k):
                setattr(self, k, kwargs[k])

        # methods in CellpyData to update if adding new attributes:
        # ATTRS_CELLPYFILE

        # place to put "checks" etc:
        # _extract_meta_from_cellpy_file
        # _create_infotable()

        self.raw_data_files = []
        self.raw_data_files_length = []
        self.raw_units = cellpy_units
        self.raw_limits = cellpy_limits

        # self.data = collections.OrderedDict()  # not used
        # self.summary = collections.OrderedDict()  # not used

        self.raw = pd.DataFrame()
        self.summary = pd.DataFrame()
        # self.summary_made = False  # Should be removed
        self.steps = pd.DataFrame()  # is this used? - check!
        # self.step_table_made = False  # Should be removed
        # self.parameter_table = collections.OrderedDict()
        self.summary_table_version = SUMMARY_TABLE_VERSION
        self.step_table_version = STEP_TABLE_VERSION
        self.cellpy_file_version = CELLPY_FILE_VERSION
        self.raw_table_version = RAW_TABLE_VERSION

    @staticmethod
    def _header_str(hdr):
        txt = "\n"
        txt += 80 * "-" + "\n"
        txt += f" {hdr} ".center(80) + "\n"
        txt += 80 * "-" + "\n"
        return txt

    def __str__(self):
        txt = "<DataSet>\n"
        txt += "loaded from file\n"
        if isinstance(self.loaded_from, (list, tuple)):
            for f in self.loaded_from:
                txt += str(f)
                txt += "\n"

        else:
            txt += str(self.loaded_from)
            txt += "\n"
        txt += "\n* GLOBAL\n"
        txt += f"material:            {self.material}\n"
        txt += f"mass (active):       {self.mass}\n"
        txt += f"test ID:             {self.test_ID}\n"
        txt += f"mass (total):        {self.tot_mass}\n"
        txt += f"nominal capacity:    {self.nom_cap}\n"
        txt += f"channel index:       {self.channel_index}\n"
        txt += f"DataSet name:        {self.name}\n"
        txt += f"creator:             {self.creator}\n"
        txt += f"schedule file name:  {self.schedule_file_name}\n"

        try:
            if self.start_datetime:
                start_datetime_str = xldate_as_datetime(self.start_datetime)
            else:
                start_datetime_str = "Not given"
        except AttributeError:
            start_datetime_str = "NOT READABLE YET"

        txt += f"start-date:         {start_datetime_str}\n"

        txt += self._header_str("DATA")
        try:
            txt += str(self.raw.describe())
        except (AttributeError, ValueError):
            txt += "EMPTY (Not processed yet)\n"

        txt += self._header_str("SUMMARY")
        try:
            txt += str(self.summary.describe())
        except (AttributeError, ValueError):
            txt += "EMPTY (Not processed yet)\n"

        txt += self._header_str("STEP TABLE")
        try:
            txt += str(self.steps.describe())
            txt += str(self.steps.head())
        except (AttributeError, ValueError):
            txt += "EMPTY (Not processed yet)\n"

        txt += self._header_str("RAW UNITS")
        try:
            txt += str(self.raw.describe())
            txt += str(self.raw.head())
        except (AttributeError, ValueError):
            txt += "EMPTY (Not processed yet)\n"
        return txt

    @property
    def summary_made(self):
        """check if the summary table exists"""
        try:
            empty = self.summary.empty
        except AttributeError:
            empty = True
        return not empty

    @property
    def steps_made(self):
        """check if the step table exists"""
        try:
            empty = self.steps.empty
        except AttributeError:
            empty = True
        return not empty

    @property
    def no_data(self):
        # TODO: @jepe should consider renaming this to be in-line with "steps_made" etc. (or renaming steps_made and
        #   summary_made to e.g. no_steps, no_summary)
        try:
            empty = self.raw.empty
        except AttributeError:
            empty = True
        return empty class FileID(object):
    """class for storing information about the raw-data files.

    This class is used for storing and handling raw-data file information.
    It is important to keep track of when the data was extracted from the
    raw-data files so that it is easy to know if the hdf5-files used for
    @storing "treated" data is up-to-date.

    Attributes:
        name (str): Filename of the raw-data file.
        full_name (str): Filename including path of the raw-data file.
        size (float): Size of the raw-data file.
        last_modified (datetime): Last time of modification of the raw-data
            file.
        last_accessed (datetime): last time of access of the raw-data file.
        last_info_changed (datetime): st_ctime of the raw-data file.
        location (str): Location of the raw-data file.

    """

    def __init__(self, filename=None):
        make_defaults = True
        if filename:
            if os.path.isfile(filename):
                fid_st = os.stat(filename)
                self.name = os.path.abspath(filename)
                self.full_name = filename
                self.size = fid_st.st_size
                self.last_modified = fid_st.st_mtime
                self.last_accessed = fid_st.st_atime
                self.last_info_changed = fid_st.st_ctime
                self.location = os.path.dirname(filename)
                self.last_data_point = 0  # used later when updating is implemented
                make_defaults = False

        if make_defaults:
            self.name = None
            self.full_name = None
            self.size = 0
            self.last_modified = None
            self.last_accessed = None
            self.last_info_changed = None
            self.location = None
            self._last_data_point = 0  # to be used later when updating is implemented

    def __str__(self):
        txt = "\n<fileID>\n"
        txt += f"full name: {self.full_name}\n"
        txt += f"name: {self.name}\n"
        txt += f"location: {self.location}\n"
        if self.last_modified is not None:
            txt += f"modified: {self.last_modified}\n"
        else:
            txt += "modified: NAN\n"
        if self.size is not None:
            txt += f"size: {self.size}\n"
        else:
            txt += "size: NAN\n"

        txt += f"last data point: {self.last_data_point}\n"
        return txt

    @property
    def last_data_point(self):
        # TODO: consider including a method here to find the last data point (raw data)
        # ideally, this value should be set when loading the raw data before
        # merging files (if it consists of several files)
        return self._last_data_point

    @last_data_point.setter
    def last_data_point(self, value):
        self._last_data_point = value

    def populate(self, filename):
        """Finds the file-stats and populates the class with stat values.

        Args:
            filename (str): name of the file.
        """

        if os.path.isfile(filename):
            fid_st = os.stat(filename)
            self.name = os.path.abspath(filename)
            self.full_name = filename
            self.size = fid_st.st_size
            self.last_modified = fid_st.st_mtime
            self.last_accessed = fid_st.st_atime
            self.last_info_changed = fid_st.st_ctime
            self.location = os.path.dirname(filename)

    def get_raw(self):
        """Get a list with information about the file.

        The returned list contains name, size, last_modified and location.
        """
        return [self.name, self.size, self.last_modified, self.location]

    def get_name(self):
        """Get the filename."""
        return self.name

    def get_size(self):
        """Get the size of the file."""
        return self.size

    def get_last(self):
        """Get last modification time of the file."""
        return self.last_modified def identify_last_data_point(data):
    """Find the last data point and store it in the fid instance"""

    logging.debug("searching for last data point")
    hdr_data_point = HEADERS_NORMAL.data_point_txt
    try:
        if hdr_data_point in data.raw.columns:

            last_data_point = data.raw[hdr_data_point].max()
        else:
            last_data_point = data.raw.index.max()
    except AttributeError:
        logging.debug("AttributeError - setting last data point to 0")
        last_data_point = 0
    if not last_data_point > 0:
        last_data_point = 0
    data.raw_data_files[0].last_data_point = last_data_point
    logging.debug(f"last data point: {last_data_point}")
    return data def interpolate_y_on_x(
    df,
    x=None,
    y=None,
    new_x=None,
    dx=10.0,
    number_of_points=None,
    direction=1,
    **kwargs,
):
    """Interpolate a column based on another column.

    Args:
        df: DataFrame with the (cycle) data.
        x: Column name for the x-value (defaults to the step-time column).
        y: Column name for the y-value (defaults to the voltage column).
        new_x (numpy array or None): Interpolate using these new x-values
            instead of generating x-values based on dx or number_of_points.
        dx: step-value (defaults to 10.0)
        number_of_points: number of points for interpolated values (use
            instead of dx and overrides dx if given).
        direction (-1,1): if direction is negative, then invert the
            x-values before interpolating.
        **kwargs: arguments passed to scipy.interpolate.interp1d

    Returns: DataFrame with interpolated y-values based on given or
        generated x-values.

    """

    # TODO: allow for giving a fixed interpolation range (x-values).
    #  Remember to treat extrapolation properly (e.g. replace with NaN?).

    if x is None:
        x = df.columns[0]
    if y is None:
        y = df.columns[1]

    xs = df[x].values
    ys = df[y].values

    if direction > 0:
        x_min = xs.min()
        x_max = xs.max()
    else:
        x_max = xs.min()
        x_min = xs.max()
        dx = -dx

    bounds_error = kwargs.pop("bounds_error", False)
    f = interpolate.interp1d(xs, ys, bounds_error=bounds_error, **kwargs)
    if new_x is None:
        if number_of_points:
            new_x = np.linspace(x_min, x_max, number_of_points)
        else:
            new_x = np.arange(x_min, x_max, dx)

    new_y = f(new_x)

    new_df = pd.DataFrame({x: new_x, y: new_y})

    return new_df def pickle_protocol(level):
    return PickleProtocol(level) def xldate_as_datetime(xldate, datemode=0, option="to_datetime"):
    """Converts a xls date stamp to a more sensible format.

    Args:
        xldate (str): date stamp in Excel format.
        datemode (int): 0 for 1900-based, 1 for 1904-based.
        option (str): option in ("to_datetime", "to_float", "to_string"),
            return value

    Returns:
        datetime (datetime object, float, or string).

    """

    # This does not work for numpy-arrays

    if option == "to_float":
        d = (xldate - 25589) * 86400.0
    else:
        try:
            d = datetime.datetime(1899, 12, 30) + datetime.timedelta(
                days=xldate + 1462 * datemode
            )
            # date_format = "%Y-%m-%d %H:%M:%S:%f" # with microseconds,
            # excel cannot cope with this!
            if option == "to_string":
                date_format = "%Y-%m-%d %H:%M:%S"  # without microseconds
                d = d.strftime(date_format)
        except TypeError:
            logging.info(f"The date is not of correct type [{xldate}]")
            d = xldate
    return d
import collections
import copy
import csv
import itertools
import logging
import os
import sys
import time
import warnings
import numpy as np
import pandas as pd
    import logging
from pathlib import Path, PurePosixPath, PureWindowsPath
from pandas.errors import PerformanceWarning
from scipy import interpolate
from cellpy.exceptions import DeprecatedFeature, NullData, WrongFileVersion
from cellpy.parameters import prms
from cellpy.parameters.internal_settings import (
    ATTRS_CELLPYDATA,
    ATTRS_CELLPYFILE,
    ATTRS_DATASET,
    ATTRS_DATASET_DEEP,
    get_cellpy_units,
    get_headers_normal,
    get_headers_step_table,
    get_headers_summary,
    headers_normal,
    headers_step_table,
    headers_summary,
)
from cellpy.parameters.legacy import internal_settings as old_settings
from cellpy.readers.core import (
    CELLPY_FILE_VERSION,
    MINIMUM_CELLPY_FILE_VERSION,
    PICKLE_PROTOCOL,
    Cell,
    FileID,
    identify_last_data_point,
    interpolate_y_on_x,
    pickle_protocol,
    xldate_as_datetime,
)
                from cellpy.readers.instruments.local_instrument import (
                    LocalTxtLoader as RawLoader,
                )
            from cellpy.readers.instruments.arbin_res import ArbinLoader as RawLoader
            from cellpy.readers.instruments.arbin_sql import ArbinSQLLoader as RawLoader
            from cellpy.readers.instruments.arbin_sql_csv import (
                ArbinCsvLoader as RawLoader,
            )
            from cellpy.readers.instruments.pec import PECLoader as RawLoader
            from cellpy.readers.instruments.biologics_mpr import MprLoader as RawLoader
            from cellpy.readers.instruments.maccor_txt import (
                MaccorTxtLoader as RawLoader,
            )
            from cellpy.readers.instruments.custom import CustomLoader as RawLoader
        from cellpy import log
    from cellpy import log
    from cellpy.utils import example_data
    from cellpy import log
    from cellpy.utils import example_data
        dataset_number = self._validate_dataset_number(dataset_number)
        if dataset_number is None:
            self._report_empty_dataset()
            return

        # if short:
        #     # the table only consists of steps (not cycle,step pairs) assuming
        #     # that the step numbers uniquely defines step type (this is true
        #     # for arbin at least).
        #     raise NotImplementedError

        step_specs = pd.read_csv(file_name, sep=prms.Reader.sep)
        if "step" not in step_specs.columns:
            logging.info("Missing column: step")
            raise IOError

        if "type" not in step_specs.columns:
            logging.info("Missing column: type")
            raise IOError

        if not short and "cycle" not in step_specs.columns:
            logging.info("Missing column: cycle")
            raise IOError

        self.make_step_table(step_specifications=step_specs, short=short)

    def _sort_data(self, dataset):
        # TODO: [# index]
        if self.headers_normal.data_point_txt in dataset.raw.columns:
            dataset.raw = dataset.raw.sort_values(
                self.headers_normal.data_point_txt
            ).reset_index()
            return dataset

        logging.debug("_sort_data: no datapoint header to sort by")

    def _ustep(self, n):
        un = []
        c = 0
        n = n.diff()
        for i in n:
            if i != 0:
                c += 1
            un.append(c)
        logging.debug("created u-steps")
        return un

    def make_step_table(
        self,
        step_specifications=None,
        short=False,
        profiling=False,
        all_steps=False,
        add_c_rate=True,
        skip_steps=None,
        sort_rows=True,
        dataset_number=None,
        from_data_point=None,
    ):

        """Create a table (v.4) that contains summary information for each step.

        This function creates a table containing information about the
        different steps for each cycle and, based on that, decides what type of
        step it is (e.g. charge) for each cycle.

        The format of the steps is:

            index: cycleno - stepno - sub-step-no - ustep
            Time info (average, stdev, max, min, start, end, delta) -
            Logging info (average, stdev, max, min, start, end, delta) -
            Current info (average, stdev, max, min, start, end, delta) -
            Voltage info (average,  stdev, max, min, start, end, delta) -
            Type (from pre-defined list) - SubType -
            Info

         Args:
            step_specifications (pandas.DataFrame): step specifications
            short (bool): step specifications in short format
            profiling (bool): turn on profiling

            all_steps (bool): investigate all steps including same steps within
                one cycle (this is useful for e.g. GITT).
            add_c_rate (bool): include a C-rate estimate in the steps
            skip_steps (list of integers): list of step numbers that should not
                be processed (future feature - not used yet).
            sort_rows (bool): sort the rows after processing.
            dataset_number: defaults to self.dataset_number
            from_data_point (int): first data point to use

        Returns:
            None
        """
        # TODO: @jepe - include option for omitting steps
        # TODO: @jepe  - make it is possible to update only new data

        time_00 = time.time()
        dataset_number = self._validate_dataset_number(dataset_number)
        if dataset_number is None:
            self._report_empty_dataset()
            return

        if profiling:
            print("PROFILING MAKE_STEP_TABLE".center(80, "="))

        def first(x):
            return x.iloc[0]

        def last(x):
            return x.iloc[-1]

        def delta(x):
            if x.iloc[0] == 0.0:
                # starts from a zero value
                difference = 100.0 * x.iloc[-1]
            else:
                difference = (x.iloc[-1] - x.iloc[0]) * 100 / abs(x.iloc[0])

            return difference


        nhdr = self.headers_normal