From a96439ff197020fb55b858a46a58fdaf7d165e02 Mon Sep 17 00:00:00 2001 From: Brad Liang Date: Wed, 28 Feb 2024 18:19:19 -0800 Subject: [PATCH] codestyle (black + black[jupyter]) --- demo/notebook_cyclicvoltammetry.ipynb | 381 +++--- ...otebook_cyclicvoltammetry_peakdetect.ipynb | 505 +++---- demo/notebook_gamry_parser.ipynb | 328 +++-- demo/notebook_potentiostatic_eis.ipynb | 1162 +++++++++-------- gamry_parser/gamryparser.py | 4 +- gamry_parser/version.py | 2 +- gamry_parser/vfp600.py | 8 +- tests/test_chronoamperometry.py | 5 +- tests/test_cyclicvoltammetry.py | 5 +- tests/test_gamryparser.py | 28 +- tests/test_impedance.py | 1 + tests/test_ocp.py | 12 +- tests/test_squarewave.py | 9 +- 13 files changed, 1254 insertions(+), 1196 deletions(-) diff --git a/demo/notebook_cyclicvoltammetry.ipynb b/demo/notebook_cyclicvoltammetry.ipynb index 7a82e7d..0ab9bf2 100644 --- a/demo/notebook_cyclicvoltammetry.ipynb +++ b/demo/notebook_cyclicvoltammetry.ipynb @@ -1,194 +1,193 @@ { - "nbformat": 4, - "nbformat_minor": 2, - "metadata": { - "colab": { - "name": "Gamry-Parser CyclicVoltammetry Example", - "version": "0.3.2", - "provenance": [], - "private_outputs": true, - "collapsed_sections": [] - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - } + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "colab": { + "name": "Gamry-Parser CyclicVoltammetry Example", + "version": "0.3.2", + "provenance": [], + "private_outputs": true, + "collapsed_sections": [] }, - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Imports, initial setup (Ctrl+F9 to run all)\n", - "from google.colab import files \n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "\n", - "try:\n", - " import gamry_parser\n", - "except:\n", - " subprocess.run(\n", - " [\"pip\", \"install\", \"gamry-parser\"], \n", - " encoding=\"utf-8\", \n", - " shell=False)\n", - "finally:\n", - " import gamry_parser\n", - "\n", - "p = parser.CyclicVoltammetry()\n", - " \n", - "print('Done.')" - ], - "outputs": [], - "metadata": { - "id": "WF08aBjO8Lvh", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "\"\"\"\n", - "### SCRIPT CONFIGURATION SETTINGS ###\n", - "\"\"\"\n", - "\n", - "\"\"\"\n", - "DATA SOURCE\n", - "\"\"\"\n", - "upload_files = True\n", - "\n", - "\"\"\"\n", - "PLOTTING\n", - "Plots are generated in the notebook. They are not saved / exported.\n", - "\"\"\"\n", - "show_plots = True # do we want to show analysis plots in this notebook?\n", - "compare_curves = 3 # compare a specific curve across files\n", - "\n", - "\n", - "print('Done.')" - ], - "outputs": [], - "metadata": { - "id": "qPt8TDQgA0h6", - "colab_type": "code", - "cellView": "code", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title DTA File\n", - "if upload_files:\n", - " experiment = files.upload()\n", - "else:\n", - " !wget -c https://raw.githubusercontent.com/bcliang/gamry-parser/master/tests/cv_data.dta\n", - " experiment = [\"cv_data.dta\"]\n", - "\n" - ], - "outputs": [], - "metadata": { - "id": "3cK_P2Clmksm", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Load and Plot Curve Data\n", - "\n", - "for f in experiment:\n", - " p.load(f)\n", - " \n", - " # generate a plot based on the first curve listed in the file.\n", - " data = p.curve(p.curve_count) \n", - " \n", - " # print to screen\n", - " print('Data Preview: Last Curve')\n", - " print(data.iloc[:5])\n", - " \n", - " # matplotlib fig\n", - " if show_plots:\n", - " fig, ax = plt.subplots(figsize=(18,8))\n", - " for i in range(p.curve_count):\n", - " data = p.curve(i)\n", - " trace = ax.plot(data['Vf'], data['Im']*1e6, label=\"curve {}\".format(i))\n", - " \n", - " ax.set_title(\"{}, {} curves\".format(f, p.curve_count), fontsize=18)\n", - " ax.set_xlabel('Potential (V)')\n", - " ax.set_ylabel('Current (A)', fontsize=14)\n", - " plt.show()" - ], - "outputs": [], - "metadata": { - "id": "mxwEyYWICCYs", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Compare a specific curve across loaded files\n", - "\n", - "fig, ax = plt.subplots(figsize=(18,8))\n", - " \n", - "for f in experiment:\n", - " p.load(f)\n", - " \n", - " # generate a plot based on the first curve listed in the file.\n", - " if p.curve_count > compare_curves:\n", - " data = p.curve(compare_curves) \n", - " trace = ax.plot(data['Vf'], data['Im']*1e6, label=\"file {}\".format(f))\n", - " \n", - "ax.set_title(\"CyclicVoltammetry Test, Compare Curve #{}\".format(compare_curves), fontsize=18)\n", - "ax.set_xlabel('Potential (V)')\n", - "ax.set_ylabel('Current (A)', fontsize=14)\n", - "ax.legend()\n", - "plt.show()" - ], - "outputs": [], - "metadata": { - "id": "AQc4jnhlURDV", - "colab_type": "code", - "colab": {}, - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Download All Curves, All Experiments\n", - "\n", - "aggreg = pd.DataFrame()\n", - "\n", - "for f in experiment:\n", - " p.load(f)\n", - " \n", - " # use the curves @property to retrieve all curve data\n", - " for df in p.curves:\n", - " aggreg = aggreg.append(df)\n", - " \n", - " \n", - "aggreg.to_csv('results.csv')\n", - "files.download('results.csv')" - ], - "outputs": [], - "metadata": { - "id": "ZPXwezuvmgZ0", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - } - ] + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Imports, initial setup (Ctrl+F9 to run all)\n", + "from google.colab import files\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "try:\n", + " import gamry_parser\n", + "except:\n", + " subprocess.run([\"pip\", \"install\", \"gamry-parser\"], encoding=\"utf-8\", shell=False)\n", + "finally:\n", + " import gamry_parser\n", + "\n", + "p = parser.CyclicVoltammetry()\n", + "\n", + "print(\"Done.\")" + ], + "outputs": [], + "metadata": { + "id": "WF08aBjO8Lvh", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "\"\"\"\n", + "### SCRIPT CONFIGURATION SETTINGS ###\n", + "\"\"\"\n", + "\n", + "\"\"\"\n", + "DATA SOURCE\n", + "\"\"\"\n", + "upload_files = True\n", + "\n", + "\"\"\"\n", + "PLOTTING\n", + "Plots are generated in the notebook. They are not saved / exported.\n", + "\"\"\"\n", + "show_plots = True # do we want to show analysis plots in this notebook?\n", + "compare_curves = 3 # compare a specific curve across files\n", + "\n", + "\n", + "print(\"Done.\")" + ], + "outputs": [], + "metadata": { + "id": "qPt8TDQgA0h6", + "colab_type": "code", + "cellView": "code", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "#@title DTA File\n", + "if upload_files:\n", + " experiment = files.upload()\n", + "else:\n", + " !wget -c https://raw.githubusercontent.com/bcliang/gamry-parser/master/tests/cv_data.dta\n", + " experiment = [\"cv_data.dta\"]\n", + "\n" + ], + "outputs": [], + "metadata": { + "id": "3cK_P2Clmksm", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Load and Plot Curve Data\n", + "\n", + "for f in experiment:\n", + " p.load(f)\n", + "\n", + " # generate a plot based on the first curve listed in the file.\n", + " data = p.curve(p.curve_count)\n", + "\n", + " # print to screen\n", + " print(\"Data Preview: Last Curve\")\n", + " print(data.iloc[:5])\n", + "\n", + " # matplotlib fig\n", + " if show_plots:\n", + " fig, ax = plt.subplots(figsize=(18, 8))\n", + " for i in range(p.curve_count):\n", + " data = p.curve(i)\n", + " trace = ax.plot(data[\"Vf\"], data[\"Im\"] * 1e6, label=\"curve {}\".format(i))\n", + "\n", + " ax.set_title(\"{}, {} curves\".format(f, p.curve_count), fontsize=18)\n", + " ax.set_xlabel(\"Potential (V)\")\n", + " ax.set_ylabel(\"Current (A)\", fontsize=14)\n", + " plt.show()" + ], + "outputs": [], + "metadata": { + "id": "mxwEyYWICCYs", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Compare a specific curve across loaded files\n", + "\n", + "fig, ax = plt.subplots(figsize=(18, 8))\n", + "\n", + "for f in experiment:\n", + " p.load(f)\n", + "\n", + " # generate a plot based on the first curve listed in the file.\n", + " if p.curve_count > compare_curves:\n", + " data = p.curve(compare_curves)\n", + " trace = ax.plot(data[\"Vf\"], data[\"Im\"] * 1e6, label=\"file {}\".format(f))\n", + "\n", + "ax.set_title(\n", + " \"CyclicVoltammetry Test, Compare Curve #{}\".format(compare_curves), fontsize=18\n", + ")\n", + "ax.set_xlabel(\"Potential (V)\")\n", + "ax.set_ylabel(\"Current (A)\", fontsize=14)\n", + "ax.legend()\n", + "plt.show()" + ], + "outputs": [], + "metadata": { + "id": "AQc4jnhlURDV", + "colab_type": "code", + "colab": {}, + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Download All Curves, All Experiments\n", + "\n", + "aggreg = pd.DataFrame()\n", + "\n", + "for f in experiment:\n", + " p.load(f)\n", + "\n", + " # use the curves @property to retrieve all curve data\n", + " for df in p.curves:\n", + " aggreg = aggreg.append(df)\n", + "\n", + "\n", + "aggreg.to_csv(\"results.csv\")\n", + "files.download(\"results.csv\")" + ], + "outputs": [], + "metadata": { + "id": "ZPXwezuvmgZ0", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + } + ] } \ No newline at end of file diff --git a/demo/notebook_cyclicvoltammetry_peakdetect.ipynb b/demo/notebook_cyclicvoltammetry_peakdetect.ipynb index 87f3309..2c51660 100644 --- a/demo/notebook_cyclicvoltammetry_peakdetect.ipynb +++ b/demo/notebook_cyclicvoltammetry_peakdetect.ipynb @@ -1,253 +1,258 @@ { - "nbformat": 4, - "nbformat_minor": 2, - "metadata": { - "colab": { - "name": "Gamry-Parser CyclicVoltammetry Peak Detection Example", - "provenance": [], - "collapsed_sections": [] - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - } + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "colab": { + "name": "Gamry-Parser CyclicVoltammetry Peak Detection Example", + "provenance": [], + "collapsed_sections": [] }, - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Imports, initial setup (Ctrl+F9 to run all)\n", - "import os\n", - "import re\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "from scipy.signal import find_peaks\n", - "import copy\n", - "\n", - "try:\n", - " import gamry_parser\n", - "except:\n", - " subprocess.run(\n", - " [\"pip\", \"install\", \"gamry-parser\"], \n", - " encoding=\"utf-8\", \n", - " shell=False)\n", - "finally:\n", - " import gamry_parser\n", - "\n", - "gp = gamry_parser.CyclicVoltammetry()\n", - "\n", - "print('Done.')" - ], - "outputs": [], - "metadata": { - "id": "YyeVlSYkhahF", - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "\"\"\"\n", - "### SCRIPT CONFIGURATION SETTINGS ###\n", - "\"\"\"\n", - "#@markdown **Experimental Setup**\n", - "\n", - "#@markdown Where should the notebook search for DTA files? Examples (using google colab):\n", - "#@markdown - Mounted google drive folder: `/content/drive/`\n", - "#@markdown - If uploading files manually, : `/content/`).\n", - "\n", - "data_path = \"/content/\" #@param {type:\"string\"}\n", - "\n", - "#@markdown Filter which files we want to analyze\n", - "file_pattern = \"Search-For-Text\" #@param {type:\"string\"}\n", - "\n", - "#@markdown Extract trace labels from file name (e.g. `[17:].lower()` => drop the first 17 characters from the filename and convert to lowercase). The trace labels are used for category labeling (and plot legends)\n", - "file_label_xform = \"[51:]\" #@param {type:\"string\"}\n", - "\n", - "# create a \"results\" dataframe to contain the values we care about\n", - "data_df = pd.DataFrame()\n", - "settings_df = pd.DataFrame()\n", - "peaks_df = pd.DataFrame()\n", - "\n", - "# identify files to process\n", - "files = [f for f in os.listdir(data_path) if \n", - " os.path.splitext(f)[1].lower() == \".dta\" and\n", - " len(re.findall(file_pattern.upper(), f.upper())) > 0\n", - " ]\n" - ], - "outputs": [], - "metadata": { - "id": "ZGoqracvk9q2", - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@markdown **Process Data and Detect Peaks**\n", - "\n", - "#@markdown Which CV curves (cycle number) should be sampled? (`0` would select the first CV curve from each file)\n", - "curves_to_sample = \"0\" #@param {type:\"string\"}\n", - "curves_to_sample = [int(item.strip()) for item in curves_to_sample.split(\",\")]\n", - "\n", - "#@markdown Peak Detection: specify the peak detection parameters\n", - "peak_width_mV = 75 #@param {type:\"integer\"}\n", - "peak_height_nA = 25 #@param {type:\"integer\"}\n", - "peak_thresh_max_mV = 800 #@param {type:\"integer\"}\n", - "peak_thresh_min_mV = -100 #@param {type:\"integer\"}\n", - "\n", - "# this method finds the row that has an index value closest to the desired time elapsed\n", - "def duration_lookup(df, elapsed):\n", - " return df.index.get_loc(elapsed, method='nearest')\n", - "\n", - "# iterate through each DTA file\n", - "for index, file in enumerate(files):\n", - " print(\"Checking File {}\".format(file))\n", - "\n", - " label, ext = os.path.splitext(file)\n", - " my_label = \"-\".join(eval(\"label{}\".format(file_label_xform)).strip().split())\n", - "\n", - " # load the dta file using gamry parser\n", - " gp.load(filename=os.path.join(data_path, file))\n", - "\n", - " is_cv = gp.experiment_type == \"CV\"\n", - " if not is_cv:\n", - " # if the DTA file is a different experiment type, skip it and move to the next file.\n", - " print(\"File `{}` is not a CV experiment. Skipping\".format(file))\n", - " del files[index] # remove invalid file from list\n", - " continue\n", - " \n", - " # for each CV file, let's extract the relevant information\n", - " cv = gamry_parser.CyclicVoltammetry(filename=os.path.join(data_path, file))\n", - " cv.load()\n", - " for curve_num in curves_to_sample:\n", - " print(\"\\tProcessing Curve #{}\".format(curve_num))\n", - " v1, v2 = cv.v_range\n", - " settings = pd.DataFrame({\n", - " \"label\": my_label,\n", - " \"curves\": cv.curve_count,\n", - " \"v1_mV\": v1*1000,\n", - " \"v2_mV\": v2*1000,\n", - " \"rate_mV\": cv.scan_rate,\n", - " }, index=[0])\n", - " settings_df = settings_df.append(settings)\n", - "\n", - " data = copy.deepcopy(cv.curve(curve=curve_num))\n", - " data.Im = data.Im*1e9\n", - " data.Vf = data.Vf*1e3\n", - " data[\"label\"] = my_label #\"{:03d}-{}\".format(index, curve_num)\n", - "\n", - " data_df = data_df.append(data)\n", - "\n", - " # find peaks in the data\n", - " dV = cv.scan_rate # in mV\n", - " peak_width = int(peak_width_mV/dV)\n", - " peaks_pos, props_pos = find_peaks(\n", - " data.Im, \n", - " width=peak_width, \n", - " distance=2*peak_width, \n", - " height=peak_height_nA\n", - " )\n", - " peaks_neg, props_neg = find_peaks(\n", - " -data.Im, \n", - " width=peak_width, \n", - " distance=2*peak_width, \n", - " height=peak_height_nA\n", - " )\n", - " peaks = list(peaks_pos) + list(peaks_neg)\n", - " # remove peaks that are out of min/max range\n", - " peaks = [peak \n", - " for peak in peaks \n", - " if data.Vf.iloc[peak] >= peak_thresh_min_mV and data.Vf.iloc[peak] <= peak_thresh_max_mV]\n", - "\n", - " # add detected peaks to aggregated peak dataframe\n", - " peaks = data.iloc[peaks].sort_values(by=\"Vf\")\n", - " peaks[\"index\"] = peaks.index\n", - " peaks.reset_index(level=0, inplace=True)\n", - " peaks_df = peaks_df.append(peaks)\n", - " peaks_df = peaks_df[[\"label\", \"index\", \"Vf\", \"Im\"]]\n", - " # print(\"\\tdetected peaks (mV)\", [int(peak) for peak in data.iloc[peaks].Vf.sort_values().tolist()])\n", - "\n", - "print(\"\\nFile Metadata\")\n", - "print(settings_df.to_string(index=False))\n", - "\n", - "print(\"\\nPeaks Detected\")\n", - "print(peaks_df.to_string(index=False))" - ], - "outputs": [], - "metadata": { - "cellView": "form", - "id": "8MFNF2Qz6lef" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@markdown **I-V plot**: Overlay the loaded CyclicVoltammetry Curves\n", - "\n", - "from plotly.subplots import make_subplots\n", - "import plotly.graph_objects as go\n", - "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", - "\n", - "fig = make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", - "\n", - "for (index, exp_id) in enumerate(data_df.label.unique()):\n", - " data = data_df.loc[data_df.label == exp_id]\n", - " newTrace = go.Scatter(\n", - " x=data.Vf,\n", - " y=data.Im,\n", - " mode='lines',\n", - " name=exp_id,\n", - " legendgroup=files[index],\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[index]),\n", - " )\n", - " fig.add_trace(newTrace, row=1, col=1)\n", - " peak = peaks_df.loc[peaks_df.label == exp_id]\n", - " newTrace = go.Scatter(\n", - " x=peak.Vf, y=peak.Im, \n", - " mode=\"markers\", \n", - " showlegend=False, \n", - " marker=dict(size=12,\n", - " color=DEFAULT_PLOTLY_COLORS[index],\n", - " )\n", - " )\n", - " fig.add_trace(newTrace, row=1, col=1)\n", - "\n", - "layout = {\n", - " 'title': {'text': 'Cyclic Voltammetry Overlay',\n", - " 'yanchor': 'top',\n", - " 'y': 0.95,\n", - " 'x': 0.5 },\n", - " 'xaxis': {\n", - " 'anchor': 'x',\n", - " 'title': 'voltage, mV'\n", - " },\n", - " 'yaxis': {\n", - " 'title': 'current, nA',\n", - " 'type': 'linear'\n", - " ''\n", - " },\n", - " 'width': 1200,\n", - " 'height': 500,\n", - " 'margin': dict(l=30, r=20, t=60, b=20),\n", - "}\n", - "fig.update_layout(layout)\n", - "\n", - "config={\n", - " 'displaylogo': False,\n", - " 'modeBarButtonsToRemove': ['select2d', 'lasso2d', 'hoverClosestCartesian', 'toggleSpikelines','hoverCompareCartesian']\n", - "}\n", - "fig.show(config=config)" - ], - "outputs": [], - "metadata": { - "id": "Ulne80RrpBrW", - "cellView": "form" - } - } - ] + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Imports, initial setup (Ctrl+F9 to run all)\n", + "import os\n", + "import re\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from scipy.signal import find_peaks\n", + "import copy\n", + "\n", + "try:\n", + " import gamry_parser\n", + "except:\n", + " subprocess.run([\"pip\", \"install\", \"gamry-parser\"], encoding=\"utf-8\", shell=False)\n", + "finally:\n", + " import gamry_parser\n", + "\n", + "gp = gamry_parser.CyclicVoltammetry()\n", + "\n", + "print(\"Done.\")" + ], + "outputs": [], + "metadata": { + "id": "YyeVlSYkhahF", + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "\"\"\"\n", + "### SCRIPT CONFIGURATION SETTINGS ###\n", + "\"\"\"\n", + "\n", + "# @markdown **Experimental Setup**\n", + "\n", + "# @markdown Where should the notebook search for DTA files? Examples (using google colab):\n", + "# @markdown - Mounted google drive folder: `/content/drive/`\n", + "# @markdown - If uploading files manually, : `/content/`).\n", + "\n", + "data_path = \"/content/\" # @param {type:\"string\"}\n", + "\n", + "# @markdown Filter which files we want to analyze\n", + "file_pattern = \"Search-For-Text\" # @param {type:\"string\"}\n", + "\n", + "# @markdown Extract trace labels from file name (e.g. `[17:].lower()` => drop the first 17 characters from the filename and convert to lowercase). The trace labels are used for category labeling (and plot legends)\n", + "file_label_xform = \"[51:]\" # @param {type:\"string\"}\n", + "\n", + "# create a \"results\" dataframe to contain the values we care about\n", + "data_df = pd.DataFrame()\n", + "settings_df = pd.DataFrame()\n", + "peaks_df = pd.DataFrame()\n", + "\n", + "# identify files to process\n", + "files = [\n", + " f\n", + " for f in os.listdir(data_path)\n", + " if os.path.splitext(f)[1].lower() == \".dta\"\n", + " and len(re.findall(file_pattern.upper(), f.upper())) > 0\n", + "]" + ], + "outputs": [], + "metadata": { + "id": "ZGoqracvk9q2", + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @markdown **Process Data and Detect Peaks**\n", + "\n", + "# @markdown Which CV curves (cycle number) should be sampled? (`0` would select the first CV curve from each file)\n", + "curves_to_sample = \"0\" # @param {type:\"string\"}\n", + "curves_to_sample = [int(item.strip()) for item in curves_to_sample.split(\",\")]\n", + "\n", + "# @markdown Peak Detection: specify the peak detection parameters\n", + "peak_width_mV = 75 # @param {type:\"integer\"}\n", + "peak_height_nA = 25 # @param {type:\"integer\"}\n", + "peak_thresh_max_mV = 800 # @param {type:\"integer\"}\n", + "peak_thresh_min_mV = -100 # @param {type:\"integer\"}\n", + "\n", + "\n", + "# this method finds the row that has an index value closest to the desired time elapsed\n", + "def duration_lookup(df, elapsed):\n", + " return df.index.get_loc(elapsed, method=\"nearest\")\n", + "\n", + "\n", + "# iterate through each DTA file\n", + "for index, file in enumerate(files):\n", + " print(\"Checking File {}\".format(file))\n", + "\n", + " label, ext = os.path.splitext(file)\n", + " my_label = \"-\".join(eval(\"label{}\".format(file_label_xform)).strip().split())\n", + "\n", + " # load the dta file using gamry parser\n", + " gp.load(filename=os.path.join(data_path, file))\n", + "\n", + " is_cv = gp.experiment_type == \"CV\"\n", + " if not is_cv:\n", + " # if the DTA file is a different experiment type, skip it and move to the next file.\n", + " print(\"File `{}` is not a CV experiment. Skipping\".format(file))\n", + " del files[index] # remove invalid file from list\n", + " continue\n", + "\n", + " # for each CV file, let's extract the relevant information\n", + " cv = gamry_parser.CyclicVoltammetry(filename=os.path.join(data_path, file))\n", + " cv.load()\n", + " for curve_num in curves_to_sample:\n", + " print(\"\\tProcessing Curve #{}\".format(curve_num))\n", + " v1, v2 = cv.v_range\n", + " settings = pd.DataFrame(\n", + " {\n", + " \"label\": my_label,\n", + " \"curves\": cv.curve_count,\n", + " \"v1_mV\": v1 * 1000,\n", + " \"v2_mV\": v2 * 1000,\n", + " \"rate_mV\": cv.scan_rate,\n", + " },\n", + " index=[0],\n", + " )\n", + " settings_df = settings_df.append(settings)\n", + "\n", + " data = copy.deepcopy(cv.curve(curve=curve_num))\n", + " data.Im = data.Im * 1e9\n", + " data.Vf = data.Vf * 1e3\n", + " data[\"label\"] = my_label # \"{:03d}-{}\".format(index, curve_num)\n", + "\n", + " data_df = data_df.append(data)\n", + "\n", + " # find peaks in the data\n", + " dV = cv.scan_rate # in mV\n", + " peak_width = int(peak_width_mV / dV)\n", + " peaks_pos, props_pos = find_peaks(\n", + " data.Im, width=peak_width, distance=2 * peak_width, height=peak_height_nA\n", + " )\n", + " peaks_neg, props_neg = find_peaks(\n", + " -data.Im, width=peak_width, distance=2 * peak_width, height=peak_height_nA\n", + " )\n", + " peaks = list(peaks_pos) + list(peaks_neg)\n", + " # remove peaks that are out of min/max range\n", + " peaks = [\n", + " peak\n", + " for peak in peaks\n", + " if data.Vf.iloc[peak] >= peak_thresh_min_mV\n", + " and data.Vf.iloc[peak] <= peak_thresh_max_mV\n", + " ]\n", + "\n", + " # add detected peaks to aggregated peak dataframe\n", + " peaks = data.iloc[peaks].sort_values(by=\"Vf\")\n", + " peaks[\"index\"] = peaks.index\n", + " peaks.reset_index(level=0, inplace=True)\n", + " peaks_df = peaks_df.append(peaks)\n", + " peaks_df = peaks_df[[\"label\", \"index\", \"Vf\", \"Im\"]]\n", + " # print(\"\\tdetected peaks (mV)\", [int(peak) for peak in data.iloc[peaks].Vf.sort_values().tolist()])\n", + "\n", + "print(\"\\nFile Metadata\")\n", + "print(settings_df.to_string(index=False))\n", + "\n", + "print(\"\\nPeaks Detected\")\n", + "print(peaks_df.to_string(index=False))" + ], + "outputs": [], + "metadata": { + "cellView": "form", + "id": "8MFNF2Qz6lef" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @markdown **I-V plot**: Overlay the loaded CyclicVoltammetry Curves\n", + "\n", + "from plotly.subplots import make_subplots\n", + "import plotly.graph_objects as go\n", + "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", + "\n", + "fig = make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", + "\n", + "for index, exp_id in enumerate(data_df.label.unique()):\n", + " data = data_df.loc[data_df.label == exp_id]\n", + " newTrace = go.Scatter(\n", + " x=data.Vf,\n", + " y=data.Im,\n", + " mode=\"lines\",\n", + " name=exp_id,\n", + " legendgroup=files[index],\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[index]),\n", + " )\n", + " fig.add_trace(newTrace, row=1, col=1)\n", + " peak = peaks_df.loc[peaks_df.label == exp_id]\n", + " newTrace = go.Scatter(\n", + " x=peak.Vf,\n", + " y=peak.Im,\n", + " mode=\"markers\",\n", + " showlegend=False,\n", + " marker=dict(\n", + " size=12,\n", + " color=DEFAULT_PLOTLY_COLORS[index],\n", + " ),\n", + " )\n", + " fig.add_trace(newTrace, row=1, col=1)\n", + "\n", + "layout = {\n", + " \"title\": {\n", + " \"text\": \"Cyclic Voltammetry Overlay\",\n", + " \"yanchor\": \"top\",\n", + " \"y\": 0.95,\n", + " \"x\": 0.5,\n", + " },\n", + " \"xaxis\": {\"anchor\": \"x\", \"title\": \"voltage, mV\"},\n", + " \"yaxis\": {\"title\": \"current, nA\", \"type\": \"linear\" \"\"},\n", + " \"width\": 1200,\n", + " \"height\": 500,\n", + " \"margin\": dict(l=30, r=20, t=60, b=20),\n", + "}\n", + "fig.update_layout(layout)\n", + "\n", + "config = {\n", + " \"displaylogo\": False,\n", + " \"modeBarButtonsToRemove\": [\n", + " \"select2d\",\n", + " \"lasso2d\",\n", + " \"hoverClosestCartesian\",\n", + " \"toggleSpikelines\",\n", + " \"hoverCompareCartesian\",\n", + " ],\n", + "}\n", + "fig.show(config=config)" + ], + "outputs": [], + "metadata": { + "id": "Ulne80RrpBrW", + "cellView": "form" + } + } + ] } \ No newline at end of file diff --git a/demo/notebook_gamry_parser.ipynb b/demo/notebook_gamry_parser.ipynb index 1da0304..539f973 100644 --- a/demo/notebook_gamry_parser.ipynb +++ b/demo/notebook_gamry_parser.ipynb @@ -1,169 +1,165 @@ { - "nbformat": 4, - "nbformat_minor": 2, - "metadata": { - "colab": { - "name": "Gamry-Parser GamryParser Example", - "version": "0.3.2", - "provenance": [], - "private_outputs": true, - "collapsed_sections": [] - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - } + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "colab": { + "name": "Gamry-Parser GamryParser Example", + "version": "0.3.2", + "provenance": [], + "private_outputs": true, + "collapsed_sections": [] }, - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Imports, initial setup (Ctrl+F9 to run all)\n", - "from google.colab import files \n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "\n", - "try:\n", - " import gamry_parser\n", - "except:\n", - " subprocess.run(\n", - " [\"pip\", \"install\", \"gamry-parser\"], \n", - " encoding=\"utf-8\", \n", - " shell=False)\n", - "finally:\n", - " import gamry_parser\n", - "\n", - "p = parser.GamryParser()\n", - " \n", - "print('Done.')" - ], - "outputs": [], - "metadata": { - "id": "WF08aBjO8Lvh", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "\"\"\"\n", - "### SCRIPT CONFIGURATION SETTINGS ###\n", - "\"\"\"\n", - "\n", - "\"\"\"\n", - "DATA SOURCE\n", - "\"\"\"\n", - "upload_files = True\n", - "\n", - "\"\"\"\n", - "PLOTTING\n", - "Plots are generated in the notebook. They are not saved / exported.\n", - "\"\"\"\n", - "show_plots = True # do we want to show analysis plots in this notebook?\n", - "current_min = 0 # in figures, what is the y-axis minimum\n", - "current_max = 25e-9 # in figures, what is the y-axis maximum\n", - "\n", - "print('Done.')" - ], - "outputs": [], - "metadata": { - "id": "qPt8TDQgA0h6", - "colab_type": "code", - "cellView": "code", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title DTA File\n", - "if upload_files:\n", - " experiment = files.upload()\n", - "else:\n", - " !wget -c https://raw.githubusercontent.com/bcliang/gamry-parser/master/tests/chronoa_data.dta\n", - " experiment = [\"chronoa_data.dta\"]\n", - "\n" - ], - "outputs": [], - "metadata": { - "id": "3cK_P2Clmksm", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Load and Plot Final Curve\n", - "\n", - "for f in experiment:\n", - " p.load(f)\n", - " \n", - " # generate a plot based on the first curve listed in the file.\n", - " curve_count = p.curve_count\n", - " data = p.curve(curve_count) \n", - " \n", - " # print to screen\n", - " print('Display Curve #{} first 5 rows...'.format(curve_count))\n", - " print(data.iloc[:5])\n", - " \n", - " # matplotlib fig\n", - " if show_plots:\n", - " print('\\nPlotting..')\n", - " fig, ax = plt.subplots(figsize=(18,8))\n", - " \n", - " axis = plt.subplot(211)\n", - " plt.plot(data['T'], data['Vf'])\n", - " axis.set_title(\"{}, Curve #{}\".format(f, curve_count), fontsize=18)\n", - " axis.set_ylabel('Vf')\n", - " axis.set_xlabel('Time (s)')\n", - " \n", - " axis = plt.subplot(212)\n", - " plt.plot(data['T'], data['Im'])\n", - " axis.set_ylabel('Current', fontsize=14)\n", - " \n", - " plt.show()\n", - " " - ], - "outputs": [], - "metadata": { - "id": "mxwEyYWICCYs", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Download All Curves, All Experiments\n", - "\n", - "aggreg = pd.DataFrame()\n", - "\n", - "for f in experiment:\n", - " p.load(f)\n", - " # use the curves @property to retrieve all curves\n", - " for df in p.curves:\n", - " aggreg.append(df)\n", - "\n", - "aggreg.to_csv('results.csv')\n", - "files.download('results.csv')" - ], - "outputs": [], - "metadata": { - "id": "ZPXwezuvmgZ0", - "colab_type": "code", - "cellView": "form", - "colab": {} - } - } - ] + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Imports, initial setup (Ctrl+F9 to run all)\n", + "from google.colab import files\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "try:\n", + " import gamry_parser\n", + "except:\n", + " subprocess.run([\"pip\", \"install\", \"gamry-parser\"], encoding=\"utf-8\", shell=False)\n", + "finally:\n", + " import gamry_parser\n", + "\n", + "p = parser.GamryParser()\n", + "\n", + "print(\"Done.\")" + ], + "outputs": [], + "metadata": { + "id": "WF08aBjO8Lvh", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "\"\"\"\n", + "### SCRIPT CONFIGURATION SETTINGS ###\n", + "\"\"\"\n", + "\n", + "\"\"\"\n", + "DATA SOURCE\n", + "\"\"\"\n", + "upload_files = True\n", + "\n", + "\"\"\"\n", + "PLOTTING\n", + "Plots are generated in the notebook. They are not saved / exported.\n", + "\"\"\"\n", + "show_plots = True # do we want to show analysis plots in this notebook?\n", + "current_min = 0 # in figures, what is the y-axis minimum\n", + "current_max = 25e-9 # in figures, what is the y-axis maximum\n", + "\n", + "print(\"Done.\")" + ], + "outputs": [], + "metadata": { + "id": "qPt8TDQgA0h6", + "colab_type": "code", + "cellView": "code", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "#@title DTA File\n", + "if upload_files:\n", + " experiment = files.upload()\n", + "else:\n", + " !wget -c https://raw.githubusercontent.com/bcliang/gamry-parser/master/tests/chronoa_data.dta\n", + " experiment = [\"chronoa_data.dta\"]\n", + "\n" + ], + "outputs": [], + "metadata": { + "id": "3cK_P2Clmksm", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Load and Plot Final Curve\n", + "\n", + "for f in experiment:\n", + " p.load(f)\n", + "\n", + " # generate a plot based on the first curve listed in the file.\n", + " curve_count = p.curve_count\n", + " data = p.curve(curve_count)\n", + "\n", + " # print to screen\n", + " print(\"Display Curve #{} first 5 rows...\".format(curve_count))\n", + " print(data.iloc[:5])\n", + "\n", + " # matplotlib fig\n", + " if show_plots:\n", + " print(\"\\nPlotting..\")\n", + " fig, ax = plt.subplots(figsize=(18, 8))\n", + "\n", + " axis = plt.subplot(211)\n", + " plt.plot(data[\"T\"], data[\"Vf\"])\n", + " axis.set_title(\"{}, Curve #{}\".format(f, curve_count), fontsize=18)\n", + " axis.set_ylabel(\"Vf\")\n", + " axis.set_xlabel(\"Time (s)\")\n", + "\n", + " axis = plt.subplot(212)\n", + " plt.plot(data[\"T\"], data[\"Im\"])\n", + " axis.set_ylabel(\"Current\", fontsize=14)\n", + "\n", + " plt.show()" + ], + "outputs": [], + "metadata": { + "id": "mxwEyYWICCYs", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Download All Curves, All Experiments\n", + "\n", + "aggreg = pd.DataFrame()\n", + "\n", + "for f in experiment:\n", + " p.load(f)\n", + " # use the curves @property to retrieve all curves\n", + " for df in p.curves:\n", + " aggreg.append(df)\n", + "\n", + "aggreg.to_csv(\"results.csv\")\n", + "files.download(\"results.csv\")" + ], + "outputs": [], + "metadata": { + "id": "ZPXwezuvmgZ0", + "colab_type": "code", + "cellView": "form", + "colab": {} + } + } + ] } \ No newline at end of file diff --git a/demo/notebook_potentiostatic_eis.ipynb b/demo/notebook_potentiostatic_eis.ipynb index 648957a..76ebb53 100644 --- a/demo/notebook_potentiostatic_eis.ipynb +++ b/demo/notebook_potentiostatic_eis.ipynb @@ -1,569 +1,599 @@ { - "nbformat": 4, - "nbformat_minor": 2, - "metadata": { - "colab": { - "name": "Gamry-Parser Potentiostatic EIS Example", - "provenance": [], - "collapsed_sections": [ - "OaPK3VEDxG2y" - ], - "toc_visible": true - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - } + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "colab": { + "name": "Gamry-Parser Potentiostatic EIS Example", + "provenance": [], + "collapsed_sections": [ + "OaPK3VEDxG2y" + ], + "toc_visible": true }, - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@title Imports, initial setup (Ctrl+F9 to run all)\n", - "try:\n", - " import gamry_parser\n", - "except:\n", - " subprocess.run(\n", - " [\"pip\", \"install\", \"gamry-parser\"], \n", - " encoding=\"utf-8\", \n", - " shell=False)\n", - "finally:\n", - " import gamry_parser\n", - "\n", - "import os\n", - "import re\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "\n", - "z = gamry_parser.Impedance()\n", - "\n", - "print('Done.')" - ], - "outputs": [], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "\"\"\"\n", - "### SCRIPT CONFIGURATION SETTINGS ###\n", - "\"\"\"\n", - "#@markdown **Load data** Parse Gamry *.DTA files from folder\n", - "\n", - "#@markdown Where are the Gamry DTA files located?\n", - "file_path = \"/path/to/gamry/files/\" #@param {type:\"string\"}\n", - "\n", - "#@markdown What's the title of this experiment?\n", - "experiment_name = \"EIS Experimentt 1\" #@param {type:\"string\"}\n", - "\n", - "#@markdown Which of the DTA files do we want to compare? (Regular expression matching)\n", - "file_pattern = \"EIS\" #@param {type:\"string\"}\n", - "\n", - "#@markdown Which impedance frequencies should be shown? (separated by comma, e.g. `4, 1000, 10000`)\n", - "frequencies_to_show = \"1, 5, 10, 10000\" #@param {type:\"string\"}\n", - "\n", - "\n", - "frequencies_to_show = [int(val.strip()) for val in frequencies_to_show.split(\",\")]\n", - "files = [f for f in os.listdir(file_path) if \n", - " os.path.splitext(f)[1].lower() == \".dta\" and\n", - " len(re.findall(file_pattern, f)) > 0\n", - " ]\n", - "\n", - "# For repeating EIS, we need to properly sort files -- by chronological run-order instead of alphanumeric filename.\n", - "run_pattern = re.compile(\"[0-9]+_Run[0-9]+\\.DTA\", re.IGNORECASE)\n", - "files.sort(key=lambda fname: \"_\".join([\"\".join(filter(str.isdigit, x)).zfill(4) for x in run_pattern.search(fname).group().split(\"_\")]))\n", - "\n", - "if len(files) == 0:\n", - " assert False, \"No files matching the file filter [{}] were found.\".format(file_pattern)\n", - "else:\n", - " print('Found [{}] data files matching [{}]'.format(len(files), file_pattern))\n", - "\n", - "# store aggregated start time, magnitude, phase, real, and imaginary impedance into separate variables\n", - "start_times = []\n", - "df_mag = pd.DataFrame()\n", - "df_phz = pd.DataFrame()\n", - "df_real = pd.DataFrame()\n", - "df_imag = pd.DataFrame()\n", - "\n", - "# iterate through gamry files\n", - "index = 0\n", - "for dataf in files:\n", - " name = os.path.splitext(dataf)[0].split('-')\n", - " name = \", \".join(name[1:])\n", - " \n", - " # load file\n", - " f = os.path.join(file_path, dataf)\n", - " z.load(f)\n", - "\n", - " # process data header metadata\n", - " start_time = pd.Timestamp(\"{} {}\".format(z.header.get(\"DATE\"), z.header.get(\"TIME\")))\n", - " print('{} [{}] ocp={}'.format(start_time, name, z.ocv))\n", - " \n", - " # extract EIS curve\n", - " res = z.curve()\n", - " \n", - " start_times.append(start_time)\n", - " df_mag[name] = res['Zmod']\n", - " df_phz[name] = res['Zphz']\n", - " df_real[name] = res['Zreal']\n", - " df_imag[name] = res['Zimag']\n", - "\n", - "# post-processing for all collected curves\n", - "\n", - "# validate the collected data, set frequency as dataframe index\n", - "df_mag[\"Freq\"] = res[\"Freq\"]\n", - "df_mag.set_index(\"Freq\", inplace=True)\n", - "df_mag.mask(df_mag < 0, inplace=True)\n", - "df_phz[\"Freq\"] = res[\"Freq\"]\n", - "df_phz.set_index(\"Freq\", inplace=True)\n", - "df_phz.mask(df_phz > 0, inplace=True)\n", - "df_phz.mask(df_phz < -90, inplace=True)\n", - "df_real[\"Freq\"] = res[\"Freq\"]\n", - "df_real.set_index(\"Freq\", inplace=True)\n", - "df_real.mask(df_real < 0, inplace=True)\n", - "df_imag[\"Freq\"] = res[\"Freq\"]\n", - "df_imag.set_index(\"Freq\", inplace=True)\n", - "df_imag.mask(df_imag > 0, inplace=True)\n", - "df_imag = df_imag.applymap(abs)\n", - "\n", - "\n", - "# print to screen impedance magnitude for the desired frequency\n", - "def freq_lookup(df, freq):\n", - " return df.index.get_loc(freq, method='nearest')\n", - "\n", - "for freq in frequencies_to_show:\n", - " row_index = freq_lookup(df_mag, freq)\n", - " print(\"\\n Showing Z_mag @ {} Hz [actual={:0.2f} Hz]\".format(freq, df_mag.index[row_index]))\n", - " print(df_mag.iloc[row_index])\n" - ], - "outputs": [], - "metadata": { - "id": "7f1iTOecIISA", - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@markdown **Bode Plot**: Display Zmag, Zphase vs. Freq\n", - "from plotly.subplots import make_subplots\n", - "import plotly.graph_objects as go\n", - "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", - "\n", - "show_legend = False #@param{type:\"boolean\"}\n", - "fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", - "\n", - "data = []\n", - "\n", - "# Yields a tuple of column name and series for each column in the dataframe\n", - "for (index, (columnName, columnData)) in enumerate(df_mag.iteritems()):\n", - " newTrace = go.Scatter(\n", - " x=df_mag.index,\n", - " y=columnData,\n", - " mode='lines',\n", - " name=columnName,\n", - " legendgroup=columnName,\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", - " )\n", - " fig.add_trace(newTrace, row=1, col=1)\n", - "\n", - " newTrace = go.Scatter(\n", - " x=df_mag.index,\n", - " y=-1*df_phz[columnName],\n", - " mode='lines',\n", - " name=columnName,\n", - " legendgroup=columnName,\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", - " showlegend=False\n", - " )\n", - " fig.add_trace(newTrace, row=2, col=1)\n", - "\n", - "# variation = df_mag.std(axis=1) / newTrace['y']\n", - "# fig.add_trace({'x': df_mag.index, 'y': variation, 'name': 'Signal Variation'}, row=2, col=1)\n", - "\n", - "layout = {\n", - " 'title': {'text': 'Bode Plot [{}]'.format(experiment_name),\n", - " 'yanchor': 'top',\n", - " 'y': 0.95,\n", - " 'x': 0.5 },\n", - " 'xaxis': {\n", - " 'anchor': 'x',\n", - " 'type': 'log'\n", - " },\n", - " 'xaxis2': {\n", - " 'title': 'Frequency, Hz',\n", - " 'type': 'log',\n", - " 'matches': 'x'\n", - " },\n", - " 'yaxis': {\n", - " 'title': 'Magnitude, Ohm',\n", - " 'type': 'log'\n", - " ''\n", - " },\n", - " 'yaxis2': {\n", - " 'title': 'Phase, deg',\n", - " },\n", - " 'legend': {'x': 0.85, 'y': 0.97},\n", - " 'margin': dict(l=30, r=20, t=60, b=20),\n", - " 'width': 1200,\n", - " 'height': 500,\n", - "}\n", - "fig.update_layout(layout)\n", - "if not show_legend:\n", - " fig.update_layout({\"showlegend\": False})\n", - "\n", - "config={\n", - " 'displaylogo': False,\n", - " 'modeBarButtonsToRemove': ['select2d', 'lasso2d', 'hoverClosestCartesian', 'toggleSpikelines','hoverCompareCartesian']\n", - "}\n", - "fig.show(config=config)" - ], - "outputs": [], - "metadata": { - "id": "t850lx00MlBL", - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@markdown **Polar Coordinate Plot**: Display -Zimag vs. Zreal\n", - "\n", - "from plotly.subplots import make_subplots\n", - "import plotly.graph_objects as go\n", - "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", - "\n", - "logx = True #@param {type:\"boolean\"}\n", - "logy = True #@param {type:\"boolean\"}\n", - "\n", - "fig = make_subplots(rows=1, cols=1,vertical_spacing=0.02, horizontal_spacing=0.02)\n", - "\n", - "data = []\n", - "# Yields a tuple of column name and series for each column in the dataframe\n", - "for (index, columnName) in enumerate(df_real.columns):\n", - " newTrace = go.Scatter(\n", - " x=df_real[columnName],\n", - " y=-df_imag[columnName],\n", - " mode='markers+lines',\n", - " name=columnName,\n", - " legendgroup=columnName,\n", - " text='Freq: ' + df_real.index.astype(str),\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", - " )\n", - " fig.add_trace(newTrace, row=1, col=1)\n", - "\n", - "# variation = df_mag.std(axis=1) / newTrace['y']\n", - "# fig.add_trace({'x': df_mag.index, 'y': variation, 'name': 'Signal Variation'}, row=2, col=1)\n", - "\n", - "layout = {\n", - " 'title': {'text': 'Impendance Plot, Polar Coord. [tests matching: {}]'.format(file_pattern),\n", - " 'yanchor': 'top',\n", - " 'y': 0.95,\n", - " 'x': 0.5 },\n", - " 'xaxis': {\n", - " 'anchor': 'x',\n", - " 'title': 'Zreal, Ohm',\n", - " 'type': 'log' if logx else 'linear',\n", - " },\n", - " 'yaxis': {\n", - " 'title': '-Zimag, Ohm',\n", - " 'type': 'log' if logy else 'linear',\n", - " },\n", - " 'legend': {'x': 0.03, 'y': 0.97},\n", - " 'margin': dict(l=30, r=20, t=60, b=20),\n", - " 'width': 600,\n", - " 'height': 600,\n", - "}\n", - "fig.update_layout(layout)\n", - "config={\n", - " 'displaylogo': False,\n", - " 'modeBarButtonsToRemove': ['select2d', 'lasso2d', 'hoverClosestCartesian', 'toggleSpikelines','hoverCompareCartesian']\n", - "}\n", - "fig.show(config=config)" - ], - "outputs": [], - "metadata": { - "id": "V6LKqpiECgb5", - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@markdown **Time-series Plot (Z_mag)**\n", - "from plotly.subplots import make_subplots\n", - "import plotly.graph_objects as go\n", - "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", - "\n", - "impedance_type = \"magnitude\" #@param [\"magnitude\", \"phase\", \"real\", \"imaginary\"]\n", - "\n", - "#@markdown Which impedance frequencies should be shown? (separated by comma, e.g. `4, 1000, 10000`)\n", - "frequencies_to_show = \"3,5,5000\" #@param {type:\"string\"}\n", - "\n", - "impedance_map = dict(magnitude=df_mag, phase=df_phz, real=df_real, imaginary=df_imag)\n", - "impedance_yaxis_map = {\n", - " \"magnitude\": {'title': 'Impedance Magnitude, Ohm', 'type': 'log'},\n", - " \"phase\": {'title': 'Phase Angle, deg', 'type': 'linear'},\n", - " \"real\": {'title': 'Real Impedance, Ohm', 'type': 'log'},\n", - " \"imaginary\": {'title': '- Imaginary Impedance, Ohm', 'type': 'log'},\n", - "}\n", - "frequencies_to_show = [int(val.strip()) for val in frequencies_to_show.split(\",\")]\n", - "\n", - "\n", - "def freq_lookup(df, freq):\n", - " return df.index.get_loc(freq, method='nearest')\n", - "\n", - "ilocs = [freq_lookup(df_mag, freq) for freq in frequencies_to_show]\n", - "\n", - "source = impedance_map.get(impedance_type)\n", - "source_yaxis_config = impedance_yaxis_map.get(impedance_type)\n", - "\n", - "# df_mag.T.to_csv(\"magnitude_transpose.csv\")\n", - "df_time = source.copy()\n", - "df_time.columns = start_times\n", - "df_time.T.to_csv(\"longitudinal.csv\")\n", - "\n", - "df_time = source.copy().iloc[ilocs]\n", - "df_time.columns = start_times\n", - "df_time = df_time.T\n", - "df_time.to_csv(\"longitudinal-filtered.csv\")\n", - "\n", - "fig = make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", - "# Yields a tuple of column name and series for each column in the dataframe\n", - "for (index, (columnName, columnData)) in enumerate(df_time.iteritems()):\n", - " newTrace = go.Scatter(\n", - " x=df_time.index,\n", - " y=columnData,\n", - " mode='lines',\n", - " name=\"{} Hz\".format(round(columnName,1)),\n", - " legendgroup=columnName,\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", - " )\n", - " fig.add_trace(newTrace, row=1, col=1)\n", - "\n", - "layout = {\n", - " 'title': {'text': 'Time-series Plot [{}]'.format(experiment_name),\n", - " 'yanchor': 'top',\n", - " 'y': 0.95,\n", - " 'x': 0.5 },\n", - " 'xaxis': {\n", - " 'anchor': 'x',\n", - " # 'type': 'log'\n", - " },\n", - " 'yaxis': source_yaxis_config,\n", - " 'legend': {'x': 0.85, 'y': 0.97},\n", - " 'margin': dict(l=30, r=20, t=60, b=20),\n", - " 'width': 1200,\n", - " 'height': 500,\n", - "}\n", - "fig.update_layout(layout)\n", - "\n", - "config={\n", - " 'displaylogo': False,\n", - " 'modeBarButtonsToRemove': ['select2d', 'lasso2d', 'hoverClosestCartesian', 'toggleSpikelines','hoverCompareCartesian']\n", - "}\n", - "fig.show(config=config)" - ], - "outputs": [], - "metadata": { - "id": "JVPwUpRcSdXM", - "cellView": "form" - } - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "#@markdown WORK IN PROGRESS **Simulation**: Fit collected data to an electrochemical model\n", - "equivalent_model = \"Body Impedance Model\" #@param [\"Randles\", \"Randles + Diffusion\", \"Randles + Corrosion\", \"Body Impedance Model\"]\n", - "\n", - "# import additional math libs\n", - "import numpy as np\n", - "from numpy import pi, sqrt\n", - "try:\n", - " from lmfit import Model\n", - "except:\n", - " subprocess.run(\n", - " [\"pip\", \"install\", \"--upgrade\", \"lmfit\"], \n", - " encoding=\"utf-8\", \n", - " shell=False)\n", - "finally:\n", - " from lmfit import Model\n", - "\n", - " \n", - "def reference_model(freq, Rct, Cdl_C, Cdl_a, Rsol):\n", - " # modifeid randle circuit -- The dual layer capacitance is non-ideal due to \n", - " # diffusion-related limitations. It has been replaced with a constant phase\n", - " # element. Circuit layout: SERIES(Rsol, PARALLEL(Rct, CPE))\n", - " CPE1 = 1/(Cdl_C*(1j*2*pi*freq)**(Cdl_a))\n", - " \n", - " return 1/((1/Rct) + (1/CPE1)) + Rsol\n", - "\n", - "def body_impedance_model(freq, R1, C1, R2, C2, R3, C3, Rs, Cs): # P1, R2, C2, P2, R3, C3, P3, Rs, Cs):\n", - " # Layer1 = 1/((1/R1) + (C1*(1j*2*pi*(freq + P1))))\n", - " # Layer2 = 1/((1/R2) + (C2*(1j*2*pi*(freq + P2))))\n", - " # Layer3 = 1/((1/R3) + (C3*(1j*2*pi*(freq + P3))))\n", - " Layer1 = 1/((1/R1) + (C1*(1j*2*pi*(freq))))\n", - " Layer2 = 1/((1/R2) + (C2*(1j*2*pi*(freq))))\n", - " Layer3 = 1/((1/R3) + (C3*(1j*2*pi*(freq))))\n", - " Zc_s = 1/(Cs * 1j * 2 * pi * (freq))\n", - " Zr_s = Rs\n", - " return Zc_s + Zr_s + Layer1 + Layer2 + Layer3\n", - "\n", - "def diffusion_model(freq, Rct, Cdl_C, Cdl_a, Rsol, Zdf):\n", - " # A modified Randle circuit with a warburg component included.\n", - " # Circuit layout: SERIES(Rsol, PARALLEL(SERIES(Warburg, Rct), CPE))\n", - " CPE1 = 1/(Cdl_C*(1j*2*pi*freq)**(Cdl_a))\n", - " \n", - " ## use finite length diffusion constant \n", - " # Warburg = Zdf * (np.tanh(sqrt(2j*pi*freq*TCdf))/sqrt(2j*pi*freq*TCdf))\n", - " \n", - " ## use infinite warburg coeff (simplified)\n", - " Warburg = 1/(Zdf*sqrt(1j*2*pi*freq))\n", - " \n", - " return 1/((1/(Rct + Warburg)) + (1/CPE1)) + Rsol\n", - "\n", - "def corrosion_model(freq, Rc, Cdl_C, Cdl_a, Rsol, Ra, La):\n", - " # split cathodic and anodic resistances with inductive component\n", - " CPE1 = 1/(Cdl_C*(1j*2*pi*freq)**(Cdl_a))\n", - " \n", - " Za = Ra + 2j*pi*freq*La\n", - " Rct = 1 / ((1/Rc) + (1/Za))\n", - " \n", - " return 1/((1/Rct) + (1/CPE1)) + Rsol\n", - "\n", - "def corrosion2_model(freq, Rc, Cdl_C, Cdl_a, Rsol, Ra, La):\n", - " # split cathodic and anodic resistances with inductive component\n", - " CPE1 = 1/(Cdl_C*(1j*2*pi*freq)**(Cdl_a))\n", - " \n", - " Za = Ra + 2j*pi*freq*La\n", - " Rct = 1 / ((1/Rc) + (1/Za))\n", - " \n", - " return 1/((1/Rct) + (1/CPE1)) + Rsol\n", - "\n", - "# create the model\n", - "if equivalent_model == \"Body Impedance Model\":\n", - " # model a membrane as resistor and capacitor in parallel.\n", - " gmodel = Model(body_impedance_model)\n", - " gmodel.set_param_hint('C1', value = 85e-9, min = 1e-9, max=1e-6)\n", - " gmodel.set_param_hint('C2', value = 85e-9, min = 1e-9, max=1e-6)\n", - " gmodel.set_param_hint('C3', value = 85e-9, min = 1e-9, max=1e-6)\n", - " gmodel.set_param_hint('R1', value = 45e3, min=1e3, max=1e6)\n", - " gmodel.set_param_hint('R2', value = 875e3, min=1e3, max=1e6)\n", - " gmodel.set_param_hint('R3', value = 750, min=0, max=1e3)\n", - " gmodel.set_param_hint('Rs', value = 400, min=200, max=600)\n", - " gmodel.set_param_hint('Cs', value = 150e-9, min=50e-9, max=5e-6)\n", - " \n", - " \n", - "elif equivalent_model == 'Randles':\n", - " # default, use a randle's circuit with non-ideal capacitor assumption\n", - " gmodel = Model(reference_model)\n", - " gmodel.set_param_hint('Rct', value = 1e7, min = 1e3, max = 1e9)\n", - "\n", - "elif equivalent_model == 'Randles + Diffusion':\n", - " # use previous model and add a warburg\n", - " gmodel = Model(diffusion_model)\n", - " gmodel.set_param_hint('Rct', value = 1e6, min = 1e3, max = 1e10)\n", - " gmodel.set_param_hint('Zdf', value = 1e4, min = 1e3, max = 1e6)\n", - "\n", - "else: \n", - " # Randle + Corrosion\n", - " gmodel = Model(corrosion_model)\n", - " gmodel.set_param_hint('Rc', value = 5e5, min = 1e3, max = 1e10)\n", - " gmodel.set_param_hint('Ra', value = 5e5, min = 1e3, max = 1e10)\n", - " gmodel.set_param_hint('La', value = 1e6, min = 0, max = 1e9)\n", - " \n", - "# initial guess shared across all models, with defined acceptable limits\n", - "gmodel.set_param_hint('Cdl_C', value = 5e-5, min = 1e-12)\n", - "gmodel.set_param_hint('Cdl_a', value = 0.9, min = 0, max = 1)\n", - "gmodel.set_param_hint('Rsol', value = 1000, min = 100, max = 5e5)\n", - "\n", - "# now solve for each loaded sensor\n", - "a = []\n", - "freq = np.asarray(df_mag.index)\n", - "for (index, columnName) in enumerate(df_mag.columns):\n", - " print('Model Simulation [{}] on [{}]'.format(equivalent_model, columnName))\n", - " impedance = np.asarray(df_real[columnName]) - 1j * np.asarray(df_imag[columnName])\n", - " \n", - " # fit_weights = (np.arange(len(freq))**.6)/len(freq) #weight ~ freq\n", - " fit_weights = np.ones(len(freq))/len(freq) #equal weight\n", - " \n", - " result = gmodel.fit(impedance, freq=freq, weights = fit_weights)\n", - " print(result.fit_report(show_correl=False))\n", - "\n", - " fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", - " data = []\n", - " # Yields a tuple of column name and series for each column in the dataframe\n", - " rawTrace = go.Scatter(\n", - " x=freq,\n", - " y=df_mag[columnName],\n", - " mode='markers',\n", - " name=columnName,\n", - " legendgroup='raw',\n", - " marker=dict(color=DEFAULT_PLOTLY_COLORS[0]),\n", - " )\n", - " fig.add_trace(rawTrace, row=1, col=1)\n", - " fitTrace = go.Scatter(\n", - " x=freq,\n", - " y=np.abs(result.best_fit),\n", - " mode='lines',\n", - " name=columnName,\n", - " legendgroup='model',\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[1]),\n", - " )\n", - " fig.add_trace(fitTrace, row=1, col=1)\n", - " rawTrace = go.Scatter(\n", - " x=freq,\n", - " y=-df_phz[columnName],\n", - " mode='markers',\n", - " name=columnName,\n", - " legendgroup='raw',\n", - " showlegend=False,\n", - " marker=dict(color=DEFAULT_PLOTLY_COLORS[0]),\n", - " )\n", - " fig.add_trace(rawTrace, row=2, col=1)\n", - " fitTrace = go.Scatter(\n", - " x=freq,\n", - " y=-180/pi*np.angle(result.best_fit),\n", - " mode='lines',\n", - " name=columnName,\n", - " legendgroup='model',\n", - " showlegend=False,\n", - " line=dict(color=DEFAULT_PLOTLY_COLORS[1]),\n", - " )\n", - " fig.add_trace(fitTrace, row=2, col=1)\n", - " layout = {\n", - " 'title': 'Model Fit [{}] for [{}]'.format(equivalent_model, columnName),\n", - " 'xaxis': {\n", - " 'anchor': 'x',\n", - " 'type': 'log'\n", - " },\n", - " 'xaxis2': {\n", - " 'anchor': 'x',\n", - " 'type': 'log'\n", - " },\n", - " 'yaxis': {\n", - " 'type': 'log'\n", - " },\n", - " 'legend': {'x': 0.85, 'y': 0.97},\n", - " 'margin': dict(l=30, r=20, t=60, b=20),\n", - " 'width': 1200,\n", - " 'height': 500,\n", - " }\n", - " \n", - " fig.update_layout(layout)\n", - " fig.show()" - ], - "outputs": [], - "metadata": { - "id": "V17rqu3-A74s", - "cellView": "form" - } - } - ] + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @title Imports, initial setup (Ctrl+F9 to run all)\n", + "try:\n", + " import gamry_parser\n", + "except:\n", + " subprocess.run([\"pip\", \"install\", \"gamry-parser\"], encoding=\"utf-8\", shell=False)\n", + "finally:\n", + " import gamry_parser\n", + "\n", + "import os\n", + "import re\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "z = gamry_parser.Impedance()\n", + "\n", + "print(\"Done.\")" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "\"\"\"\n", + "### SCRIPT CONFIGURATION SETTINGS ###\n", + "\"\"\"\n", + "\n", + "# @markdown **Load data** Parse Gamry *.DTA files from folder\n", + "\n", + "# @markdown Where are the Gamry DTA files located?\n", + "file_path = \"/path/to/gamry/files/\" # @param {type:\"string\"}\n", + "\n", + "# @markdown What's the title of this experiment?\n", + "experiment_name = \"EIS Experimentt 1\" # @param {type:\"string\"}\n", + "\n", + "# @markdown Which of the DTA files do we want to compare? (Regular expression matching)\n", + "file_pattern = \"EIS\" # @param {type:\"string\"}\n", + "\n", + "# @markdown Which impedance frequencies should be shown? (separated by comma, e.g. `4, 1000, 10000`)\n", + "frequencies_to_show = \"1, 5, 10, 10000\" # @param {type:\"string\"}\n", + "\n", + "\n", + "frequencies_to_show = [int(val.strip()) for val in frequencies_to_show.split(\",\")]\n", + "files = [\n", + " f\n", + " for f in os.listdir(file_path)\n", + " if os.path.splitext(f)[1].lower() == \".dta\" and len(re.findall(file_pattern, f)) > 0\n", + "]\n", + "\n", + "# For repeating EIS, we need to properly sort files -- by chronological run-order instead of alphanumeric filename.\n", + "run_pattern = re.compile(\"[0-9]+_Run[0-9]+\\.DTA\", re.IGNORECASE)\n", + "files.sort(\n", + " key=lambda fname: \"_\".join(\n", + " [\n", + " \"\".join(filter(str.isdigit, x)).zfill(4)\n", + " for x in run_pattern.search(fname).group().split(\"_\")\n", + " ]\n", + " )\n", + ")\n", + "\n", + "if len(files) == 0:\n", + " assert False, \"No files matching the file filter [{}] were found.\".format(\n", + " file_pattern\n", + " )\n", + "else:\n", + " print(\"Found [{}] data files matching [{}]\".format(len(files), file_pattern))\n", + "\n", + "# store aggregated start time, magnitude, phase, real, and imaginary impedance into separate variables\n", + "start_times = []\n", + "df_mag = pd.DataFrame()\n", + "df_phz = pd.DataFrame()\n", + "df_real = pd.DataFrame()\n", + "df_imag = pd.DataFrame()\n", + "\n", + "# iterate through gamry files\n", + "index = 0\n", + "for dataf in files:\n", + " name = os.path.splitext(dataf)[0].split(\"-\")\n", + " name = \", \".join(name[1:])\n", + "\n", + " # load file\n", + " f = os.path.join(file_path, dataf)\n", + " z.load(f)\n", + "\n", + " # process data header metadata\n", + " start_time = pd.Timestamp(\n", + " \"{} {}\".format(z.header.get(\"DATE\"), z.header.get(\"TIME\"))\n", + " )\n", + " print(\"{} [{}] ocp={}\".format(start_time, name, z.ocv))\n", + "\n", + " # extract EIS curve\n", + " res = z.curve()\n", + "\n", + " start_times.append(start_time)\n", + " df_mag[name] = res[\"Zmod\"]\n", + " df_phz[name] = res[\"Zphz\"]\n", + " df_real[name] = res[\"Zreal\"]\n", + " df_imag[name] = res[\"Zimag\"]\n", + "\n", + "# post-processing for all collected curves\n", + "\n", + "# validate the collected data, set frequency as dataframe index\n", + "df_mag[\"Freq\"] = res[\"Freq\"]\n", + "df_mag.set_index(\"Freq\", inplace=True)\n", + "df_mag.mask(df_mag < 0, inplace=True)\n", + "df_phz[\"Freq\"] = res[\"Freq\"]\n", + "df_phz.set_index(\"Freq\", inplace=True)\n", + "df_phz.mask(df_phz > 0, inplace=True)\n", + "df_phz.mask(df_phz < -90, inplace=True)\n", + "df_real[\"Freq\"] = res[\"Freq\"]\n", + "df_real.set_index(\"Freq\", inplace=True)\n", + "df_real.mask(df_real < 0, inplace=True)\n", + "df_imag[\"Freq\"] = res[\"Freq\"]\n", + "df_imag.set_index(\"Freq\", inplace=True)\n", + "df_imag.mask(df_imag > 0, inplace=True)\n", + "df_imag = df_imag.applymap(abs)\n", + "\n", + "\n", + "# print to screen impedance magnitude for the desired frequency\n", + "def freq_lookup(df, freq):\n", + " return df.index.get_loc(freq, method=\"nearest\")\n", + "\n", + "\n", + "for freq in frequencies_to_show:\n", + " row_index = freq_lookup(df_mag, freq)\n", + " print(\n", + " \"\\n Showing Z_mag @ {} Hz [actual={:0.2f} Hz]\".format(\n", + " freq, df_mag.index[row_index]\n", + " )\n", + " )\n", + " print(df_mag.iloc[row_index])" + ], + "outputs": [], + "metadata": { + "id": "7f1iTOecIISA", + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @markdown **Bode Plot**: Display Zmag, Zphase vs. Freq\n", + "from plotly.subplots import make_subplots\n", + "import plotly.graph_objects as go\n", + "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", + "\n", + "show_legend = False # @param{type:\"boolean\"}\n", + "fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", + "\n", + "data = []\n", + "\n", + "# Yields a tuple of column name and series for each column in the dataframe\n", + "for index, (columnName, columnData) in enumerate(df_mag.iteritems()):\n", + " newTrace = go.Scatter(\n", + " x=df_mag.index,\n", + " y=columnData,\n", + " mode=\"lines\",\n", + " name=columnName,\n", + " legendgroup=columnName,\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", + " )\n", + " fig.add_trace(newTrace, row=1, col=1)\n", + "\n", + " newTrace = go.Scatter(\n", + " x=df_mag.index,\n", + " y=-1 * df_phz[columnName],\n", + " mode=\"lines\",\n", + " name=columnName,\n", + " legendgroup=columnName,\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", + " showlegend=False,\n", + " )\n", + " fig.add_trace(newTrace, row=2, col=1)\n", + "\n", + "# variation = df_mag.std(axis=1) / newTrace['y']\n", + "# fig.add_trace({'x': df_mag.index, 'y': variation, 'name': 'Signal Variation'}, row=2, col=1)\n", + "\n", + "layout = {\n", + " \"title\": {\n", + " \"text\": \"Bode Plot [{}]\".format(experiment_name),\n", + " \"yanchor\": \"top\",\n", + " \"y\": 0.95,\n", + " \"x\": 0.5,\n", + " },\n", + " \"xaxis\": {\"anchor\": \"x\", \"type\": \"log\"},\n", + " \"xaxis2\": {\"title\": \"Frequency, Hz\", \"type\": \"log\", \"matches\": \"x\"},\n", + " \"yaxis\": {\"title\": \"Magnitude, Ohm\", \"type\": \"log\" \"\"},\n", + " \"yaxis2\": {\n", + " \"title\": \"Phase, deg\",\n", + " },\n", + " \"legend\": {\"x\": 0.85, \"y\": 0.97},\n", + " \"margin\": dict(l=30, r=20, t=60, b=20),\n", + " \"width\": 1200,\n", + " \"height\": 500,\n", + "}\n", + "fig.update_layout(layout)\n", + "if not show_legend:\n", + " fig.update_layout({\"showlegend\": False})\n", + "\n", + "config = {\n", + " \"displaylogo\": False,\n", + " \"modeBarButtonsToRemove\": [\n", + " \"select2d\",\n", + " \"lasso2d\",\n", + " \"hoverClosestCartesian\",\n", + " \"toggleSpikelines\",\n", + " \"hoverCompareCartesian\",\n", + " ],\n", + "}\n", + "fig.show(config=config)" + ], + "outputs": [], + "metadata": { + "id": "t850lx00MlBL", + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @markdown **Polar Coordinate Plot**: Display -Zimag vs. Zreal\n", + "\n", + "from plotly.subplots import make_subplots\n", + "import plotly.graph_objects as go\n", + "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", + "\n", + "logx = True # @param {type:\"boolean\"}\n", + "logy = True # @param {type:\"boolean\"}\n", + "\n", + "fig = make_subplots(rows=1, cols=1, vertical_spacing=0.02, horizontal_spacing=0.02)\n", + "\n", + "data = []\n", + "# Yields a tuple of column name and series for each column in the dataframe\n", + "for index, columnName in enumerate(df_real.columns):\n", + " newTrace = go.Scatter(\n", + " x=df_real[columnName],\n", + " y=-df_imag[columnName],\n", + " mode=\"markers+lines\",\n", + " name=columnName,\n", + " legendgroup=columnName,\n", + " text=\"Freq: \" + df_real.index.astype(str),\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", + " )\n", + " fig.add_trace(newTrace, row=1, col=1)\n", + "\n", + "# variation = df_mag.std(axis=1) / newTrace['y']\n", + "# fig.add_trace({'x': df_mag.index, 'y': variation, 'name': 'Signal Variation'}, row=2, col=1)\n", + "\n", + "layout = {\n", + " \"title\": {\n", + " \"text\": \"Impendance Plot, Polar Coord. [tests matching: {}]\".format(\n", + " file_pattern\n", + " ),\n", + " \"yanchor\": \"top\",\n", + " \"y\": 0.95,\n", + " \"x\": 0.5,\n", + " },\n", + " \"xaxis\": {\n", + " \"anchor\": \"x\",\n", + " \"title\": \"Zreal, Ohm\",\n", + " \"type\": \"log\" if logx else \"linear\",\n", + " },\n", + " \"yaxis\": {\n", + " \"title\": \"-Zimag, Ohm\",\n", + " \"type\": \"log\" if logy else \"linear\",\n", + " },\n", + " \"legend\": {\"x\": 0.03, \"y\": 0.97},\n", + " \"margin\": dict(l=30, r=20, t=60, b=20),\n", + " \"width\": 600,\n", + " \"height\": 600,\n", + "}\n", + "fig.update_layout(layout)\n", + "config = {\n", + " \"displaylogo\": False,\n", + " \"modeBarButtonsToRemove\": [\n", + " \"select2d\",\n", + " \"lasso2d\",\n", + " \"hoverClosestCartesian\",\n", + " \"toggleSpikelines\",\n", + " \"hoverCompareCartesian\",\n", + " ],\n", + "}\n", + "fig.show(config=config)" + ], + "outputs": [], + "metadata": { + "id": "V6LKqpiECgb5", + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @markdown **Time-series Plot (Z_mag)**\n", + "from plotly.subplots import make_subplots\n", + "import plotly.graph_objects as go\n", + "from plotly.colors import DEFAULT_PLOTLY_COLORS\n", + "\n", + "impedance_type = \"magnitude\" # @param [\"magnitude\", \"phase\", \"real\", \"imaginary\"]\n", + "\n", + "# @markdown Which impedance frequencies should be shown? (separated by comma, e.g. `4, 1000, 10000`)\n", + "frequencies_to_show = \"3,5,5000\" # @param {type:\"string\"}\n", + "\n", + "impedance_map = dict(magnitude=df_mag, phase=df_phz, real=df_real, imaginary=df_imag)\n", + "impedance_yaxis_map = {\n", + " \"magnitude\": {\"title\": \"Impedance Magnitude, Ohm\", \"type\": \"log\"},\n", + " \"phase\": {\"title\": \"Phase Angle, deg\", \"type\": \"linear\"},\n", + " \"real\": {\"title\": \"Real Impedance, Ohm\", \"type\": \"log\"},\n", + " \"imaginary\": {\"title\": \"- Imaginary Impedance, Ohm\", \"type\": \"log\"},\n", + "}\n", + "frequencies_to_show = [int(val.strip()) for val in frequencies_to_show.split(\",\")]\n", + "\n", + "\n", + "def freq_lookup(df, freq):\n", + " return df.index.get_loc(freq, method=\"nearest\")\n", + "\n", + "\n", + "ilocs = [freq_lookup(df_mag, freq) for freq in frequencies_to_show]\n", + "\n", + "source = impedance_map.get(impedance_type)\n", + "source_yaxis_config = impedance_yaxis_map.get(impedance_type)\n", + "\n", + "# df_mag.T.to_csv(\"magnitude_transpose.csv\")\n", + "df_time = source.copy()\n", + "df_time.columns = start_times\n", + "df_time.T.to_csv(\"longitudinal.csv\")\n", + "\n", + "df_time = source.copy().iloc[ilocs]\n", + "df_time.columns = start_times\n", + "df_time = df_time.T\n", + "df_time.to_csv(\"longitudinal-filtered.csv\")\n", + "\n", + "fig = make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", + "# Yields a tuple of column name and series for each column in the dataframe\n", + "for index, (columnName, columnData) in enumerate(df_time.iteritems()):\n", + " newTrace = go.Scatter(\n", + " x=df_time.index,\n", + " y=columnData,\n", + " mode=\"lines\",\n", + " name=\"{} Hz\".format(round(columnName, 1)),\n", + " legendgroup=columnName,\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[index % len(DEFAULT_PLOTLY_COLORS)]),\n", + " )\n", + " fig.add_trace(newTrace, row=1, col=1)\n", + "\n", + "layout = {\n", + " \"title\": {\n", + " \"text\": \"Time-series Plot [{}]\".format(experiment_name),\n", + " \"yanchor\": \"top\",\n", + " \"y\": 0.95,\n", + " \"x\": 0.5,\n", + " },\n", + " \"xaxis\": {\n", + " \"anchor\": \"x\",\n", + " # 'type': 'log'\n", + " },\n", + " \"yaxis\": source_yaxis_config,\n", + " \"legend\": {\"x\": 0.85, \"y\": 0.97},\n", + " \"margin\": dict(l=30, r=20, t=60, b=20),\n", + " \"width\": 1200,\n", + " \"height\": 500,\n", + "}\n", + "fig.update_layout(layout)\n", + "\n", + "config = {\n", + " \"displaylogo\": False,\n", + " \"modeBarButtonsToRemove\": [\n", + " \"select2d\",\n", + " \"lasso2d\",\n", + " \"hoverClosestCartesian\",\n", + " \"toggleSpikelines\",\n", + " \"hoverCompareCartesian\",\n", + " ],\n", + "}\n", + "fig.show(config=config)" + ], + "outputs": [], + "metadata": { + "id": "JVPwUpRcSdXM", + "cellView": "form" + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "# @markdown WORK IN PROGRESS **Simulation**: Fit collected data to an electrochemical model\n", + "equivalent_model = \"Body Impedance Model\" # @param [\"Randles\", \"Randles + Diffusion\", \"Randles + Corrosion\", \"Body Impedance Model\"]\n", + "\n", + "# import additional math libs\n", + "import numpy as np\n", + "from numpy import pi, sqrt\n", + "\n", + "try:\n", + " from lmfit import Model\n", + "except:\n", + " subprocess.run(\n", + " [\"pip\", \"install\", \"--upgrade\", \"lmfit\"], encoding=\"utf-8\", shell=False\n", + " )\n", + "finally:\n", + " from lmfit import Model\n", + "\n", + "\n", + "def reference_model(freq, Rct, Cdl_C, Cdl_a, Rsol):\n", + " # modifeid randle circuit -- The dual layer capacitance is non-ideal due to\n", + " # diffusion-related limitations. It has been replaced with a constant phase\n", + " # element. Circuit layout: SERIES(Rsol, PARALLEL(Rct, CPE))\n", + " CPE1 = 1 / (Cdl_C * (1j * 2 * pi * freq) ** (Cdl_a))\n", + "\n", + " return 1 / ((1 / Rct) + (1 / CPE1)) + Rsol\n", + "\n", + "\n", + "def body_impedance_model(\n", + " freq, R1, C1, R2, C2, R3, C3, Rs, Cs\n", + "): # P1, R2, C2, P2, R3, C3, P3, Rs, Cs):\n", + " # Layer1 = 1/((1/R1) + (C1*(1j*2*pi*(freq + P1))))\n", + " # Layer2 = 1/((1/R2) + (C2*(1j*2*pi*(freq + P2))))\n", + " # Layer3 = 1/((1/R3) + (C3*(1j*2*pi*(freq + P3))))\n", + " Layer1 = 1 / ((1 / R1) + (C1 * (1j * 2 * pi * (freq))))\n", + " Layer2 = 1 / ((1 / R2) + (C2 * (1j * 2 * pi * (freq))))\n", + " Layer3 = 1 / ((1 / R3) + (C3 * (1j * 2 * pi * (freq))))\n", + " Zc_s = 1 / (Cs * 1j * 2 * pi * (freq))\n", + " Zr_s = Rs\n", + " return Zc_s + Zr_s + Layer1 + Layer2 + Layer3\n", + "\n", + "\n", + "def diffusion_model(freq, Rct, Cdl_C, Cdl_a, Rsol, Zdf):\n", + " # A modified Randle circuit with a warburg component included.\n", + " # Circuit layout: SERIES(Rsol, PARALLEL(SERIES(Warburg, Rct), CPE))\n", + " CPE1 = 1 / (Cdl_C * (1j * 2 * pi * freq) ** (Cdl_a))\n", + "\n", + " ## use finite length diffusion constant\n", + " # Warburg = Zdf * (np.tanh(sqrt(2j*pi*freq*TCdf))/sqrt(2j*pi*freq*TCdf))\n", + "\n", + " ## use infinite warburg coeff (simplified)\n", + " Warburg = 1 / (Zdf * sqrt(1j * 2 * pi * freq))\n", + "\n", + " return 1 / ((1 / (Rct + Warburg)) + (1 / CPE1)) + Rsol\n", + "\n", + "\n", + "def corrosion_model(freq, Rc, Cdl_C, Cdl_a, Rsol, Ra, La):\n", + " # split cathodic and anodic resistances with inductive component\n", + " CPE1 = 1 / (Cdl_C * (1j * 2 * pi * freq) ** (Cdl_a))\n", + "\n", + " Za = Ra + 2j * pi * freq * La\n", + " Rct = 1 / ((1 / Rc) + (1 / Za))\n", + "\n", + " return 1 / ((1 / Rct) + (1 / CPE1)) + Rsol\n", + "\n", + "\n", + "def corrosion2_model(freq, Rc, Cdl_C, Cdl_a, Rsol, Ra, La):\n", + " # split cathodic and anodic resistances with inductive component\n", + " CPE1 = 1 / (Cdl_C * (1j * 2 * pi * freq) ** (Cdl_a))\n", + "\n", + " Za = Ra + 2j * pi * freq * La\n", + " Rct = 1 / ((1 / Rc) + (1 / Za))\n", + "\n", + " return 1 / ((1 / Rct) + (1 / CPE1)) + Rsol\n", + "\n", + "\n", + "# create the model\n", + "if equivalent_model == \"Body Impedance Model\":\n", + " # model a membrane as resistor and capacitor in parallel.\n", + " gmodel = Model(body_impedance_model)\n", + " gmodel.set_param_hint(\"C1\", value=85e-9, min=1e-9, max=1e-6)\n", + " gmodel.set_param_hint(\"C2\", value=85e-9, min=1e-9, max=1e-6)\n", + " gmodel.set_param_hint(\"C3\", value=85e-9, min=1e-9, max=1e-6)\n", + " gmodel.set_param_hint(\"R1\", value=45e3, min=1e3, max=1e6)\n", + " gmodel.set_param_hint(\"R2\", value=875e3, min=1e3, max=1e6)\n", + " gmodel.set_param_hint(\"R3\", value=750, min=0, max=1e3)\n", + " gmodel.set_param_hint(\"Rs\", value=400, min=200, max=600)\n", + " gmodel.set_param_hint(\"Cs\", value=150e-9, min=50e-9, max=5e-6)\n", + "\n", + "\n", + "elif equivalent_model == \"Randles\":\n", + " # default, use a randle's circuit with non-ideal capacitor assumption\n", + " gmodel = Model(reference_model)\n", + " gmodel.set_param_hint(\"Rct\", value=1e7, min=1e3, max=1e9)\n", + "\n", + "elif equivalent_model == \"Randles + Diffusion\":\n", + " # use previous model and add a warburg\n", + " gmodel = Model(diffusion_model)\n", + " gmodel.set_param_hint(\"Rct\", value=1e6, min=1e3, max=1e10)\n", + " gmodel.set_param_hint(\"Zdf\", value=1e4, min=1e3, max=1e6)\n", + "\n", + "else:\n", + " # Randle + Corrosion\n", + " gmodel = Model(corrosion_model)\n", + " gmodel.set_param_hint(\"Rc\", value=5e5, min=1e3, max=1e10)\n", + " gmodel.set_param_hint(\"Ra\", value=5e5, min=1e3, max=1e10)\n", + " gmodel.set_param_hint(\"La\", value=1e6, min=0, max=1e9)\n", + "\n", + "# initial guess shared across all models, with defined acceptable limits\n", + "gmodel.set_param_hint(\"Cdl_C\", value=5e-5, min=1e-12)\n", + "gmodel.set_param_hint(\"Cdl_a\", value=0.9, min=0, max=1)\n", + "gmodel.set_param_hint(\"Rsol\", value=1000, min=100, max=5e5)\n", + "\n", + "# now solve for each loaded sensor\n", + "a = []\n", + "freq = np.asarray(df_mag.index)\n", + "for index, columnName in enumerate(df_mag.columns):\n", + " print(\"Model Simulation [{}] on [{}]\".format(equivalent_model, columnName))\n", + " impedance = np.asarray(df_real[columnName]) - 1j * np.asarray(df_imag[columnName])\n", + "\n", + " # fit_weights = (np.arange(len(freq))**.6)/len(freq) #weight ~ freq\n", + " fit_weights = np.ones(len(freq)) / len(freq) # equal weight\n", + "\n", + " result = gmodel.fit(impedance, freq=freq, weights=fit_weights)\n", + " print(result.fit_report(show_correl=False))\n", + "\n", + " fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.02)\n", + " data = []\n", + " # Yields a tuple of column name and series for each column in the dataframe\n", + " rawTrace = go.Scatter(\n", + " x=freq,\n", + " y=df_mag[columnName],\n", + " mode=\"markers\",\n", + " name=columnName,\n", + " legendgroup=\"raw\",\n", + " marker=dict(color=DEFAULT_PLOTLY_COLORS[0]),\n", + " )\n", + " fig.add_trace(rawTrace, row=1, col=1)\n", + " fitTrace = go.Scatter(\n", + " x=freq,\n", + " y=np.abs(result.best_fit),\n", + " mode=\"lines\",\n", + " name=columnName,\n", + " legendgroup=\"model\",\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[1]),\n", + " )\n", + " fig.add_trace(fitTrace, row=1, col=1)\n", + " rawTrace = go.Scatter(\n", + " x=freq,\n", + " y=-df_phz[columnName],\n", + " mode=\"markers\",\n", + " name=columnName,\n", + " legendgroup=\"raw\",\n", + " showlegend=False,\n", + " marker=dict(color=DEFAULT_PLOTLY_COLORS[0]),\n", + " )\n", + " fig.add_trace(rawTrace, row=2, col=1)\n", + " fitTrace = go.Scatter(\n", + " x=freq,\n", + " y=-180 / pi * np.angle(result.best_fit),\n", + " mode=\"lines\",\n", + " name=columnName,\n", + " legendgroup=\"model\",\n", + " showlegend=False,\n", + " line=dict(color=DEFAULT_PLOTLY_COLORS[1]),\n", + " )\n", + " fig.add_trace(fitTrace, row=2, col=1)\n", + " layout = {\n", + " \"title\": \"Model Fit [{}] for [{}]\".format(equivalent_model, columnName),\n", + " \"xaxis\": {\"anchor\": \"x\", \"type\": \"log\"},\n", + " \"xaxis2\": {\"anchor\": \"x\", \"type\": \"log\"},\n", + " \"yaxis\": {\"type\": \"log\"},\n", + " \"legend\": {\"x\": 0.85, \"y\": 0.97},\n", + " \"margin\": dict(l=30, r=20, t=60, b=20),\n", + " \"width\": 1200,\n", + " \"height\": 500,\n", + " }\n", + "\n", + " fig.update_layout(layout)\n", + " fig.show()" + ], + "outputs": [], + "metadata": { + "id": "V17rqu3-A74s", + "cellView": "form" + } + } + ] } \ No newline at end of file diff --git a/gamry_parser/gamryparser.py b/gamry_parser/gamryparser.py index 57fc071..dee90b9 100644 --- a/gamry_parser/gamryparser.py +++ b/gamry_parser/gamryparser.py @@ -234,7 +234,9 @@ def read_header(self) -> Tuple[Dict[str, Any], int]: return self._header, self.header_length - def _read_curve_data(self, fid: TextIOWrapper) -> Tuple[List[str], List[str], pd.DataFrame]: + def _read_curve_data( + self, fid: TextIOWrapper + ) -> Tuple[List[str], List[str], pd.DataFrame]: """helper function to process an EXPLAIN Table Args: diff --git a/gamry_parser/version.py b/gamry_parser/version.py index 5becc17..3d18726 100644 --- a/gamry_parser/version.py +++ b/gamry_parser/version.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "0.5.0" diff --git a/gamry_parser/vfp600.py b/gamry_parser/vfp600.py index 9af5347..2262a6a 100644 --- a/gamry_parser/vfp600.py +++ b/gamry_parser/vfp600.py @@ -8,7 +8,9 @@ class VFP600(parser.GamryParser): """Load experiment data generated by Gamry VFP600 software (expected in EXPLAIN format, including header)""" - def __init__(self, filename: Optional[str] = None, to_timestamp: Optional[bool] = None): + def __init__( + self, filename: Optional[str] = None, to_timestamp: Optional[bool] = None + ): """VFP600.__init__ Args: @@ -81,7 +83,9 @@ def sample_count(self, curve: int = 0) -> int: return len(self._curves[curve - 1].index) if len(self._curves) > 0 else 0 - def _read_curve_data(self, fid: TextIOWrapper) -> Tuple[List[str], List[str], pd.DataFrame]: + def _read_curve_data( + self, fid: TextIOWrapper + ) -> Tuple[List[str], List[str], pd.DataFrame]: """helper function to process an EXPLAIN Table Args: diff --git a/tests/test_chronoamperometry.py b/tests/test_chronoamperometry.py index 35852e1..f454d37 100644 --- a/tests/test_chronoamperometry.py +++ b/tests/test_chronoamperometry.py @@ -9,12 +9,15 @@ "test_data", ) + class TestChronoAmperometry(unittest.TestCase): def setUp(self): pass def test_getters(self): - gp = parser.ChronoAmperometry(filename=os.path.join(FIXTURE_PATH, "chronoa_data.dta")) + gp = parser.ChronoAmperometry( + filename=os.path.join(FIXTURE_PATH, "chronoa_data.dta") + ) self.assertIsNone(gp.sample_time) self.assertEqual(gp.sample_count, 0) diff --git a/tests/test_cyclicvoltammetry.py b/tests/test_cyclicvoltammetry.py index 7e64f11..865ce7f 100644 --- a/tests/test_cyclicvoltammetry.py +++ b/tests/test_cyclicvoltammetry.py @@ -7,6 +7,7 @@ "test_data", ) + class TestCyclicVoltammetry(unittest.TestCase): def setUp(self): pass @@ -21,7 +22,9 @@ def test_is_loaded(self): self.assertIsNone(gp.experiment_type) def test_getters(self): - gp = parser.CyclicVoltammetry(filename=os.path.join(FIXTURE_PATH, "cv_data.dta")) + gp = parser.CyclicVoltammetry( + filename=os.path.join(FIXTURE_PATH, "cv_data.dta") + ) gp.load() vrange = gp.v_range self.assertEqual(vrange[0], 0.1) diff --git a/tests/test_gamryparser.py b/tests/test_gamryparser.py index 82ea96e..81555c4 100644 --- a/tests/test_gamryparser.py +++ b/tests/test_gamryparser.py @@ -41,7 +41,9 @@ def test_read_header(self): self.assertEqual(gp.header["CHECK2PARAM"].get("start"), 300) self.assertEqual(gp.header["CHECK2PARAM"].get("finish"), 0.5) - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "cv_data_incompleteheader.dta")) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "cv_data_incompleteheader.dta") + ) _, count = gp.read_header() self.assertEqual(gp.header["DELAY"], dict(enable=False, start=300, finish=0.1)) @@ -75,14 +77,18 @@ def test_load(self): self.assertEqual(curve5["IERange"].iloc[-1], 5) def test_use_datetime(self): - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "chronoa_data.dta"), to_timestamp=False) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "chronoa_data.dta"), to_timestamp=False + ) gp.load() curve = gp.curve() # 'T' should return elapsed time in seconds self.assertEqual(curve["T"][0], 0) self.assertEqual(curve["T"].iloc[-1], 270) - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "chronoa_data.dta"), to_timestamp=True) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "chronoa_data.dta"), to_timestamp=True + ) gp.load() curve = gp.curve() # 'T' should return datetime objects @@ -90,7 +96,9 @@ def test_use_datetime(self): self.assertEqual(curve["T"].iloc[-1], pd.to_datetime("3/10/2019 12:04:30")) def test_aborted_experiment(self): - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "eispot_data_curveaborted.dta")) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "eispot_data_curveaborted.dta") + ) gp.load() self.assertEqual(gp.curve_count, 1) self.assertEqual(gp._curves[0].shape, (5, 10)) @@ -120,7 +128,9 @@ def test_ocvcurve_self(self): gp.load() self.assertEqual(gp.ocv_curve, None) self.assertEqual(gp.ocv, None) - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "ocvcurve_data.dta")) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "ocvcurve_data.dta") + ) gp.load() self.assertEqual(gp.ocv_curve.iloc[0]["T"], 0.258333) self.assertEqual(gp.ocv_curve.iloc[-1]["T"], 10.3333) @@ -131,7 +141,9 @@ def test_locale(self): # confirm that files will load properly with non-US locales locale.setlocale(locale.LC_ALL, "de_DE.utf8") - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "chronoa_de_data.dta")) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "chronoa_de_data.dta") + ) gp.load() curve = gp.curve() self.assertEqual(curve["T"].iloc[-1], 270) @@ -154,7 +166,9 @@ def test_locale(self): # If parsed with the wrong locale, we should expect data corruption # in the resulting dataframe. locale.setlocale(locale.LC_ALL, "en_US.utf8") - gp = parser.GamryParser(filename=os.path.join(FIXTURE_PATH, "chronoa_de_data.dta")) + gp = parser.GamryParser( + filename=os.path.join(FIXTURE_PATH, "chronoa_de_data.dta") + ) gp.load() curve = gp.curve() self.assertEqual(curve["Vf"].iloc[0], -50) diff --git a/tests/test_impedance.py b/tests/test_impedance.py index a345c59..c7052f6 100644 --- a/tests/test_impedance.py +++ b/tests/test_impedance.py @@ -7,6 +7,7 @@ "test_data", ) + class TestImpedance(unittest.TestCase): def setUp(self): pass diff --git a/tests/test_ocp.py b/tests/test_ocp.py index 2e7eb50..1f59aa2 100644 --- a/tests/test_ocp.py +++ b/tests/test_ocp.py @@ -17,8 +17,7 @@ def setUp(self): def test_use_datetime(self): gp = parser.OpenCircuitPotential( - filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), - to_timestamp=False + filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), to_timestamp=False ) gp.load() curve = gp.curve() @@ -27,8 +26,7 @@ def test_use_datetime(self): self.assertEqual(curve["T"].iloc[-1], 105.175) gp = parser.OpenCircuitPotential( - filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), - to_timestamp=True + filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), to_timestamp=True ) gp.load() curve = gp.curve() @@ -40,8 +38,7 @@ def test_use_datetime(self): def test_is_loaded(self): gp = parser.OpenCircuitPotential( - filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), - to_timestamp=False + filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), to_timestamp=False ) gp.load() self.assertEqual(len(gp.curves), 1) @@ -67,8 +64,7 @@ def test_is_loaded(self): def test_getters(self): gp = parser.OpenCircuitPotential( - filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), - to_timestamp=False + filename=os.path.join(FIXTURE_PATH, "ocp_data.dta"), to_timestamp=False ) self.assertIsNone(gp.ocv_curve) gp.load() diff --git a/tests/test_squarewave.py b/tests/test_squarewave.py index db90f78..d4d4967 100644 --- a/tests/test_squarewave.py +++ b/tests/test_squarewave.py @@ -9,6 +9,7 @@ "test_data", ) + class TestSquareWaveVoltammetry(unittest.TestCase): def setUp(self): pass @@ -21,7 +22,9 @@ def test_is_loaded(self): # should raise exception if non-swv data is specified self.assertRaises( - AssertionError, gp.load, filename=os.path.join(FIXTURE_PATH, "test_chronoamperometry.dta") + AssertionError, + gp.load, + filename=os.path.join(FIXTURE_PATH, "test_chronoamperometry.dta"), ) # should retain nulled values if data is not loaded @@ -33,7 +36,9 @@ def test_is_loaded(self): self.assertEqual(gp.cycles, 0) def test_getters(self): - gp = parser.SquareWaveVoltammetry(filename=os.path.join(FIXTURE_PATH, "squarewave_data.dta")) + gp = parser.SquareWaveVoltammetry( + filename=os.path.join(FIXTURE_PATH, "squarewave_data.dta") + ) gp.load() self.assertTupleEqual(gp.v_range, (0, -0.5))