Providing Valid Inputs#

PyProbe uses Pydantic for input validation. This exists to ensure that the data provided is in the correct format to prevent unexpected errors. Most of the time, this will happen behind-the-scenes, so you will only notice it if there is a problem. This example is written to demonstrate how these errors may come about.

RawData Validation#

The RawData class is a specific variant of the Result object which only stores data in the standard PyProBE format. Therefore, validation is performed when a RawData object is created to verify this.

If you follow the standard method for importing data into PyProBE, you should never experience these errors, however it is helpful to know that they exist.

We will start with a normal dataset, printing the type that the procedure data is stored in:

[1]:
import pyprobe
import polars as pl

info_dictionary = {'Name': 'Sample cell',
                   'Chemistry': 'NMC622',
                   'Nominal Capacity [Ah]': 0.04,
                   'Cycler number': 1,
                   'Channel number': 1,}
data_directory = '../../../tests/sample_data/neware'

# Create a cell object
cell = pyprobe.Cell(info=info_dictionary)
cell.add_procedure(procedure_name='Sample',
                   folder_path = data_directory,
                   filename = 'sample_data_neware.parquet')
print(type(cell.procedure['Sample']))
<class 'pyprobe.filters.Procedure'>

The Procedure class inherits from RawData, which has a defined set of required columns (the PyProBE standard format):

[2]:
print(pyprobe.rawdata.required_columns)
['Time [s]', 'Step', 'Event', 'Current [A]', 'Voltage [V]', 'Capacity [Ah]']

Whenever a RawData class (or any of the filters module classes, that inherit from it) are created, the dataframe is checked against these required columns. We will create an example dataframe that is missing columns, which will be identified by the error that is returned.

[3]:
incorrect_dataframe = pl.DataFrame({'Time [s]': [1, 2, 3],
                                    'Voltage [V]': [3.5, 3.6, 3.7],
                                    'Current [A]': [0.1, 0.2, 0.3],
                                    })
pyprobe.rawdata.RawData(base_dataframe=incorrect_dataframe,
                        info = {})
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[3], line 5
      1 incorrect_dataframe = pl.DataFrame({'Time [s]': [1, 2, 3],
      2                                     'Voltage [V]': [3.5, 3.6, 3.7],
      3                                     'Current [A]': [0.1, 0.2, 0.3],
      4                                     })
----> 5 pyprobe.rawdata.RawData(base_dataframe=incorrect_dataframe,
      6                         info = {})

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/main.py:193, in BaseModel.__init__(self, **data)
    191 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    192 __tracebackhide__ = True
--> 193 self.__pydantic_validator__.validate_python(data, self_instance=self)

ValidationError: 1 validation error for RawData
base_dataframe
  Value error, Missing required columns: ['Step', 'Event', 'Capacity [Ah]'] [type=value_error, input_value=shape: (3, 3)
┌──...───────┘, input_type=DataFrame]
    For further information visit https://errors.pydantic.dev/2.8/v/value_error

You will also see a validation error if you try to create one of these classes with a data object that is not a Polars DataFrame or LazyFrame:

[4]:
incorrect_data_dict = {'Time [s]': [1, 2, 3],
                                    'Voltage [V]': [3.5, 3.6, 3.7],
                                    'Current [A]': [0.1, 0.2, 0.3],
                                    }
pyprobe.rawdata.RawData(base_dataframe=incorrect_data_dict,
                        info = {})
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[4], line 5
      1 incorrect_data_dict = {'Time [s]': [1, 2, 3],
      2                                     'Voltage [V]': [3.5, 3.6, 3.7],
      3                                     'Current [A]': [0.1, 0.2, 0.3],
      4                                     }
----> 5 pyprobe.rawdata.RawData(base_dataframe=incorrect_data_dict,
      6                         info = {})

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/main.py:193, in BaseModel.__init__(self, **data)
    191 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    192 __tracebackhide__ = True
--> 193 self.__pydantic_validator__.validate_python(data, self_instance=self)

ValidationError: 2 validation errors for RawData
base_dataframe.is-instance[LazyFrame]
  Input should be an instance of LazyFrame [type=is_instance_of, input_value={'Time [s]': [1, 2, 3], '...t [A]': [0.1, 0.2, 0.3]}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.8/v/is_instance_of
base_dataframe.is-instance[DataFrame]
  Input should be an instance of DataFrame [type=is_instance_of, input_value={'Time [s]': [1, 2, 3], '...t [A]': [0.1, 0.2, 0.3]}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.8/v/is_instance_of

Analysis Module Validation#

You are much more likely to experience validation errors when dealing with the functions and classes in the analysis module. These may require a particular PyProBE object to work.

As an example, the Cycling class requires an Experiment input. This is because it provides calculations based on the cycle() method of the experiment class:

[5]:
experiment_object = cell.procedure['Sample'].experiment('Break-in Cycles')
print(type(experiment_object))
<class 'pyprobe.filters.Experiment'>

The experiment object should return no errors:

[6]:
from pyprobe.analysis.cycling import Cycling
cycling = Cycling(input_data = experiment_object)

However, if I were to filter the object further, I would get an error:

[7]:
cycling = Cycling(input_data = experiment_object.cycle(1))
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[7], line 1
----> 1 cycling = Cycling(input_data = experiment_object.cycle(1))

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/main.py:193, in BaseModel.__init__(self, **data)
    191 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    192 __tracebackhide__ = True
--> 193 self.__pydantic_validator__.validate_python(data, self_instance=self)

ValidationError: 1 validation error for Cycling
input_data
  Input should be a valid dictionary or instance of Experiment [type=model_type, input_value=Cycle(base_dataframe=<Laz...hours']}, cycle_info=[]), input_type=Cycle]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type

Functions in the analysis module also contain type validation. This occurs on two levels. First, the inputs to the function are checked. E.g. for the gradient function of the differentiation module, input_data is required as a PyProBE object, and string column names are required for x and y:

[8]:
from pyprobe.analysis import differentiation

gradient = differentiation.gradient(input_data = cell.procedure['Sample'].experiment('Break-in Cycles').discharge(-1),
                                    x = "Capacity [Ah]",
                                    y = "Voltage [V]")

But if I provide an array to input_data, I will get an error to say that input_data should be one of many PyProBE objects:

[9]:
import numpy as np
gradient = differentiation.gradient(input_data = np.zeros((10, 2)),
                                    x = "Capacity [Ah]",
                                    y = "Voltage [V]")
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[9], line 2
      1 import numpy as np
----> 2 gradient = differentiation.gradient(input_data = np.zeros((10, 2)),
      3                                     x = "Capacity [Ah]",
      4                                     y = "Voltage [V]")

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/validate_call_decorator.py:60, in validate_call.<locals>.validate.<locals>.wrapper_function(*args, **kwargs)
     58 @functools.wraps(function)
     59 def wrapper_function(*args, **kwargs):
---> 60     return validate_call_wrapper(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/_internal/_validate_call.py:96, in ValidateCallWrapper.__call__(self, *args, **kwargs)
     95 def __call__(self, *args: Any, **kwargs: Any) -> Any:
---> 96     res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
     97     if self.__return_pydantic_validator__:
     98         return self.__return_pydantic_validator__(res)

ValidationError: 6 validation errors for gradient
input_data.RawData
  Input should be a valid dictionary or instance of RawData [type=model_type, input_value=array([[0., 0.],
       [..., 0.],
       [0., 0.]]), input_type=ndarray]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type
input_data.Procedure
  Input should be a valid dictionary or instance of Procedure [type=model_type, input_value=array([[0., 0.],
       [..., 0.],
       [0., 0.]]), input_type=ndarray]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type
input_data.Experiment
  Input should be a valid dictionary or instance of Experiment [type=model_type, input_value=array([[0., 0.],
       [..., 0.],
       [0., 0.]]), input_type=ndarray]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type
input_data.Cycle
  Input should be a valid dictionary or instance of Cycle [type=model_type, input_value=array([[0., 0.],
       [..., 0.],
       [0., 0.]]), input_type=ndarray]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type
input_data.Step
  Input should be a valid dictionary or instance of Step [type=model_type, input_value=array([[0., 0.],
       [..., 0.],
       [0., 0.]]), input_type=ndarray]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type
input_data.Result
  Input should be a valid dictionary or instance of Result [type=model_type, input_value=array([[0., 0.],
       [..., 0.],
       [0., 0.]]), input_type=ndarray]
    For further information visit https://errors.pydantic.dev/2.8/v/model_type

Analysis functions will also check that the columns you require for the computation are present in the PyProBE objects provided. As an example, we will call the gradient() method, requesting to differentiate a column that does not exist in the underlying data:

[10]:
gradient = differentiation.gradient(input_data = cell.procedure['Sample'].experiment('Break-in Cycles').discharge(-1),
                                    x = "Temperature [C]",
                                    y = "Voltage [V]")
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[10], line 1
----> 1 gradient = differentiation.gradient(input_data = cell.procedure['Sample'].experiment('Break-in Cycles').discharge(-1),
      2                                     x = "Temperature [C]",
      3                                     y = "Voltage [V]")

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/validate_call_decorator.py:60, in validate_call.<locals>.validate.<locals>.wrapper_function(*args, **kwargs)
     58 @functools.wraps(function)
     59 def wrapper_function(*args, **kwargs):
---> 60     return validate_call_wrapper(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/_internal/_validate_call.py:96, in ValidateCallWrapper.__call__(self, *args, **kwargs)
     95 def __call__(self, *args: Any, **kwargs: Any) -> Any:
---> 96     res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
     97     if self.__return_pydantic_validator__:
     98         return self.__return_pydantic_validator__(res)

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pyprobe/analysis/differentiation.py:38, in gradient(input_data, x, y)
     22 """Differentiate smooth data with a finite difference method.
     23
     24 A wrapper of the numpy.gradient function. This method calculates the gradient
   (...)
     35     calculated gradient.
     36 """
     37 # 2. Validate the inputs to the method
---> 38 validator = AnalysisValidator(
     39     input_data=input_data,
     40     required_columns=[x, y],
     41     # required_type not neccessary here as type specified when declaring
     42     # input_data attribute is strict enough
     43 )
     44 # 3. Retrieve the validated columns as numpy arrays
     45 x_data, y_data = validator.variables

File /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pydantic/main.py:193, in BaseModel.__init__(self, **data)
    191 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    192 __tracebackhide__ = True
--> 193 self.__pydantic_validator__.validate_python(data, self_instance=self)

ValidationError: 1 validation error for AnalysisValidator
  Value error, Missing required columns: ['Temperature [C]'] [type=value_error, input_value={'input_data': Step(base_...re [C]', 'Voltage [V]']}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.8/v/value_error