From 9c80d98199acaeaa52d834a87a317b430aafbc26 Mon Sep 17 00:00:00 2001 From: Brad Liang Date: Thu, 6 May 2021 11:34:12 -0700 Subject: [PATCH 1/3] add new notebooks in demo/ folder, basic cleanup --- demo/notebook_cyclicvoltammetry.ipynb | 10 +- ...otebook_cyclicvoltammetry_peakdetect.ipynb | 253 ++++++++ demo/notebook_gamry_parser.ipynb | 12 +- demo/notebook_potentiostatic_eis.ipynb | 575 ++++++++++++++++++ 4 files changed, 843 insertions(+), 7 deletions(-) create mode 100644 demo/notebook_cyclicvoltammetry_peakdetect.ipynb create mode 100644 demo/notebook_potentiostatic_eis.ipynb diff --git a/demo/notebook_cyclicvoltammetry.ipynb b/demo/notebook_cyclicvoltammetry.ipynb index e5ed4d4..ac4f4b6 100644 --- a/demo/notebook_cyclicvoltammetry.ipynb +++ b/demo/notebook_cyclicvoltammetry.ipynb @@ -30,10 +30,14 @@ "import matplotlib.pyplot as plt\n", "\n", "try:\n", - " import gamry_parser as parser\n", + " import gamry_parser\n", "except:\n", - " !pip install -q --upgrade gamry-parser\n", - " import gamry_parser as parser\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", diff --git a/demo/notebook_cyclicvoltammetry_peakdetect.ipynb b/demo/notebook_cyclicvoltammetry_peakdetect.ipynb new file mode 100644 index 0000000..0f037cb --- /dev/null +++ b/demo/notebook_cyclicvoltammetry_peakdetect.ipynb @@ -0,0 +1,253 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "Gamry-Parser CyclicVoltammetry Peak Detection Example", + "provenance": [], + "collapsed_sections": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "code", + "metadata": { + "id": "YyeVlSYkhahF", + "cellView": "form" + }, + "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.')" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "ZGoqracvk9q2", + "cellView": "form" + }, + "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 = pandas.DataFrame()\n", + "settings_df = pandas.DataFrame()\n", + "peaks_df = pandas.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" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "cellView": "form", + "id": "8MFNF2Qz6lef" + }, + "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.get_header().get(\"TAG\") == \"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.get_v_range()\n", + " settings = pandas.DataFrame({\n", + " \"label\": my_label,\n", + " \"curves\": cv.get_curve_count(),\n", + " \"v1_mV\": v1*1000,\n", + " \"v2_mV\": v2*1000,\n", + " \"rate_mV\": cv.get_scan_rate(),\n", + " }, index=[0])\n", + " settings_df = settings_df.append(settings)\n", + "\n", + " data = copy.deepcopy(cv.get_curve_data(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.get_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))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "Ulne80RrpBrW", + "cellView": "form" + }, + "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)" + ], + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/demo/notebook_gamry_parser.ipynb b/demo/notebook_gamry_parser.ipynb index fc0f24b..6c4130d 100644 --- a/demo/notebook_gamry_parser.ipynb +++ b/demo/notebook_gamry_parser.ipynb @@ -30,10 +30,14 @@ "import matplotlib.pyplot as plt\n", "\n", "try:\n", - " import gamry_parser as parser\n", + " import gamry_parser\n", "except:\n", - " !pip install -q --upgrade gamry-parser\n", - " import gamry_parser as parser\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", @@ -163,4 +167,4 @@ "outputs": [] } ] -} +} \ No newline at end of file diff --git a/demo/notebook_potentiostatic_eis.ipynb b/demo/notebook_potentiostatic_eis.ipynb new file mode 100644 index 0000000..77a5562 --- /dev/null +++ b/demo/notebook_potentiostatic_eis.ipynb @@ -0,0 +1,575 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "Gamry-Parser Potentiostatic EIS Example", + "provenance": [], + "collapsed_sections": [ + "OaPK3VEDxG2y" + ], + "toc_visible": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "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.')" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "7f1iTOecIISA", + "cellView": "form" + }, + "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 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", + " header = z.get_header()\n", + " start_time = pd.Timestamp(\"{} {}\".format(header.get(\"DATE\"), header.get(\"TIME\")))\n", + " print('{} [{}] ocp={}'.format(start_time, name, z.get_ocv_value()))\n", + " \n", + " # extract EIS curve\n", + " res = z.get_curve_data()\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" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "t850lx00MlBL", + "cellView": "form" + }, + "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)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "V6LKqpiECgb5", + "cellView": "form" + }, + "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)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "JVPwUpRcSdXM", + "cellView": "form" + }, + "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", + " # 'xaxis2': {\n", + " # 'title': 'Frequency, Hz',\n", + " # # 'type': 'log',\n", + " # 'matches': 'x'\n", + " # },\n", + " 'yaxis': source_yaxis_config,\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", + "\n", + "config={\n", + " 'displaylogo': False,\n", + " 'modeBarButtonsToRemove': ['select2d', 'lasso2d', 'hoverClosestCartesian', 'toggleSpikelines','hoverCompareCartesian']\n", + "}\n", + "fig.show(config=config)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "V17rqu3-A74s", + "cellView": "form" + }, + "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()" + ], + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file From 99aa5b3a165ed129838720bca936ebab3161c520 Mon Sep 17 00:00:00 2001 From: Brad Liang Date: Thu, 6 May 2021 11:35:44 -0700 Subject: [PATCH 2/3] resolve merge conflict --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c2791..20a23b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - [#40](https://github.com/bcliang/gamry-parser/pull/40) Change: GamryParser to_timestamp param #40 -- [#41](https://github.com/bcliang/gamry-parser/pull/40) Use tox as test runner +- [#41](https://github.com/bcliang/gamry-parser/pull/41) Use tox as test runner ### Added - [#42](https://github.com/bcliang/gamry-parser/pull/42) Update read_header function to support EFM140 data Files From fee4f5429a215a676a40b8ae92c6e38f39c4ffaa Mon Sep 17 00:00:00 2001 From: Brad Liang Date: Fri, 7 May 2021 08:31:08 -0700 Subject: [PATCH 3/3] rev version, changelog --- CHANGELOG.md | 12 ++++++++++++ gamry_parser/version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a23b2..7717e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,24 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - +### Changed +- + +### Added +- + +## [0.4.5] - 2021-05-07 + +### Fixed +- + ### Changed - [#40](https://github.com/bcliang/gamry-parser/pull/40) Change: GamryParser to_timestamp param #40 - [#41](https://github.com/bcliang/gamry-parser/pull/41) Use tox as test runner ### Added - [#42](https://github.com/bcliang/gamry-parser/pull/42) Update read_header function to support EFM140 data Files +- [#43](https://github.com/bcliang/gamry-parser/pull/43) Add Examples: Peak Finding, Impedance ## [0.4.4] - 2021-02-28 diff --git a/gamry_parser/version.py b/gamry_parser/version.py index cd1ee63..98a433b 100644 --- a/gamry_parser/version.py +++ b/gamry_parser/version.py @@ -1 +1 @@ -__version__ = "0.4.4" +__version__ = "0.4.5"