Skip to content

Commit

Permalink
Merge pull request #161 from ImperialCollegeLondon/refactor-dma-analy…
Browse files Browse the repository at this point in the history
…sis-code

Refactor analysis module functions
  • Loading branch information
tomjholland authored Nov 18, 2024
2 parents f99377b + 6d9c501 commit 82da37c
Show file tree
Hide file tree
Showing 32 changed files with 3,694 additions and 949 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ PyProBE is fully open source. For more information about its license, see [LICEN
<br />
<sub><b>Tom Holland</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/mohammedasher">
<img src="https://avatars.githubusercontent.com/u/168521559?v=4" width="100;" alt="mohammedasher"/>
<br />
<sub><b>Mohammed Asheruddin (Asher)</b></sub>
</a>
</td></tr>
</table>
<!-- readme: contributors -end -->
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"show-inheritance": True,
"member-order": "bysource",
}
add_module_names = False

# -- sphinxcontrib-bibtex configuration --------------------------------------
bibtex_bibfiles = ["../../CITATIONS.bib"]
Expand Down
57 changes: 15 additions & 42 deletions docs/source/developer_guide/contributing_to_the_analysis_module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,33 @@ Contributing to the Analysis Module

:mod:`pyprobe.analysis` classes are classes that perform further analysis of the data.

This document describes the standard format to be used for all PyProBE analysis classes.
This document describes the standard format to be used for all PyProBE analysis functions.
Constructing your method in this way ensures compatibility with the rest of the
PyProBE package, while keeping your code clean and easy to read.

Analysis classes are based on `Pydantic BaseModel <https://docs.pydantic.dev/latest/api/base_model/>`_
to provide input validation. However, following the steps below should allow
you to write your own analysis class without any direct interaction with pydantic
itself.

Setup
-----
1. Start by creating your class, which must inherit from
pydantic :code:`BaseModel`.
2. Declare :code:`input_data` as a variable and specify its type. The :mod:`pyprobe.typing`
module has type aliases that may be helpful here. This type should be the most
lenient type that the methods of your analysis class require.

.. literalinclude:: ../../../pyprobe/analysis/differentiation.py
:language: python
:linenos:
:lines: 15-21

3. Some analysis classes have multiple methods that need to pass information to each
other. For instance the :class:`~pyprobe.analysis.degradation_mode_analysis.DMA`
analysis class first calculates stoichiometry limits with the
:func:`~pyprobe.analysis.degradation_mode_analysis.DMA.fit_ocv` method, that are then
used in the :func:`~pyprobe.analysis.degradation_mode_analysis.DMA.quantify_degradation_modes`
method. So, when :func:`~pyprobe.analysis.degradation_mode_analysis.DMA.fit_ocv` is called,
it saves this result in `stoichiometry_limits` for use later. If they are required,
these attributes must also be defined at the top of the class.

.. literalinclude:: ../../../pyprobe/analysis/degradation_mode_analysis.py
:language: python
:linenos:
:lines: 17-31


Then you can add any additional methods to perform your calculations.

Methods
-------
Functions
---------

All calculations should be conducted inside methods. These are called by the user with
any additional information required to perform the analysis, and always return
:class:`~pyprobe.result.Result` objects. We will use the
:func:`~pyprobe.analysis.differentiation.Differentiation.differentiate_FD` method as an example.
:func:`~pyprobe.analysis.differentiation.Differentiation.gradient` method as an example.

It is recommended to use pydantic's `validate_call <https://docs.pydantic.dev/latest/api/validate_call/#pydantic.validate_call_decorator.validate_call>`_
function decorator to ensure that
objects of the correct type are being passed to your method. This provides the user with
an error message if they have not called the method correctly, simplifying debugging.

The steps to write a method are as follows:

1. Define the method and its input parameters.
1. Define the method and its input parameters. One of these is likely to be a PyProBE
object, which you can confirm has the necessary columns for your method with step
2.
2. Check that inputs to the method are valid with the
:class:`~pyprobe.analysis.utils.AnalysisValidator` class. Provide the class the
input data to the method, the columns that are required for the computation to
be performed and the required data type for `input_data`` (only if it is a stricter
requirement than the type assigned to `input_data` above).
be performed and the required data type for `input_data``.
3. If needed, you can retrieve the columns specified in the `required_columns` field
as numpy arrays by accessing the :attr:`~pyprobe.analysis.utils.AnalysisValidator.variables`
attribute of the instance of :class:`~pyprobe.analysis.utils.AnalysisValidator`.
Expand All @@ -77,7 +50,7 @@ The steps to write a method are as follows:
.. literalinclude:: ../../../pyprobe/analysis/differentiation.py
:language: python
:linenos:
:pyobject: Differentiation.differentiate_FD
:pyobject: gradient

Base
----
Expand Down
11 changes: 7 additions & 4 deletions docs/source/developer_guide/dependency_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ The performance of PyProBE is demonstrated in the :doc:`../examples/comparing-py
Pydantic
--------

`Pydantic <https://docs.pydantic.dev/latest/>`_ is used across PyProBE for class input
`Pydantic <https://docs.pydantic.dev/latest/>`_ is used across PyProBE for class and function input
validation. :class:`~pyprobe.result.Result`, :class:`~pyprobe.rawdata.RawData` and
all of the classes in the :mod:`~pyprobe.filters` module inherit from Pydantic
`BaseModel <https://docs.pydantic.dev/latest/api/base_model/>`_, as do all of
the :mod:`~pyprobe.analysis` classes. This means all of their
inputs are type-validated automatically. The :class:`~pyprobe.analysis.utils.AnalysisValidator`
`BaseModel <https://docs.pydantic.dev/latest/api/base_model/>`_. This means all of their
inputs are type-validated automatically.

Functions in the :mod:`~pyprobe.analysis` module use the `validate_call <https://docs.pydantic.dev/latest/api/validate_call/#pydantic.validate_call_decorator.validate_call>`_
decorator, to ensure the arguments passed are of the correct type.
The :class:`~pyprobe.analysis.utils.AnalysisValidator`
is a custom validation model for checking the type and columns are correct for methods
of analysis classes.

Expand Down
55 changes: 11 additions & 44 deletions docs/source/examples/LEAN-differentiation.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We start the analysis by first creating the analysis objects:"
"We can use the methods of the `differentiation` module to calculate the gradients of the data."
]
},
{
Expand All @@ -71,20 +71,11 @@
"metadata": {},
"outputs": [],
"source": [
"from pyprobe.analysis.differentiation import Differentiation\n",
"\n",
"discharge_differentiation = Differentiation(input_data = final_cycle.discharge(0))\n",
"charge_differentiation = Differentiation(input_data = final_cycle.charge(0).constant_current())"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"discharge_dQdV = discharge_differentiation.differentiate_LEAN(x = 'Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"charge_dQdV = charge_differentiation.differentiate_LEAN(x = 'Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"from pyprobe.analysis import differentiation\n",
"discharge_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.discharge(0), \n",
" x = 'Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"charge_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.charge(0).constant_current(), \n",
" x = 'Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(discharge_dQdV, 'Capacity [Ah]', 'd(Capacity [Ah])/d(Voltage [V])', label='Discharge', color='blue')\n",
"fig.add_line(charge_dQdV, 'Capacity [Ah]', 'd(Capacity [Ah])/d(Voltage [V])', label='Charge', color='red')\n",
Expand All @@ -105,8 +96,8 @@
"metadata": {},
"outputs": [],
"source": [
"discharge_dQdV = discharge_differentiation.differentiate_LEAN(x = 'Capacity [mAh]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"charge_dQdV = charge_differentiation.differentiate_LEAN(x = 'Capacity [mAh]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"discharge_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.discharge(0), x = 'Capacity [mAh]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"charge_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.charge(0).constant_current(), x = 'Capacity [mAh]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(discharge_dQdV, 'Capacity [mAh]', 'd(Capacity [mAh])/d(Voltage [V])', label='Discharge', color='blue')\n",
"fig.add_line(charge_dQdV, 'Capacity [mAh]', 'd(Capacity [mAh])/d(Voltage [V])', label='Charge', color='red')\n",
Expand All @@ -127,38 +118,14 @@
"metadata": {},
"outputs": [],
"source": [
"discharge_dQdV = discharge_differentiation.differentiate_LEAN(x = 'Cycle Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"charge_dQdV = charge_differentiation.differentiate_LEAN(x = 'Cycle Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"discharge_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.discharge(0), x = 'Cycle Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"charge_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.charge(0).constant_current(), x = 'Cycle Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(discharge_dQdV, 'Cycle Capacity [Ah]', 'd(Cycle Capacity [Ah])/d(Voltage [V])', label='Discharge', color='blue')\n",
"fig.add_line(charge_dQdV, 'Cycle Capacity [Ah]', 'd(Cycle Capacity [Ah])/d(Voltage [V])', label='Charge', color='red')\n",
"fig.show_image()\n",
"# fig.show() # This will show the plot interactively, it is commented out for the sake of the documentation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The LEAN method is only applicable when the `x` data is uniformly spaced. The example above uses the default method behaviour of differentiating only the longest uniformly spaced period. If your `x` data has multiple uniformly spaced periods, however, you can use this method with the `section = 'all'` option.\n",
"\n",
"The pulse below has two sampling periods:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pulse_rest = cell.procedure['Sample'].experiment('Discharge Pulses').rest(7,8)\n",
"print('dt = ', np.diff(pulse_rest.get('Time [s]')))\n",
"\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(pulse_rest, 'Time [s]', 'Voltage [V]')\n",
"fig.show_image()\n",
"# fig.show() # This will show the plot interactively, it is commented out for the sake of the documentation"
]
}
],
"metadata": {
Expand All @@ -177,7 +144,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.4"
"version": "3.12.3"
}
},
"nbformat": 4,
Expand Down
19 changes: 10 additions & 9 deletions docs/source/examples/analysing-GITT-data.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
"metadata": {},
"outputs": [],
"source": [
"from pyprobe.analysis.pulsing import Pulsing\n",
"pulse_object = Pulsing(input_data=pulsing_experiment)"
"from pyprobe.analysis import pulsing\n",
"pulse_object = pulsing.Pulsing(input_data=pulsing_experiment)"
]
},
{
Expand All @@ -139,7 +139,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We can also extract key parameters from the pulsing experiment, with the `pulse_summary()` method."
"We can also extract key parameters from the pulsing experiment, with the `get_resistances()` method."
]
},
{
Expand All @@ -148,14 +148,15 @@
"metadata": {},
"outputs": [],
"source": [
"print(pulse_object.pulse_summary().data)"
"pulse_resistances = pulsing.get_resistances(input_data=pulsing_experiment)\n",
"print(pulse_resistances.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The pulse summary can take an argument of a list of times at which to evaluate the resistance after the pulse, for instance at 10s after the pulse:"
"The `get_resistances()` method can take an argument of a list of times at which to evaluate the resistance after the pulse, for instance at 10s after the pulse:"
]
},
{
Expand All @@ -164,8 +165,8 @@
"metadata": {},
"outputs": [],
"source": [
"pulse_summary = pulse_object.pulse_summary(r_times=[10])\n",
"print(pulse_summary.data)"
"pulse_resistances = pulsing.get_resistances(input_data=pulsing_experiment, r_times=[10])\n",
"print(pulse_resistances.data)"
]
},
{
Expand All @@ -182,8 +183,8 @@
"outputs": [],
"source": [
"fig = pyprobe.Plot()\n",
"fig.add_line(pulse_summary, 'SOC', 'R0 [Ohms]', color='blue', label='R0')\n",
"fig.add_line(pulse_summary, 'SOC', 'R_10s [Ohms]', color='red', label='R_10s')\n",
"fig.add_line(pulse_resistances, 'SOC', 'R0 [Ohms]', color='blue', label='R0')\n",
"fig.add_line(pulse_resistances, 'SOC', 'R_10s [Ohms]', color='red', label='R_10s')\n",
"fig.yaxis_title = 'Resistance [Ohms]'\n",
"fig.show_image()"
]
Expand Down
43 changes: 19 additions & 24 deletions docs/source/examples/differentiating-voltage-data.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,9 @@
"metadata": {},
"outputs": [],
"source": [
"from pyprobe.analysis.differentiation import Differentiation\n",
"from pyprobe.analysis import differentiation\n",
"\n",
"discharge_differentiation = Differentiation(input_data = final_cycle.discharge(0))\n",
"raw_data_dVdQ = discharge_differentiation.differentiate_FD(\"Capacity [Ah]\", \"Voltage [V]\", gradient='dydx')\n",
"raw_data_dVdQ = differentiation.gradient(final_cycle.discharge(0),\"Capacity [Ah]\", \"Voltage [V]\")\n",
"print(raw_data_dVdQ.column_list)\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(raw_data_dVdQ, 'Voltage [V]', 'd(Voltage [V])/d(Capacity [Ah])')\n",
Expand All @@ -99,17 +98,16 @@
"metadata": {},
"outputs": [],
"source": [
"from pyprobe.analysis.smoothing import Smoothing\n",
"from pyprobe.analysis import smoothing\n",
"\n",
"smoother = Smoothing(input_data = final_cycle.discharge(0))\n",
"\n",
"level_smooothed_data = smoother.level_smoothing(target_column='Voltage [V]',\n",
" interval=0.002,\n",
" monotonic=True,\n",
"downsampled_data = smoothing.downsample(input_data = final_cycle.discharge(0),\n",
" target_column='Voltage [V]',\n",
" sampling_interval=0.002,\n",
" )\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(final_cycle.discharge(0), 'Capacity [Ah]', 'Voltage [V]', label='Raw data', color='red',)\n",
"fig.add_line(level_smooothed_data, 'Capacity [Ah]', 'Voltage [V]', label='Level smoothed data', color='blue', dash='dash')\n",
"# fig.add_line(final_cycle.discharge(0), 'Capacity [Ah]', 'Voltage [V]', label='Raw data', color='red',)\n",
"fig.add_line(downsampled_data, 'Capacity [Ah]', 'Voltage [V]', label='Level smoothed data', color='blue', dash='dash')\n",
"fig.show_image()"
]
},
Expand All @@ -126,11 +124,10 @@
"metadata": {},
"outputs": [],
"source": [
"differentiation_object = Differentiation(input_data = level_smooothed_data)\n",
"level_smoothed_data_dVdQ = differentiation_object.differentiate_FD(\"Capacity [Ah]\", \"Voltage [V]\", gradient='dxdy')\n",
"downsampled_data_dVdQ = differentiation.gradient(downsampled_data, \"Voltage [V]\", \"Capacity [Ah]\")\n",
"\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(level_smoothed_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])')\n",
"fig.add_line(downsampled_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])')\n",
"fig.show_image()"
]
},
Expand All @@ -147,19 +144,17 @@
"metadata": {},
"outputs": [],
"source": [
"smoother = Smoothing(input_data = final_cycle.discharge(0))\n",
"\n",
"spline_smoothed_data = smoother.spline_smoothing(x='Capacity [Ah]',\n",
" target_column='Voltage [V]',\n",
" smoothing_lambda=1e-10,\n",
" )\n",
"differentiation_object = Differentiation(input_data = spline_smoothed_data)\n",
"spline_smoothed_data_dVdQ = differentiation_object.differentiate_FD(\"Capacity [Ah]\", \"Voltage [V]\", gradient='dxdy')\n",
"spline_smoothed_data = smoothing.spline_smoothing(input_data = final_cycle.discharge(0),\n",
" x='Capacity [Ah]',\n",
" target_column='Voltage [V]',\n",
" smoothing_lambda=1e-10,\n",
" ) \n",
"spline_smoothed_data_dVdQ = differentiation.gradient(spline_smoothed_data,\"Voltage [V]\",\"Capacity [Ah]\")\n",
"\n",
"\n",
"\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(level_smoothed_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='Level smoothed data', color='red')\n",
"fig.add_line(downsampled_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='Level smoothed data', color='red')\n",
"fig.add_line(spline_smoothed_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='Spline smoothed data', color='blue')\n",
"fig.show_image()\n"
]
Expand All @@ -177,10 +172,10 @@
"metadata": {},
"outputs": [],
"source": [
"LEAN_dQdV = discharge_differentiation.differentiate_LEAN(x = 'Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"LEAN_dQdV = differentiation.differentiate_LEAN(input_data = final_cycle.discharge(0), x = 'Capacity [Ah]', y='Voltage [V]', k = 10, gradient = 'dxdy')\n",
"\n",
"fig = pyprobe.Plot()\n",
"fig.add_line(level_smoothed_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='Level smoothed data', color='red')\n",
"fig.add_line(downsampled_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='Level smoothed data', color='red')\n",
"fig.add_line(spline_smoothed_data_dVdQ, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='Spline smoothed data', color='blue')\n",
"fig.add_line(LEAN_dQdV, 'Voltage [V]', 'd(Capacity [Ah])/d(Voltage [V])', label='LEAN smoothed data', color='green')\n",
"fig.show_image()"
Expand Down
Loading

0 comments on commit 82da37c

Please sign in to comment.