diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e7f35e8d..87384e51 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,12 @@
name: "Build and test"
on:
pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
workflow_dispatch:
jobs:
qa:
- if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
diff --git a/docs/source/user_guide/input_data_guidance.rst b/docs/source/user_guide/input_data_guidance.rst
index ae14e1e0..bc79dd82 100644
--- a/docs/source/user_guide/input_data_guidance.rst
+++ b/docs/source/user_guide/input_data_guidance.rst
@@ -16,6 +16,19 @@ PyProBE is able to import data from the following cyclers:
- .mpt
- .txt
+* Arbin: :code:`'arbin'`
+
+ - .csv
+ - .xlsx
+
+* Maccor: :code:`'maccor'`
+
+ - .csv
+
+* Basytec: :code:`'basytec'`
+
+ - .txt
+
PyProBE data columns
--------------------
@@ -42,34 +55,83 @@ Once converted into the standard PyProBE format, the data columns stored in
current is passed and decreases when discharge current is passed.
The table below summarises the data columns in the PyProBE format and the corresponding
-column names of of data from supported cyclers:
+column names that are required in data from supported cyclers:
+
+.. raw:: html
+
+
+
+
.. table::
- :widths: 20 20 20 20
-
- +----------------+-----------+------------------------+-----------------------------+
- | PyProBE | Required? | Neware | BioLogic |
- +================+===========+========================+=============================+
- | Date | No | Date | "Acquisition started on" |
- | | | | in header |
- +----------------+-----------+------------------------+-----------------------------+
- | Time [s] | Yes | *Auto from Date* | time/* |
- +----------------+-----------+------------------------+-----------------------------+
- | Step | Yes | Step Index | Ns |
- +----------------+-----------+------------------------+-----------------------------+
- | Cycle | Yes | *Auto from Step* | *Auto from Step* |
- | | | | |
- +----------------+-----------+------------------------+-----------------------------+
- | Event | Yes | *Auto from Step* | *Auto from Step* |
- | | | | |
- +----------------+-----------+------------------------+-----------------------------+
- | Current [A] | Yes | Current(*) | I/* |
- +----------------+-----------+------------------------+-----------------------------+
- | Voltage [V] | Yes | Voltage(*) | Ecell/* |
- +----------------+-----------+------------------------+-----------------------------+
- | Capacity [Ah] | Yes | Chg. Cap.(*), | Q charge/\*, |
- | | | DChg. Cap.(*) | Q discharge/* |
- +----------------+-----------+------------------------+-----------------------------+
+ :widths: 20 20 20 20 20 20 20
+ :class: scrollable-table
+
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | PyProBE | Required? | Neware | BioLogic | Arbin | Maccor | Basytec |
+ +======================+===========+========================+=============================+=============================+=============================+=============================+
+ | ``Date`` | No | ``Date`` | ``Acquisition started on`` | ``Date Time`` | ``DPT Time`` | ``~Start of Test`` |
+ | | | | in header | | | in header |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Time [s]`` | Yes | *Auto from Date* | ``time/*`` | ``Test Time (*)`` | ``Test Time (sec)`` | ``~Time[*]`` |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Step`` | Yes | ``Step Index`` | ``Ns`` | ``Step Index`` | ``Step`` | ``Line`` |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Cycle`` | Yes | *Auto from Step* | *Auto from Step* | *Auto from Step* | *Auto from Step* | *Auto from Step* |
+ | | | | | | | |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Event`` | Yes | *Auto from Step* | *Auto from Step* | *Auto from Step* | *Auto from Step* | *Auto from Step* |
+ | | | | | | | |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Current [A]`` | Yes | ``Current(*)`` | ``I/*`` | ``Current (*)`` | ``Current`` | ``I[*]`` |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Voltage [V]`` | Yes | ``Voltage(*)`` | ``Ecell/*`` | ``Voltage (*)`` | ``Voltage`` | ``U[*]`` |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Capacity [Ah]`` | Yes | ``Chg. Cap.(*)``, | ``Q charge/*``, | ``Charge Capacity (*)``, | ``Capacity`` | ``Ah[*]`` |
+ | | | ``DChg. Cap.(*)`` | ``Q discharge/*`` | ``Discharge Capacity (*)`` | | |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+ | ``Temperature [C]`` | No | ``T1(*)`` | ``Temperature/*`` | ``Aux_Temperature_1 (*)`` | ``Temp 1`` | ``T1[*]`` |
+ +----------------------+-----------+------------------------+-----------------------------+-----------------------------+-----------------------------+-----------------------------+
+
+.. raw:: html
+
+
+
+Where no units are provided (as is the case with Maccor), the PyProBE default units are
+assumed.
The columns marked with *Auto from ...* are automatically generated by the PyProBE
data import process. This process includes automatic unit conversion to the PyProBE
diff --git a/pyprobe/cell.py b/pyprobe/cell.py
index 8e5fd1a3..892e572d 100644
--- a/pyprobe/cell.py
+++ b/pyprobe/cell.py
@@ -8,7 +8,7 @@
import polars as pl
from pydantic import BaseModel, Field, field_validator, validate_call
-from pyprobe.cyclers import basecycler, biologic, neware
+from pyprobe.cyclers import arbin, basecycler, basytec, biologic, maccor, neware
from pyprobe.filters import Procedure
from pyprobe.readme_processor import process_readme
@@ -98,11 +98,14 @@ def process_cycler_file(
"neware": neware.Neware,
"biologic": biologic.Biologic,
"biologic_MB": biologic.BiologicMB,
+ "arbin": arbin.Arbin,
+ "maccor": maccor.Maccor,
+ "basytec": basytec.Basytec,
}
t1 = time.time()
importer = cycler_dict[cycler](input_data_path=input_data_path)
self._write_parquet(importer, output_data_path)
- print(f"\tparquet written in {time.time()-t1:.2f} seconds.")
+ print(f"\tparquet written in {time.time()-t1: .2f} seconds.")
@validate_call
def process_generic_file(
@@ -150,7 +153,7 @@ def process_generic_file(
column_dict=column_dict,
)
self._write_parquet(importer, output_data_path)
- print(f"\tparquet written in {time.time()-t1:.2f} seconds.")
+ print(f"\tparquet written in {time.time()-t1: .2f} seconds.")
@validate_call
def add_procedure(
diff --git a/pyprobe/cyclers/arbin.py b/pyprobe/cyclers/arbin.py
new file mode 100644
index 00000000..956ea4f0
--- /dev/null
+++ b/pyprobe/cyclers/arbin.py
@@ -0,0 +1,21 @@
+"""A module to load and process Arbin battery cycler data."""
+
+
+from pyprobe.cyclers.basecycler import BaseCycler
+
+
+class Arbin(BaseCycler):
+ """A class to load and process Neware battery cycler data."""
+
+ input_data_path: str
+ column_dict: dict[str, str] = {
+ "Date Time": "Date",
+ "Test Time (*)": "Time [*]",
+ "Step Index": "Step",
+ "Current (*)": "Current [*]",
+ "Voltage (*)": "Voltage [*]",
+ "Charge Capacity (*)": "Charge Capacity [*]",
+ "Discharge Capacity (*)": "Discharge Capacity [*]",
+ "Aux_Temperature_1 (*)": "Temperature [*]",
+ }
+ datetime_format: str = "%d/%m/%Y %H:%M:%S%.f"
diff --git a/pyprobe/cyclers/basecycler.py b/pyprobe/cyclers/basecycler.py
index 7a2c7861..8a5d0776 100644
--- a/pyprobe/cyclers/basecycler.py
+++ b/pyprobe/cyclers/basecycler.py
@@ -3,10 +3,10 @@
import glob
import os
import warnings
-from typing import Any, Dict, List, Optional
+from typing import Dict, List, Optional
import polars as pl
-from pydantic import BaseModel
+from pydantic import BaseModel, field_validator, model_validator
from pyprobe.units import Units
@@ -19,13 +19,132 @@ class BaseCycler(BaseModel):
column_dict: Dict[str, str]
"""A dictionary mapping the column name format of the cycler to the PyProBE format.
Units are indicated by an asterisk (*)."""
+ datetime_format: Optional[str] = None
+ """The string format of the date column if present. See the
+ `chrono crate `_
+ documentation for more information on the format string.
+ """
- def model_post_init(self, __context: Any) -> None:
- """Post initialization method for the BaseModel."""
+ @field_validator("input_data_path")
+ @classmethod
+ def _check_input_data_path(cls, value: str) -> str:
+ """Check if the input data path is valid.
+
+ Args:
+ value (str): The input data path.
+
+ Returns:
+ str: The input data path.
+ """
+ if "*" in value:
+ files = glob.glob(value)
+ if len(files) == 0:
+ raise ValueError(f"No files found with the pattern {value}.")
+ elif not os.path.exists(value):
+ raise ValueError(f"File not found: path {value} does not exist.")
+ return value
+
+ @field_validator("column_dict")
+ @classmethod
+ def _check_column_dict(cls, value: Dict[str, str]) -> Dict[str, str]:
+ """Check if the column dictionary is valid.
+
+ Args:
+ value (Dict[str, str]): The column dictionary.
+
+ Returns:
+ Dict[str, str]: The column dictionary.
+ """
+ pyprobe_data_columns = {value for value in value.values()}
+ pyprobe_required_columns = {
+ "Time [*]",
+ "Current [*]",
+ "Voltage [*]",
+ "Step",
+ "Capacity [*]",
+ }
+ missing_columns = pyprobe_required_columns - pyprobe_data_columns
+ extra_error_message = ""
+ if "Capacity [*]" in missing_columns:
+ if {"Charge Capacity [*]", "Discharge Capacity [*]"}.issubset(
+ pyprobe_data_columns
+ ):
+ missing_columns.remove("Capacity [*]")
+ else:
+ missing_columns.add("Charge Capacity [*]")
+ missing_columns.add("Discharge Capacity [*]")
+ extra_error_message = (
+ " Capacity can be specified as 'Capacity [*]' or "
+ "'Charge Capacity [*]' and 'Discharge Capacity [*]'."
+ )
+ if len(missing_columns) > 0:
+ raise ValueError(
+ f"The column dictionary is missing one or more required columns: "
+ f"{missing_columns}." + extra_error_message
+ )
+ return value
+
+ @model_validator(mode="after")
+ def import_and_validate_data(self) -> "BaseCycler":
+ """Import the data and validate the column mapping."""
dataframe_list = self._get_dataframe_list()
self._imported_dataframe = self.get_imported_dataframe(dataframe_list)
- self._dataframe_columns = self._imported_dataframe.collect_schema().names()
- self._column_map = self._map_columns(self.column_dict, self._dataframe_columns)
+ self._column_map = self._map_columns(
+ self.column_dict, self._imported_dataframe.collect_schema().names()
+ )
+ self._check_missing_columns(self.column_dict, self._column_map)
+ return self
+
+ @staticmethod
+ def _check_missing_columns(
+ column_dict: Dict[str, str], column_map: Dict[str, Dict[str, str | pl.DataType]]
+ ) -> None:
+ """Check for missing columns in the imported data.
+
+ Args:
+ column_map (Dict[str, Dict[str, str | pl.DataType]]):
+ A dictionary mapping the column name format of the cycler to the PyProBE
+ format.
+
+ Raises:
+ ValueError:
+ If any of ["Time", "Current", "Voltage", "Capacity", "Step"]
+ are missing.
+ """
+ pyprobe_required_columns = set(
+ [
+ "Time",
+ "Current",
+ "Voltage",
+ "Capacity",
+ "Step",
+ ]
+ )
+ missing_columns = pyprobe_required_columns - set(column_map.keys())
+ if missing_columns:
+ if "Capacity" in missing_columns:
+ if (
+ "Charge Capacity" in column_map.keys()
+ and "Discharge Capacity" in column_map.keys()
+ ):
+ missing_columns.remove("Capacity")
+ else:
+ missing_columns.add("Charge Capacity")
+ missing_columns.add("Discharge Capacity")
+ if len(missing_columns) > 0:
+ search_names = []
+ for column in missing_columns:
+ if column != "Step":
+ full_name = column + " [*]"
+ else:
+ full_name = column
+ for cycler_name, pyprobe_name in column_dict.items():
+ if pyprobe_name == full_name:
+ search_names.append(cycler_name)
+ raise ValueError(
+ f"PyProBE cannot find the following columns, please check your data: "
+ f"{search_names}."
+ )
@staticmethod
def read_file(filepath: str) -> pl.DataFrame | pl.LazyFrame:
@@ -119,16 +238,47 @@ def _match_unit(column_name: str, pattern: str) -> Optional[str]:
def _map_columns(
cls, column_dict: Dict[str, str], dataframe_columns: List[str]
) -> Dict[str, Dict[str, str | pl.DataType]]:
- """Map the columns of the imported dataframe to the PyProBE format."""
+ """Map the columns of the imported dataframe to the PyProBE format.
+
+ Args:
+ column_dict (Dict[str, str]):
+ A dictionary mapping the column name format of the cycler to the PyProBE
+ format.
+ dataframe_columns (List[str]): The columns of the imported dataframe.
+
+ Returns:
+ Dict[str, Dict[str, str | pl.DataType]]:
+ A dictionary mapping the column name format of the cycler to the PyProBE
+ format.
+
+ Fields (for each quantity):
+ Cycler column name (str): The name of the column in the cycler data.
+ PyProBE column name (str):
+ The name of the column in the PyProBE data.
+ Unit (str): The unit of the column.
+ Type (pl.DataType): The data type of the column.
+ """
column_map: Dict[str, Dict[str, str | pl.DataType]] = {}
for cycler_format, pyprobe_format in column_dict.items():
for cycler_column_name in dataframe_columns:
unit = cls._match_unit(cycler_column_name, cycler_format)
if unit is not None:
quantity = pyprobe_format.replace(" [*]", "")
- if quantity == "Temperature":
- if unit != "K":
- unit = "C"
+ default_units = {
+ "Time": "s",
+ "Current": "A",
+ "Voltage": "V",
+ "Capacity": "Ah",
+ "Charge Capacity": "Ah",
+ "Discharge Capacity": "Ah",
+ "Temperature": "C",
+ }
+
+ if quantity == "Temperature" and unit != "K":
+ unit = "C"
+ elif unit == "" and quantity in default_units:
+ unit = default_units[quantity]
+
pyprobe_column_name = pyprobe_format.replace("*", unit)
column_map[quantity] = {}
@@ -143,6 +293,37 @@ def _map_columns(
column_map[quantity]["Type"] = pl.Float64
return column_map
+ def _assign_instructions(self) -> None:
+ instruction_dict = {
+ "Date": self.date,
+ "Time": self.time,
+ "Current": self.current,
+ "Voltage": self.voltage,
+ "Capacity": self.capacity,
+ "Temperature": self.temperature,
+ "Step": self.step,
+ "Cycle": self.cycle,
+ "Event": self.event,
+ }
+ for quantity in self._column_map.keys():
+ self._column_map[quantity]["Instruction"] = instruction_dict[quantity]
+
+ @staticmethod
+ def _tabulate_column_map(
+ column_map: Dict[str, Dict[str, str | pl.DataType]]
+ ) -> str:
+ data = {
+ "Quantity": list(column_map.keys()),
+ "Cycler column name": [
+ v["Cycler column name"] for v in column_map.values()
+ ],
+ "PyProBE column name": [
+ v["PyProBE column name"] for v in column_map.values()
+ ],
+ }
+
+ return pl.DataFrame(data)
+
def _convert_names(self, quantity: str) -> pl.Expr:
"""Write a column in the PyProBE column name format and convert its type.
@@ -170,15 +351,17 @@ def pyprobe_dataframe(self) -> pl.DataFrame:
pl.DataFrame: The DataFrame.
"""
required_columns = [
- self.date,
+ self.date if "Date" in self._column_map.keys() else None,
self.time,
self.cycle,
self.step,
self.event,
self.current,
self.voltage,
- self.capacity,
- self.temperature,
+ self.capacity
+ if "Capacity" in self._column_map.keys()
+ else self.capacity_from_ch_dch,
+ self.temperature if "Temperature" in self._column_map.keys() else None,
]
name_converters = [
self._convert_names(quantity) for quantity in self._column_map.keys()
@@ -188,16 +371,15 @@ def pyprobe_dataframe(self) -> pl.DataFrame:
return imported_dataframe.select(required_columns)
@property
- def date(self) -> Optional[pl.Expr]:
+ def date(self) -> pl.Expr:
"""Identify and format the date column.
Returns:
- Optional[pl.Expr]: A polars expression for the date column.
+ pl.Expr: A polars expression for the date column.
"""
- if "Date" in self._column_map.keys():
- return pl.col("Date").str.to_datetime(time_unit="us")
- else:
- return None
+ return pl.col("Date").str.to_datetime(
+ format=self.datetime_format, time_unit="us"
+ )
@property
def time(self) -> pl.Expr:
@@ -276,28 +458,18 @@ def capacity(self) -> pl.Expr:
Returns:
pl.Expr: A polars expression for the capacity column.
"""
- if "Capacity" in self._column_map.keys():
- return Units(
- "Capacity", self._column_map["Capacity"]["Unit"]
- ).to_default_unit()
- else:
- return self.capacity_from_ch_dch
+ return Units("Capacity", self._column_map["Capacity"]["Unit"]).to_default_unit()
@property
- def temperature(self) -> Optional[pl.Expr]:
+ def temperature(self) -> pl.Expr:
"""Identify and format the temperature column.
- An optional column, if not found, a column of None values is returned.
-
Returns:
- Optional[pl.Expr]: A polars expression for the temperature column.
+ pl.Expr: A polars expression for the temperature column.
"""
- if "Temperature" in self._column_map.keys():
- return Units(
- "Temperature", self._column_map["Temperature"]["Unit"]
- ).to_default_unit()
- else:
- return None
+ return Units(
+ "Temperature", self._column_map["Temperature"]["Unit"]
+ ).to_default_unit()
@property
def step(self) -> pl.Expr:
diff --git a/pyprobe/cyclers/basytec.py b/pyprobe/cyclers/basytec.py
new file mode 100644
index 00000000..7b8ee00e
--- /dev/null
+++ b/pyprobe/cyclers/basytec.py
@@ -0,0 +1,58 @@
+"""A module to load and process Basytec battery cycler data."""
+
+
+from datetime import datetime
+
+import polars as pl
+
+from pyprobe.cyclers.basecycler import BaseCycler
+
+
+class Basytec(BaseCycler):
+ """A class to load and process Basytec battery cycler data."""
+
+ input_data_path: str
+ column_dict: dict[str, str] = {
+ "Date": "Date",
+ "~Time[*]": "Time [*]",
+ "Line": "Step",
+ "I[*]": "Current [*]",
+ "U[*]": "Voltage [*]",
+ "Ah[*]": "Capacity [*]",
+ "T1[*]": "Temperature [*]",
+ }
+
+ @staticmethod
+ def read_file(filepath: str) -> pl.DataFrame | pl.LazyFrame:
+ """Read a battery cycler file into a DataFrame.
+
+ Args:
+ filepath: The path to the file.
+
+ Returns:
+ pl.DataFrame | pl.LazyFrame: The DataFrame.
+ """
+ n_header_lines = 0
+ with open(filepath, "r", encoding="utf-8") as file:
+ for line in file:
+ if line.startswith("~"):
+ n_header_lines += 1
+ if line.startswith("~Start of Test"):
+ start_time_line = line
+
+ _, value = start_time_line.split(": ")
+ start_time = datetime.strptime(value.strip(), "%d.%m.%Y %H:%M:%S")
+
+ dataframe = pl.scan_csv(
+ filepath, skip_rows=n_header_lines - 1, separator="\t", infer_schema=False
+ )
+
+ dataframe = dataframe.with_columns(
+ (
+ (pl.col("~Time[s]").cast(pl.Float64) * 1000000).cast(pl.Duration)
+ + pl.lit(start_time)
+ )
+ .cast(str)
+ .alias("Date")
+ )
+ return dataframe
diff --git a/pyprobe/cyclers/biologic.py b/pyprobe/cyclers/biologic.py
index 7f6120ac..19d667e5 100644
--- a/pyprobe/cyclers/biologic.py
+++ b/pyprobe/cyclers/biologic.py
@@ -18,6 +18,7 @@ class Biologic(BaseCycler):
"time/*": "Time [*]",
"Ns": "Step",
"I/*": "Current [*]",
+ "/*": "Current [*]",
"Ecell/*": "Voltage [*]",
"Q charge/*": "Charge Capacity [*]",
"Q discharge/*": "Discharge Capacity [*]",
@@ -60,15 +61,6 @@ def read_file(filepath: str) -> pl.DataFrame | pl.LazyFrame:
)
return dataframe
- @property
- def step(self) -> pl.Expr:
- """Identify the step number.
-
- Returns:
- pl.Expr: A polars expression for the step number.
- """
- return pl.col("Step") + 1
-
class BiologicMB(Biologic):
"""A class to load and process Biologic Modulo Bat battery cycler data."""
diff --git a/pyprobe/cyclers/maccor.py b/pyprobe/cyclers/maccor.py
new file mode 100644
index 00000000..a424cb6e
--- /dev/null
+++ b/pyprobe/cyclers/maccor.py
@@ -0,0 +1,96 @@
+"""A module to load and process Maccor battery cycler data."""
+
+
+import polars as pl
+
+from pyprobe.cyclers.basecycler import BaseCycler
+
+
+class Maccor(BaseCycler):
+ """A class to load and process Neware battery cycler data."""
+
+ input_data_path: str
+ column_dict: dict[str, str] = {
+ "DPT Time": "Date",
+ "Test Time (sec)": "Time [*]",
+ "Step": "Step",
+ "Current": "Current [*]",
+ "Voltage": "Voltage [*]",
+ "Capacity": "Capacity [*]",
+ "Temp 1": "Temperature [*]",
+ }
+ datetime_format: str = "%d-%b-%y %I:%M:%S %p"
+
+ @staticmethod
+ def read_file(filepath: str) -> pl.DataFrame | pl.LazyFrame:
+ """Read a battery cycler file into a DataFrame.
+
+ Args:
+ filepath: The path to the file.
+
+ Returns:
+ pl.DataFrame | pl.LazyFrame: The DataFrame.
+ """
+ dataframe = pl.scan_csv(filepath, skip_rows=2, infer_schema=False)
+ return dataframe
+
+ @property
+ def date(self) -> pl.Expr:
+ """Identify and format the date column.
+
+ For the Maccor cycler, this takes the first date in the file and adds the time
+ column to it.
+
+ Returns:
+ pl.Expr: A polars expression for the date column.
+ """
+ if "Date" in self._column_map.keys():
+ date_col = pl.col("Date").str.to_datetime(
+ format=self.datetime_format, time_unit="us"
+ )
+ return (
+ (pl.col("Time [s]").cast(pl.Float64) * 1000000).cast(pl.Duration)
+ + date_col.first()
+ ).alias("Date")
+ else:
+ return None
+
+ @property
+ def charge_capacity(self) -> pl.Expr:
+ """Identify and format the charge capacity column.
+
+ For the Maccor cycler, this is the capacity column when the current is positive.
+
+ Returns:
+ pl.Expr: A polars expression for the charge capacity column.
+ """
+ current_direction = self.current.sign()
+ charge_capacity = pl.col("Capacity [Ah]") * current_direction.replace(-1, 0)
+ return charge_capacity.alias("Charge Capacity [Ah]")
+
+ @property
+ def discharge_capacity(self) -> pl.Expr:
+ """Identify and format the discharge capacity column.
+
+ For the Maccor cycler, this is the capacity column when the current is negative.
+
+ Returns:
+ pl.Expr: A polars expression for the discharge capacity column.
+ """
+ current_direction = self.current.sign()
+ charge_capacity = (
+ pl.col("Capacity [Ah]") * current_direction.replace(1, 0).abs()
+ )
+ return charge_capacity.alias("Discharge Capacity [Ah]")
+
+ @property
+ def capacity(self) -> pl.Expr:
+ """Identify and format the capacity column.
+
+ For the Maccor cycler remove the option to calculate the capacity from a single
+ capacity column.
+
+ Returns:
+ pl.Expr: A polars expression for the capacity column.
+ """
+ return self.capacity_from_ch_dch
diff --git a/pyprobe/cyclers/neware.py b/pyprobe/cyclers/neware.py
index 9afcdef6..fad51051 100644
--- a/pyprobe/cyclers/neware.py
+++ b/pyprobe/cyclers/neware.py
@@ -4,6 +4,7 @@
import polars as pl
from pyprobe.cyclers.basecycler import BaseCycler
+from pyprobe.units import Units
class Neware(BaseCycler):
@@ -18,27 +19,116 @@ class Neware(BaseCycler):
"Chg. Cap.(*)": "Charge Capacity [*]",
"DChg. Cap.(*)": "Discharge Capacity [*]",
"T1(*)": "Temperature [*]",
+ "Total Time": "Time [*]",
+ "Capacity(*)": "Capacity [*]",
}
+ @staticmethod
+ def read_file(filepath: str) -> pl.DataFrame | pl.LazyFrame:
+ """Read a battery cycler file into a DataFrame.
+
+ Args:
+ filepath (str): The path to the file.
+
+ Returns:
+ pl.DataFrame | pl.LazyFrame: The DataFrame.
+ """
+ dataframe = BaseCycler.read_file(filepath)
+ if "Time" in dataframe.collect_schema().names():
+ dataframe = dataframe.with_columns(
+ pl.col("Time")
+ .str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S%.f")
+ .cast(pl.Float64)
+ .alias("Time")
+ )
+ dataframe = dataframe.with_columns(
+ pl.col("Time") - pl.col("Time").first().alias("Time")
+ )
+ dataframe = dataframe.with_columns(pl.col("Time") / 1e6)
+ if "Total Time" in dataframe.collect_schema().names():
+ dataframe = dataframe.with_columns(
+ pl.col("Total Time")
+ .str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S%.f")
+ .cast(pl.Float64)
+ .alias("Total Time")
+ )
+ dataframe = dataframe.with_columns(
+ pl.col("Total Time") - pl.col("Total Time").first().alias("Total Time")
+ )
+ dataframe = dataframe.with_columns(pl.col("Total Time") / 1e6)
+ return dataframe
+
@property
def time(self) -> pl.Expr:
"""Identify and format the time column.
+ For Neware data, by default the time column is calculated from the "Date"
+ column if it exists.
+
Returns:
pl.Expr: A polars expression for the time column.
"""
- if (
- self._imported_dataframe.dtypes[
- self._imported_dataframe.columns.index("Date")
- ]
- != pl.Datetime
- ):
- date = pl.col("Date").str.to_datetime().alias("Date")
+ if "Date" in self._column_map.keys():
+ return (
+ (self.date.diff().dt.total_microseconds().cum_sum() / 1e6)
+ .fill_null(strategy="zero")
+ .alias("Time [s]")
+ )
+ else:
+ return pl.col("Time [s]")
+
+ @property
+ def charge_capacity(self) -> pl.Expr:
+ """Identify and format the charge capacity column.
+
+ For the Neware cycler, this is either the "Chg. Cap.(*)" column or the
+ "Capacity(*)" column when the current is positive.
+
+ Returns:
+ pl.Expr: A polars expression for the charge capacity column.
+ """
+ if "Charge Capacity" in self._column_map.keys():
+ return super().charge_capacity
else:
- date = pl.col("Date")
+ current_direction = self.current.sign()
+ charge_capacity = (
+ Units(
+ "Capacity", self._column_map["Capacity"]["Unit"]
+ ).to_default_unit()
+ * current_direction.replace(-1, 0).abs()
+ )
+ return charge_capacity.alias("Charge Capacity [Ah]")
- return (
- (date.diff().dt.total_microseconds().cum_sum() / 1e6)
- .fill_null(strategy="zero")
- .alias("Time [s]")
- )
+ @property
+ def discharge_capacity(self) -> pl.Expr:
+ """Identify and format the discharge capacity column.
+
+ For the Neware cycler, this is either the "DChg. Cap.(*)" column or the
+ "Capacity(*)" column when the current is negative.
+
+ Returns:
+ pl.Expr: A polars expression for the discharge capacity column.
+ """
+ if "Discharge Capacity" in self._column_map.keys():
+ return super().discharge_capacity
+ else:
+ current_direction = self.current.sign()
+ discharge_capacity = (
+ Units(
+ "Capacity", self._column_map["Capacity"]["Unit"]
+ ).to_default_unit()
+ * current_direction.replace(1, 0).abs()
+ )
+ return discharge_capacity.alias("Discharge Capacity [Ah]")
+
+ @property
+ def capacity(self) -> pl.Expr:
+ """Identify and format the capacity column.
+
+ For the Neware cycler remove the option to calculate the capacity from a single
+ capacity column.
+
+ Returns:
+ pl.Expr: A polars expression for the capacity column.
+ """
+ return self.capacity_from_ch_dch
diff --git a/tests/cyclers/test_arbin.py b/tests/cyclers/test_arbin.py
new file mode 100644
index 00000000..3e2d5c29
--- /dev/null
+++ b/tests/cyclers/test_arbin.py
@@ -0,0 +1,30 @@
+"""Tests for the Arbin cycler class."""
+
+from pyprobe.cyclers.arbin import Arbin
+
+
+def test_read_and_process(benchmark):
+ """Test the full process of reading and processing a file."""
+ arbin_cycler = Arbin(
+ input_data_path="tests/sample_data/arbin/sample_data_arbin.csv"
+ )
+
+ def read_and_process():
+ return arbin_cycler.pyprobe_dataframe
+
+ pyprobe_dataframe = benchmark(read_and_process)
+ expected_columns = [
+ "Date",
+ "Time [s]",
+ "Step",
+ "Cycle",
+ "Event",
+ "Current [A]",
+ "Voltage [V]",
+ "Capacity [Ah]",
+ "Temperature [C]",
+ ]
+ assert set(pyprobe_dataframe.columns) == set(expected_columns)
+ assert set(
+ pyprobe_dataframe.select("Event").unique().collect().to_series().to_list()
+ ) == set([0, 1, 2])
diff --git a/tests/cyclers/test_basecycler.py b/tests/cyclers/test_basecycler.py
index 17214d39..57537d79 100644
--- a/tests/cyclers/test_basecycler.py
+++ b/tests/cyclers/test_basecycler.py
@@ -1,6 +1,7 @@
"""Test the basecycler module."""
import copy
import os
+import re
import polars as pl
import polars.testing as pl_testing
@@ -77,21 +78,10 @@ def sample_cycler_instance(sample_dataframe, column_dict):
)
-def test_map_columns(column_dict):
- """Test initialising the basecycler."""
- # test with single file
- dict_with_extra = copy.deepcopy(column_dict)
- dict_with_extra["Ecell [*]"] = "Voltage [*]"
- column_list = [
- "DateTime",
- "T [s]",
- "V [V]",
- "I [mA]",
- "Q [Ah]",
- "Count",
- "Temp [C]",
- ]
- expected_map = {
+@pytest.fixture
+def sample_column_map():
+ """A sample column map."""
+ return {
"Date": {
"Cycler column name": "DateTime",
"PyProBE column name": "Date",
@@ -136,6 +126,64 @@ def test_map_columns(column_dict):
},
}
+
+def test_input_data_path_validator():
+ """Test the input data path validator."""
+ # test with invalid path
+ path = "invalid_path"
+ with pytest.raises(ValueError, match=f"File not found: path {path} does not exist"):
+ BaseCycler._check_input_data_path(path)
+
+ path = "invalid_path*"
+ with pytest.raises(ValueError, match=f"No files found with the pattern {path}."):
+ BaseCycler._check_input_data_path(path)
+
+ # test with valid path
+ assert (
+ BaseCycler._check_input_data_path(
+ "tests/sample_data/neware/sample_data_neware.csv"
+ )
+ == "tests/sample_data/neware/sample_data_neware.csv"
+ )
+
+
+def test_column_dict_validator(column_dict):
+ """Test the column dictionary validator."""
+ # test with missing columns
+ column_dict.pop("I [*]")
+ column_dict.pop("Q [*]")
+ expected_message = re.escape(
+ "The column dictionary is missing one or more required columns: "
+ "{'Current [*]'}."
+ )
+
+ with pytest.raises(ValueError, match=expected_message):
+ BaseCycler._check_column_dict(column_dict)
+
+ column_dict.pop("Q_ch [*]")
+ column_dict.pop("Q_dis [*]")
+ with pytest.raises(
+ ValueError,
+ ):
+ BaseCycler._check_column_dict(column_dict)
+
+
+def test_map_columns(column_dict, sample_column_map):
+ """Test initialising the basecycler."""
+ # test with single file
+ dict_with_extra = copy.deepcopy(column_dict)
+ dict_with_extra["Ecell [*]"] = "Voltage [*]"
+ column_list = [
+ "DateTime",
+ "T [s]",
+ "V [V]",
+ "I [mA]",
+ "Q [Ah]",
+ "Count",
+ "Temp [C]",
+ ]
+ expected_map = sample_column_map
+
assert BaseCycler._map_columns(dict_with_extra, column_list) == expected_map
# missing columns
@@ -144,6 +192,53 @@ def test_map_columns(column_dict):
assert BaseCycler._map_columns(dict_with_extra, column_list) == expected_map
+def test_check_missing_columns(sample_column_map, column_dict):
+ """Test the check missing columns method."""
+ sample_column_map.pop("Current")
+ expected_message = (
+ "PyProBE cannot find the following columns, please check your data: ['I [*]']."
+ )
+ with pytest.raises(ValueError, match=re.escape(expected_message)):
+ BaseCycler._check_missing_columns(column_dict, sample_column_map)
+
+
+def test_tabulate_column_map(sample_column_map):
+ """Test tabulating the column map."""
+ column_map_table = BaseCycler._tabulate_column_map(sample_column_map)
+ expected_dataframe = pl.DataFrame(
+ {
+ "Quantity": [
+ "Date",
+ "Time",
+ "Voltage",
+ "Current",
+ "Capacity",
+ "Step",
+ "Temperature",
+ ],
+ "Cycler column name": [
+ "DateTime",
+ "T [s]",
+ "V [V]",
+ "I [mA]",
+ "Q [Ah]",
+ "Count",
+ "Temp [C]",
+ ],
+ "PyProBE column name": [
+ "Date",
+ "Time [s]",
+ "Voltage [V]",
+ "Current [mA]",
+ "Capacity [Ah]",
+ "Step",
+ "Temperature [C]",
+ ],
+ }
+ )
+ pl_testing.assert_frame_equal(column_map_table, expected_dataframe)
+
+
def test_init(sample_cycler_instance, sample_dataframe):
"""Test initialising the basecycler."""
df = sample_dataframe.with_columns(
diff --git a/tests/cyclers/test_basytec.py b/tests/cyclers/test_basytec.py
new file mode 100644
index 00000000..761f808e
--- /dev/null
+++ b/tests/cyclers/test_basytec.py
@@ -0,0 +1,52 @@
+"""Tests for the Basytec cycler class."""
+
+from datetime import datetime
+
+import polars as pl
+from polars.testing import assert_frame_equal
+
+from pyprobe.cyclers.basytec import Basytec
+
+
+def test_read_file_basytec():
+ """Test reading a Basytec file."""
+ dataframe = Basytec.read_file(
+ "tests/sample_data/basytec/sample_data_basytec.txt"
+ ).collect()
+ assert "Date" in dataframe.columns
+ assert dataframe["Date"][0] == "2023-06-19 17:56:53.000000"
+ assert dataframe["Date"][2] == "2023-06-19 17:56:54.002823"
+
+
+def test_read_and_process_basytec():
+ """Test the full process of reading and processing a file."""
+ basytec_cycler = Basytec(
+ input_data_path="tests/sample_data/basytec/sample_data_basytec.txt"
+ )
+ pyprobe_dataframe = basytec_cycler.pyprobe_dataframe
+ expected_columns = [
+ "Date",
+ "Time [s]",
+ "Step",
+ "Cycle",
+ "Event",
+ "Current [A]",
+ "Voltage [V]",
+ "Capacity [Ah]",
+ "Temperature [C]",
+ ]
+ assert set(pyprobe_dataframe.columns) == set(expected_columns)
+ last_row = pl.LazyFrame(
+ {
+ "Date": datetime(2023, 6, 19, 17, 58, 3, 235803),
+ "Time [s]": [70.235804],
+ "Cycle": [0],
+ "Step": [4],
+ "Event": [1],
+ "Current [A]": [0.449602],
+ "Voltage [V]": [3.53285],
+ "Capacity [Ah]": [0.001248916998009],
+ "Temperature [C]": [25.47953],
+ }
+ )
+ assert_frame_equal(pyprobe_dataframe.tail(1), last_row)
diff --git a/tests/cyclers/test_biologic.py b/tests/cyclers/test_biologic.py
index e4293252..9e94aa27 100644
--- a/tests/cyclers/test_biologic.py
+++ b/tests/cyclers/test_biologic.py
@@ -93,7 +93,7 @@ def read_and_process():
steps = list(
pyprobe_dataframe.select(pl.col("Step")).collect().unique().to_numpy().flatten()
)
- assert set(steps) == set([1, 2])
+ assert set(steps) == set([0, 1])
pyprobe_dataframe = biologic_MB_cycler.pyprobe_dataframe
expected_columns = [
@@ -121,7 +121,7 @@ def read_and_process():
steps = list(
pyprobe_dataframe.select(pl.col("Step")).collect().unique().to_numpy().flatten()
)
- assert set(steps) == set([1, 2, 3, 4, 5, 6])
+ assert set(steps) == set([0, 1, 2, 3, 4, 5])
def test_process_dataframe(monkeypatch):
@@ -164,7 +164,7 @@ def test_process_dataframe(monkeypatch):
expected_dataframe = pl.DataFrame(
{
"Time [s]": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
- "Step": [1, 1, 2, 2, 2, 1, 1],
+ "Step": [0, 0, 1, 1, 1, 0, 0],
"Current [A]": [1e-3, 2e-3, 3e-3, 4e-3, 0, 0, 0],
"Voltage [V]": [4.0, 5.0, 6.0, 7.0, 0.0, 0.0, 0.0],
"Capacity [Ah]": [0.020, 0.040, 0.030, 0.020, 0.020, 0.020, 0.020],
diff --git a/tests/cyclers/test_maccor.py b/tests/cyclers/test_maccor.py
new file mode 100644
index 00000000..dfbd7588
--- /dev/null
+++ b/tests/cyclers/test_maccor.py
@@ -0,0 +1,127 @@
+"""Tests for the Maccor cycler class."""
+from datetime import datetime
+
+import polars as pl
+import pytest
+from polars.testing import assert_frame_equal
+
+from pyprobe.cyclers.maccor import Maccor
+
+
+def test_read_and_process_maccor():
+ """Test reading and processing a sample Maccor file."""
+ maccor_cycler = Maccor(
+ input_data_path="tests/sample_data/maccor/sample_data_maccor.csv"
+ )
+ pyprobe_dataframe = maccor_cycler.pyprobe_dataframe
+ expected_columns = [
+ "Date",
+ "Time [s]",
+ "Step",
+ "Cycle",
+ "Event",
+ "Current [A]",
+ "Voltage [V]",
+ "Capacity [Ah]",
+ "Temperature [C]",
+ ]
+ assert set(pyprobe_dataframe.columns) == set(expected_columns)
+ last_row = pl.LazyFrame(
+ {
+ "Date": datetime(2023, 11, 23, 15, 56, 24, 60000),
+ "Time [s]": [13.06],
+ "Cycle": [0],
+ "Step": [2],
+ "Event": [1],
+ "Current [A]": [28.798],
+ "Voltage [V]": [3.716],
+ "Capacity [Ah]": [0.048],
+ "Temperature [C]": [22.2591],
+ }
+ )
+ assert_frame_equal(pyprobe_dataframe.tail(1), last_row)
+
+
+@pytest.fixture
+def sample_dataframe():
+ """Return a sample DataFrame."""
+ return pl.DataFrame(
+ {
+ "Date": [
+ "01-Jan-21 11:00:00 PM",
+ "01-Jan-21 11:00:01 PM",
+ "01-Jan-21 11:00:02 PM",
+ "01-Jan-21 11:00:03 PM",
+ "01-Jan-21 11:00:04 PM",
+ "01-Jan-21 11:00:05 PM",
+ ],
+ "Time [s]": [0.0, 1.0, 2.0, 3.0, 4.0, 5.1],
+ "Capacity [Ah]": [1, 2, 3, 4, 5, 6],
+ "Current [A]": [1, 1, 0, -1, -1, 0],
+ }
+ )
+
+
+def test_date(sample_dataframe):
+ """Test the date property."""
+ maccor_cycler = Maccor(
+ input_data_path="tests/sample_data/maccor/sample_data_maccor.csv"
+ )
+ date = maccor_cycler.date
+ expected_dataframe = pl.DataFrame(
+ {
+ "Date": [
+ "2021-01-01 23:00:00.0",
+ "2021-01-01 23:00:01.0",
+ "2021-01-01 23:00:02.0",
+ "2021-01-01 23:00:03.0",
+ "2021-01-01 23:00:04.0",
+ "2021-01-01 23:00:05.1",
+ ]
+ }
+ ).with_columns(
+ pl.col("Date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%.f", time_unit="us")
+ )
+ assert_frame_equal(sample_dataframe.select(date), expected_dataframe)
+
+
+def test_charge_capacity(sample_dataframe):
+ """Test the charge_capacity property."""
+ maccor_cycler = Maccor(
+ input_data_path="tests/sample_data/maccor/sample_data_maccor.csv"
+ )
+ charge_capacity = maccor_cycler.charge_capacity
+ assert_frame_equal(
+ sample_dataframe.select(charge_capacity),
+ pl.DataFrame({"Charge Capacity [Ah]": [1.0, 2.0, 0.0, 0.0, 0.0, 0.0]}),
+ )
+
+
+def test_discharge_capacity(sample_dataframe):
+ """Test the discharge_capacity property."""
+ maccor_cycler = Maccor(
+ input_data_path="tests/sample_data/maccor/sample_data_maccor.csv"
+ )
+ discharge_capacity = maccor_cycler.discharge_capacity
+ assert_frame_equal(
+ sample_dataframe.select(discharge_capacity),
+ pl.DataFrame({"Discharge Capacity [Ah]": [0.0, 0.0, 0.0, 4.0, 5.0, 0.0]}),
+ )
+
+
+def test_capacity(sample_dataframe):
+ """Test the capacity property."""
+ maccor_cycler = Maccor(
+ input_data_path="tests/sample_data/maccor/sample_data_maccor.csv"
+ )
+ capacity = maccor_cycler.capacity
+ dataframe = pl.DataFrame(
+ {
+ "Capacity [Ah]": [0.0, 20.0, 10.0, 20.0, 20.0, 20.0],
+ "Current [A]": [0.0, 3.0, -2.0, -2.0, 0.0, 0.0],
+ }
+ )
+ assert_frame_equal(
+ dataframe.select(capacity),
+ pl.DataFrame({"Capacity [Ah]": [20.0, 40.0, 30.0, 20.0, 20.0, 20.0]}),
+ )
diff --git a/tests/cyclers/test_neware.py b/tests/cyclers/test_neware.py
index 89de43ab..d64fd05e 100644
--- a/tests/cyclers/test_neware.py
+++ b/tests/cyclers/test_neware.py
@@ -22,6 +22,19 @@ def test_read_file(neware_cycler):
)
assert isinstance(unprocessed_dataframe, pl.DataFrame)
+ # Test that Time and Total time are read correctly
+ expected_start = pl.DataFrame(
+ {
+ "Time": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
+ "Total Time": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
+ }
+ )
+ pl_testing.assert_frame_equal(
+ neware_cycler._imported_dataframe.select("Time", "Total Time").head(6),
+ expected_start,
+ )
+ assert neware_cycler._imported_dataframe["Total Time"][-1] == 562784.5
+
def test_sort_files(neware_cycler):
"""Test the _sort_files method."""
@@ -46,7 +59,7 @@ def test_read_multiple_files(neware_cycler):
assert isinstance(unprocessed_dataframe, pl.DataFrame)
-def test_process_dataframe(monkeypatch):
+def test_process_dataframe():
"""Test the neware method."""
mock_dataframe = pl.DataFrame(
{
@@ -56,10 +69,18 @@ def test_process_dataframe(monkeypatch):
datetime(2022, 2, 2, 2, 2, 2),
datetime(2022, 2, 2, 2, 2, 3),
datetime(2022, 2, 2, 2, 2, 4),
+ datetime(2022, 2, 2, 2, 2, 5, 100000),
+ ],
+ "Total Time": [
+ datetime(2022, 2, 2, 2, 2, 0),
+ datetime(2022, 2, 2, 2, 2, 1),
+ datetime(2022, 2, 2, 2, 2, 2),
+ datetime(2022, 2, 2, 2, 2, 3),
+ datetime(2022, 2, 2, 2, 2, 4, 100000),
datetime(2022, 2, 2, 2, 2, 5),
],
"Step Index": [1, 2, 1, 2, 4, 5],
- "Current(mA)": [1, 2, 3, 4, 0, 0],
+ "Current(mA)": [1, 2, -3, -4, 0, 0],
"Voltage(V)": [4, 5, 6, 7, 8, 9],
"Chg. Cap.(mAh)": [
0,
@@ -69,15 +90,61 @@ def test_process_dataframe(monkeypatch):
0,
0,
],
- "DChg. Cap.(mAh)": [0, 0, 10, 20, 20, 20],
+ "DChg. Cap.(mAh)": [0, 0, 10, 20, 0, 0],
+ "Capacity(mAh)": [0, 20, 10, 20, 0, 0],
"T1(℃)": [25, 25, 25, 25, 25, 25],
}
)
mock_dataframe.write_excel("tests/sample_data/mock_dataframe.xlsx")
neware_cycler = Neware(input_data_path="tests/sample_data/mock_dataframe.xlsx")
+ pyprobe_dataframe = neware_cycler.pyprobe_dataframe
+
+ pyprobe_dataframe = pyprobe_dataframe.select(
+ [
+ "Time [s]",
+ "Step",
+ "Current [A]",
+ "Voltage [V]",
+ "Capacity [Ah]",
+ "Temperature [C]",
+ ]
+ )
+ expected_dataframe = pl.DataFrame(
+ {
+ "Time [s]": [0.0, 1.0, 2.0, 3.0, 4.0, 5.1],
+ "Step": [1, 2, 1, 2, 4, 5],
+ "Current [A]": [1e-3, 2e-3, -3e-3, -4e-3, 0, 0],
+ "Voltage [V]": [4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
+ "Capacity [Ah]": [20.0e-3, 40.0e-3, 30.0e-3, 20.0e-3, 20.0e-3, 20.0e-3],
+ "Temperature [C]": [25.0, 25.0, 25.0, 25.0, 25.0, 25.0],
+ }
+ )
+ pl_testing.assert_frame_equal(pyprobe_dataframe, expected_dataframe)
+ os.remove("tests/sample_data/mock_dataframe.xlsx")
+ # Test with a dataframe that does not contain a Charge or Discharge Capacity column
+ mock_dataframe = mock_dataframe.drop("Chg. Cap.(mAh)")
+ mock_dataframe = mock_dataframe.drop("DChg. Cap.(mAh)")
+ mock_dataframe.write_excel("tests/sample_data/mock_dataframe.xlsx")
+ neware_cycler = Neware(input_data_path="tests/sample_data/mock_dataframe.xlsx")
pyprobe_dataframe = neware_cycler.pyprobe_dataframe
+ pyprobe_dataframe = pyprobe_dataframe.select(
+ [
+ "Time [s]",
+ "Step",
+ "Current [A]",
+ "Voltage [V]",
+ "Capacity [Ah]",
+ "Temperature [C]",
+ ]
+ )
+ pl_testing.assert_frame_equal(pyprobe_dataframe, expected_dataframe)
+ # Test with a dataframe that does not contain a "Date" column
+ mock_dataframe = mock_dataframe.drop("Date")
+ mock_dataframe.write_excel("tests/sample_data/mock_dataframe.xlsx")
+ neware_cycler = Neware(input_data_path="tests/sample_data/mock_dataframe.xlsx")
+ pyprobe_dataframe = neware_cycler.pyprobe_dataframe
pyprobe_dataframe = pyprobe_dataframe.select(
[
"Time [s]",
@@ -90,9 +157,9 @@ def test_process_dataframe(monkeypatch):
)
expected_dataframe = pl.DataFrame(
{
- "Time [s]": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
+ "Time [s]": [0.0, 1.0, 2.0, 3.0, 4.1, 5.0],
"Step": [1, 2, 1, 2, 4, 5],
- "Current [A]": [1e-3, 2e-3, 3e-3, 4e-3, 0, 0],
+ "Current [A]": [1e-3, 2e-3, -3e-3, -4e-3, 0, 0],
"Voltage [V]": [4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
"Capacity [Ah]": [20.0e-3, 40.0e-3, 30.0e-3, 20.0e-3, 20.0e-3, 20.0e-3],
"Temperature [C]": [25.0, 25.0, 25.0, 25.0, 25.0, 25.0],
diff --git a/tests/sample_data/arbin/sample_data_arbin.csv b/tests/sample_data/arbin/sample_data_arbin.csv
new file mode 100644
index 00000000..c8c47269
--- /dev/null
+++ b/tests/sample_data/arbin/sample_data_arbin.csv
@@ -0,0 +1,14 @@
+Data Point,Date Time,Test Time (s),Step Time (s),Cycle Index,Step Index,TC_Counter1,TC_Counter2,TC_Counter3,Current (A),Voltage (V),Power (W),Charge Capacity (Ah),Discharge Capacity (Ah),Charge Energy (Wh),Discharge Energy (Wh),Capacity (Ah),mAh/g,ACR (Ohm),dV/dt (V/s),Internal Resistance (Ohm),dQ/dV (Ah/V),dV/dQ (V/Ah),Aux_Temperature_1 (C),Aux_dT/dt_1 (C/s)
+1," 09/20/2024 08:32:34.558",30.0005,30.0005,1,1,0,0,0,0,3.534595,0,0,0,0,0,0,0,,1.98217E-05,,,,24.66422,-0.09131343
+2," 09/20/2024 08:33:04.559",60.0008,60.0008,1,1,0,0,0,0,3.534597,0,0,0,0,0,0,0,,2.71962E-05,,,,24.73967,0.02648412
+3," 09/20/2024 08:33:34.559",90.0013,90.0013,1,1,0,0,0,0,3.534578,0,0,0,0,0,0,0,,1.00484E-05,,,,24.71824,0.04604245
+4," 09/20/2024 08:34:04.559",120.0016,120.0016,1,1,0,0,0,0,3.534572,0,0,0,0,0,0,0,,2.60919E-05,,,,24.63211,-0.09084845
+5," 09/20/2024 08:34:34.560",150.0017,150.0017,1,1,0,0,0,0,3.534552,0,0,0,0,0,0,0,,1.1247E-05,,0,0,24.75344,0.06064539
+6," 09/20/2024 08:35:04.560",180.0019,180.0019,1,1,0,0,0,0,3.534617,0,0,0,0,0,0,0,,9.40362E-06,,,,24.62239,-0.1132662
+7," 09/20/2024 08:35:34.560",210.0021,210.0021,1,1,0,0,0,0,3.534601,0,0,0,0,0,0,0,,2.22192E-05,,,,24.75955,0.01128928
+8," 09/20/2024 08:36:04.560",240.0022,240.0022,1,1,0,0,0,0,3.53458,0,0,0,0,0,0,0,,2.30485E-05,,,,24.721,0.0339758
+9," 09/20/2024 08:36:34.560",270.0025,270.0025,1,1,0,0,0,0,3.534572,0,0,0,0,0,0,0,,2.02819E-05,,0,0,24.73034,0.07981738
+10," 09/20/2024 08:37:04.559",300.0008,300.0008,1,1,0,0,0,0,3.534585,0,0,0,0,0,0,0,,2.2678E-05,0,0,0,24.72579,0.03765556
+11," 09/20/2024 08:37:04.562",300.0039,0.0006,1,2,0,0,0,0,3.534586,0,0,0,0,0,0,0,,2.2678E-05,0,0,0,24.72550637,0.037089043
+12," 09/20/2024 08:37:05.256",300.6979,0.0012,1,3,0,0,0,2.647604,3.594547,9.516937015,2.01404E-05,2.04379E-05,7.1302E-05,7.21224E-05,2.01404E-05,0,,0.119767502,,,,24.66201,-0.08973769
+13," 09/20/2024 08:37:05.772",301.214,0.5173,1,3,0,0,0,2.650138,3.599601,9.539439395,0.000400839,2.04379E-05,0.001441009,7.21224E-05,0.000400839,0,,,,,,24.68785,-0.08973769
\ No newline at end of file
diff --git a/tests/sample_data/basytec/sample_data_basytec.txt b/tests/sample_data/basytec/sample_data_basytec.txt
new file mode 100644
index 00000000..49e47434
--- /dev/null
+++ b/tests/sample_data/basytec/sample_data_basytec.txt
@@ -0,0 +1,87 @@
+~Resultfile from Basytec Battery Test System
+~Date and Time of Data Converting: 07.07.2023 10:55:58
+~
+~Name of Test: test
+~Battery: cell
+~Testplan: plan.pln
+~Testchannel: 1814 CH14 XCTS_40
+~Start of Test: 19.06.2023 17:56:53
+~End of Test: 05.07.2023 11:05:04
+~Operator (Test): basytec
+~Operator (Data converting): basytec
+~
+~Time[s] DataSet t-Step[s] t-Set[s] t-Cyc[s] Line Command U[V] I[A] P[W] Ah[Ah] Ah-Cyc-Charge Ah-Cyc-Discharge Ah-Cyc-Charge-0 Ah-Cyc-Discharge-0 Ah-Step Wh[Wh] Wh-Cyc-Charge Wh-Cyc-Discharge Wh-Cyc-Charge-0 Wh-Cyc-Discharge-0 Wh-Step T1[�C] R-AC R-DC Level Cyc-Count Count State
+0 1 0 0 0 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 0 1 3
+0.00401299999999999 2 0.00401299999999999 0.00401299999999999 0.00401299999999999 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 0
+1.002823 3 1.002823 1.002823 1.002823 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+2.00232933333334 4 2.00232933333334 2.00232933333334 2.00232933333334 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+3.003276 5 3.003276 3.003276 3.003276 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+4.00280533333333 6 4.00280533333333 4.00280533333333 4.00280533333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+5.002879 7 5.002879 5.002879 5.002879 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+6.00394733333334 8 6.00394733333334 6.00394733333334 6.00394733333334 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+7.002707 9 7.002707 7.002707 7.002707 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+8.002436 10 8.002436 8.002436 8.002436 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+9.00285333333332 11 9.00285333333332 9.00285333333332 9.00285333333332 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+10.0027263333333 12 10.0027263333333 10.0027263333333 10.0027263333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+11.002709 13 11.002709 11.002709 11.002709 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+12.0024576666667 14 12.0024576666667 12.0024576666667 12.0024576666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+13.0030616666667 15 13.0030616666667 13.0030616666667 13.0030616666667 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+14.0028443333333 16 14.0028443333333 14.0028443333333 14.0028443333333 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+15.0027586666667 17 15.0027586666667 15.0027586666667 15.0027586666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+16.0028896666667 18 16.0028896666667 16.0028896666667 16.0028896666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+17.003209 19 17.003209 17.003209 17.003209 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+18.0026233333333 20 18.0026233333333 18.0026233333333 18.0026233333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.48258 0 0 1 1 1 1
+19.0030663333333 21 19.0030663333333 19.0030663333333 19.0030663333333 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+20.002527 22 20.002527 20.002527 20.002527 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+21.0026856666667 23 21.0026856666667 21.0026856666667 21.0026856666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+22.0025676666667 24 22.0025676666667 22.0025676666667 22.0025676666667 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+23.0030373333333 25 23.0030373333333 23.0030373333333 23.0030373333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+24.0026603333333 26 24.0026603333333 24.0026603333333 24.0026603333333 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+25.0030303333333 27 25.0030303333333 25.0030303333333 25.0030303333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+26.003809 28 26.003809 26.003809 26.003809 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+27.0026733333333 29 27.0026733333333 27.0026733333333 27.0026733333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+28.002601 30 28.002601 28.002601 28.002601 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+29.00298 31 29.00298 29.00298 29.00298 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+30.003332 32 30.003332 30.003332 30.003332 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+31.002611 33 31.002611 31.002611 31.002611 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+32.0028283333333 34 32.0028283333333 32.0028283333333 32.0028283333333 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+33.0026066666667 35 33.0026066666667 33.0026066666667 33.0026066666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+34.002829 36 34.002829 34.002829 34.002829 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+35.0031953333334 37 35.0031953333334 35.0031953333334 35.0031953333334 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+36.0027136666667 38 36.0027136666667 36.0027136666667 36.0027136666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+37.0026203333332 39 37.0026203333332 37.0026203333332 37.0026203333332 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+38.002518 40 38.002518 38.002518 38.002518 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+39.0025693333332 41 39.0025693333332 39.0025693333332 39.0025693333332 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+40.0033496666668 42 40.0033496666668 40.0033496666668 40.0033496666668 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47647 0 0 1 1 1 1
+41.0029476666667 43 41.0029476666667 41.0029476666667 41.0029476666667 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+42.0031113333332 44 42.0031113333332 42.0031113333332 42.0031113333332 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+43.0028363333333 45 43.0028363333333 43.0028363333333 43.0028363333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+44.0032296666668 46 44.0032296666668 44.0032296666668 44.0032296666668 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+45.0027653333332 47 45.0027653333332 45.0027653333332 45.0027653333332 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+46.0025643333334 48 46.0025643333334 46.0025643333334 46.0025643333334 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+47.002801 49 47.002801 47.002801 47.002801 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+48.0028053333334 50 48.0028053333334 48.0028053333334 48.0028053333334 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+49.0030513333333 51 49.0030513333333 49.0030513333333 49.0030513333333 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+50.0032166666666 52 50.0032166666666 50.0032166666666 50.0032166666666 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+51.0028750000001 53 51.0028750000001 51.0028750000001 51.0028750000001 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.48258 0 0 1 1 1 1
+52.0029506666668 54 52.0029506666668 52.0029506666668 52.0029506666668 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+53.0026653333334 55 53.0026653333334 53.0026653333334 53.0026653333334 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+54.0035790000001 56 54.0035790000001 54.0035790000001 54.0035790000001 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+55.0025433333335 57 55.0025433333335 55.0025433333335 55.0025433333335 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+56.0029926666668 58 56.0029926666668 56.0029926666668 56.0029926666668 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+57.0006923333335 59 57.0006923333335 57.0006923333335 57.0006923333335 3 Pause 3.52575489148741 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+58.0032793333332 60 58.0032793333332 58.0032793333332 58.0032793333332 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+59.0035526666665 61 59.0035526666665 59.0035526666665 59.0035526666665 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 1
+59.9999993333334 62 59.9999993333334 59.9999993333334 59.9999993333334 3 Pause 3.5255631284671 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25.47953 0 0 1 1 1 2
+60.2322676666668 63 0 60.2322676666668 60.2322676666668 4 Charge 3.52575489148741 0.00195196122033401 0.00688213682058638 1.259385498273E-7 1.259385498273E-7 0 1.259385498273E-7 0 1.259385498273E-7 4.44028458080434E-7 4.44028458080434E-7 0 4.44028458080434E-7 0 4.44028458080434E-7 25.47953 0 0 1 1 1 3
+60.2382183333332 64 0.00595066666666666 60.2382183333332 60.2382183333332 4 Charge 3.52997367793431 0.453505656857602 1.60086303150165 8.75566604107107E-7 8.75566604107107E-7 0 8.75566604107107E-7 0 8.75566604107107E-7 3.09019575792926E-6 3.09019575792926E-6 0 3.09019575792926E-6 0 3.09019575792926E-6 25.47953 0.00934282342688728 0.00934282342688728 1 1 1 0
+61.2359273333333 65 1.00365966666667 61.2359273333333 61.2359273333333 4 Charge 3.53074073001556 0.449601734416934 1.58742715599151 0.000125506197586236 0.000125506197586236 0 0.000125506197586236 0 0.000125506197586236 0.00044309987534853 0.00044309987534853 0 0.00044309987534853 0 0.00044309987534853 25.47953 0.00934282342688728 0.0111378108693745 1 1 1 1
+62.2359889999999 66 2.00372133333333 62.2359889999999 62.2359889999999 4 Charge 3.53112425605619 0.448951080676823 1.58530205076057 0.000250358011318813 0.000250358011318813 0 0.000250358011318813 0 0.000250358011318813 0.000883949623025544 0.000883949623025544 0 0.000883949623025544 0 0.000883949623025544 25.48258 0.00934282342688728 0.0120120249192036 1 1 1 1
+63.2355006666668 67 3.003233 63.2355006666668 63.2355006666668 4 Charge 3.53150778209682 0.448951080676823 1.58547423519098 0.00037510288607274 0.00037510288607274 0 0.00037510288607274 0 0.00037510288607274 0.00132445862000565 0.00132445862000565 0 0.00132445862000565 0 0.00132445862000565 25.48258 0.00934282342688728 0.0128700267138511 1 1 1 1
+64.2362143333334 68 4.00394666666668 64.2362143333334 64.2362143333334 4 Charge 3.53169954511713 0.449601734416934 1.58785824092416 0.000500030166903541 0.000500030166903541 0 0.000500030166903541 0 0.000500030166903541 0.00176564619027433 0.00176564619027433 0 0.00176564619027433 0 0.00176564619027433 25.48258 0.00934282342688728 0.0132796975750234 1 1 1 1
+65.2362810000001 69 5.00401333333332 65.2362810000001 65.2362810000001 4 Charge 3.53189130813745 0.448951080676823 1.58564641962138 0.000624815120301225 0.000624815120301225 0 0.000624815120301225 0 0.000624815120301225 0.00220635871205227 0.00220635871205227 0 0.00220635871205227 0 0.00220635871205227 25.48258 0.00934282342688728 0.013728028439878 1 1 1 1
+66.2359013333334 70 6.00363366666667 66.2359013333334 66.2359013333334 4 Charge 3.53208307115776 0.448951080676823 1.58573251183659 0.000749617238366292 0.000749617238366292 0 0.000749617238366292 0 0.000749617238366292 0.00264716195585662 0.00264716195585662 0 0.00264716195585662 0 0.00264716195585662 25.47953 0.00934282342688728 0.0141570293372017 1 1 1 1
+67.2357786666667 71 7.00351099999999 67.2357786666667 67.2357786666667 4 Charge 3.53227483417807 0.449601734416934 1.58811689188375 0.000874462050401922 0.000874462050401922 0 0.000874462050401922 0 0.000874462050401922 0.00308814490084926 0.00308814490084926 0 0.00308814490084926 0 0.00308814490084926 25.47953 0.00934282342688728 0.0145648295984128 1 1 1 1
+68.2355886666667 72 8.00332099999999 68.2355886666667 68.2355886666667 4 Charge 3.53246659719839 0.448951080676823 1.585904696267 0.000999238021103003 0.000999238021103003 0 0.000999238021103003 0 0.000999238021103003 0.00352890651280298 0.00352890651280298 0 0.00352890651280298 0 0.00352890651280298 25.47953 0.00934282342688728 0.0150150311318493 1 1 1 1
+69.2353230000001 73 9.00305533333332 69.2353230000001 69.2353230000001 4 Charge 3.5326583602187 0.448951080676823 1.5859907884822 0.00112403671203023 0.00112403671203023 0 0.00112403671203023 0 0.00112403671203023 0.0039697725279783 0.0039697725279783 0 0.0039697725279783 0 0.0039697725279783 25.47953 0.00934282342688728 0.015444032029173 1 1 1 1
+70.2358036666668 74 10.003536 70.2358036666668 70.2358036666668 4 Charge 3.53285012323902 0.449601734416934 1.58837554284334 0.001248916998009 0.001248916998009 0 0.001248916998009 0 0.001248916998009 0.00441094744918941 0.00441094744918941 0 0.00441094744918941 0 0.00441094744918941 25.47953 0.00934282342688728 0.0158499616218021 1 1 1 1
\ No newline at end of file
diff --git a/tests/sample_data/maccor/sample_data_maccor.csv b/tests/sample_data/maccor/sample_data_maccor.csv
new file mode 100644
index 00000000..e9c06eb4
--- /dev/null
+++ b/tests/sample_data/maccor/sample_data_maccor.csv
@@ -0,0 +1,18 @@
+Today's Date ,28-Nov-23
+Date of Test:,23-Nov-23 3:56:08 PM
+Rec,Cycle C,Step,Test Time (sec),Step Time (sec),Capacity,Energy,Current,Voltage,DPT Time,Temp 1
+1,1,1,0.0000,0.0000,0,0,0,3.668,23-Nov-23 3:56:11 PM,22.2591
+2,1,1,1.0000,1.0000,0,0,0,3.668,23-Nov-23 3:56:12 PM,22.2591
+3,1,1,2.0000,2.0000,0,0,0,3.668,23-Nov-23 3:56:13 PM,22.2591
+4,1,1,3.0000,3.0000,0,0,0,3.668,23-Nov-23 3:56:14 PM,22.2591
+5,1,1,4.0000,4.0000,0,0,0,3.668,23-Nov-23 3:56:15 PM,22.2591
+6,1,1,5.0000,5.0000,0,0,0,3.668,23-Nov-23 3:56:16 PM,22.2591
+7,1,1,6.0000,6.0000,0,0,0,3.668,23-Nov-23 3:56:17 PM,22.2591
+8,1,1,7.0000,7.0000,0,0,0,3.668,23-Nov-23 3:56:18 PM,22.2591
+9,1,1,8.0000,8.0000,0,0,0,3.668,23-Nov-23 3:56:19 PM,22.2591
+10,1,1,9.0000,9.0000,0,0,0,3.668,23-Nov-23 3:56:20 PM,22.2591
+11,1,1,10.0000,10.0000,0,0,0,3.668,23-Nov-23 3:56:21 PM,22.2591
+12,1,2,10.0600,0.0600,0.000,0.002,28.844,3.709,23-Nov-23 3:56:21 PM,22.2591
+13,1,2,11.0600,1.0600,0.008,0.031,28.799,3.713,23-Nov-23 3:56:22 PM,22.2591
+14,1,2,12.0600,2.0600,0.016,0.061,28.798,3.715,23-Nov-23 3:56:23 PM,22.2591
+15,1,2,13.0600,3.0600,0.024,0.091,28.798,3.716,23-Nov-23 3:56:24 PM,22.2591
\ No newline at end of file