Tutorial: Comparing cycles of a cyclic voltammogram
Internet connection required: This tutorial reads data from a github repository here.
Here we will show how to use ixdat
and python to analyze two cases where the difference in integrated current between two cycles in cyclic voltammatry is needed.
Using the CO stripping example, we will show three ways of visualizing the stripping experiment and determining the surface area of a platinum electrode. Each gives the amount of charge associated with oxidation of adsorbed CO, and thus an estimate of the electrochemical surface area of the electrode. From most generalized to most automated, they are:
Finding the right timespans, getting the data as numpy vectors with
grab()
, and integrating withtrapz()
Selecting the data with
CyclicVoltammagram
indexing andselect_sweep()
, and integrating withintegrate()
Using
subtract()
to get aCyclicVoltammagramDiff
object that does the analysis.
Then, you will, on your own, choose whichever method you like to calculate the amount of charge associated with the reduction of an oxide layer, which gives an estimate of the thickness of the oxide layer.
Setup
We’ll use numpy
and pathlib.Path
as well as ixdat
’s CyclicVoltammagram
measurement type.
[1]:
import numpy as np
from pathlib import Path
from ixdat.techniques import CyclicVoltammogram
importing ixdat v0.2.7.dev1 from C:\Users\Søren\git\ixdat\src\ixdat\__init__.py
Loading raw data
[2]:
# Below is An option to load from file. Today we will load from url
# co_strip = CyclicVoltammagram.read(d"../loading_appending_and_saving/co_strip.csv", reader="ixdat")
if True: # Set this to False for offline work (requires you have downloaded the data file.)
co_strip = CyclicVoltammogram.read_url(
"https://raw.githubusercontent.com/ixdat/tutorials/43e85d07254e67e950f9c7081ffe1fa7f053cc08/L3_data_structure/exports/co_strip.csv",
reader="ixdat"
)
else:
co_strip = CyclicVoltammogram.read(
"./data/co_strip.csv",
reader="ixdat"
)
skipping the following line:
ixdat version = 0.2.0dev
skipping the following line:
backend_name = BackendBase(none, address=none)
[3]:
co_strip.plot()
[3]:
<Axes: xlabel='$U_{RHE}$ / [V]', ylabel='<I>/mA'>
Selecting and calibrating
[4]:
co_strip.calibrate(
# RE_vs_RHE=0.715, # NOTE, the potential was already calibrated,
# and recalibrating after exporting and reading
# causes problems in ixdat v0.2.0 :(
A_el=0.196,
R_Ohm=100
)
help(co_strip.calibrate)
Help on method calibrate in module ixdat.measurements:
calibrate(*args, **kwargs) method of ixdat.techniques.cv.CyclicVoltammogram instance
Add a calibration of the Measurement's default calibration type
The calibration class is determined by the measurement's `technique`.
*args and **kwargs are passed to the calibration class's `__init__`.
Raises:
TechniqueError if no calibration class for the measurement's technique
[5]:
co_strip.grab(co_strip.U_name)
[5]:
(array([ 0. , 1. , 2. , ..., 499.49669385,
499.65079403, 499.80489397]),
array([0.40165539, 0.40083473, 0.39980446, ..., 0.38106973, 0.37801271,
0.37514458]))
[6]:
co_strip.plot_measurement()
co_strip.plot()
[6]:
<Axes: xlabel='$U_{RHE}$ / [V] $_{ohm. corr.}$', ylabel='J / [mA cm$^{-2}$]'>
Method 1: grab()
and np.trapz
[7]:
co_strip.plot_measurement(tspan=[180, 220])
co_strip.plot_measurement(tspan=[300, 340])
[7]:
[<Axes: xlabel='time / [s]', ylabel='$U_{RHE}$ / [V] $_{ohm. corr.}$'>,
<Axes: ylabel='J / [mA cm$^{-2}$]'>]
[8]:
tspan_strip = [195, 215]
t_strip, I_strip = co_strip.grab("raw_current", tspan=tspan_strip)
tspan_base = [310, 330]
t_base, I_base = co_strip.grab("raw_current", tspan=tspan_base)
[9]:
print("got these vectors for the strip:")
print(f"t/[s] = {t_strip} \nand \nI/[mA] = {I_strip}")
print()
print(f"they have these shapes: {t_strip.shape} and {I_strip.shape}")
print()
print(f"And for the base, the vectors have shapes: {t_base.shape} and {I_base.shape}")
got these vectors for the strip:
t/[s] = [195.11739755 195.27149773 195.42559767 195.57969761 195.73379755
195.88789773 196.04199767 196.19609761 196.35019755 196.50429749
196.65839767 196.81249762 196.96659756 197.1206975 197.27479768
197.42889762 197.58299756 197.7370975 197.89119768 198.04529762
198.19939756 198.35349751 198.50759768 198.66169763 198.81579757
198.96989751 199.12399769 199.27809763 199.43219757 199.58629751
199.74039745 199.89449763 200.04859757 200.20269752 200.35679746
200.51089764 200.66499758 200.81909752 200.97319746 201.12729764
201.28139758 201.43549752 201.58959746 201.74369764 201.89779758
202.05189753 202.20599747 202.36009765 202.51419759 202.66829753
202.82239747 202.97649741 203.13059759 203.28469753 203.43879747
203.59289742 203.74699759 203.90109754 204.05529761 204.20929742
204.3634975 204.51749754 204.67169762 204.82569742 204.9798975
205.13389754 205.28809738 205.44209743 205.5962975 205.75029755
205.90449739 206.05849743 206.21269751 206.36669755 206.52089739
206.67489743 206.82909751 206.98309755 207.13729739 207.29129744
207.44549751 207.59949756 207.7536974 207.90769744 208.06189752
208.21589756 208.3700974 208.52409744 208.67829752 208.83229756
208.9864974 209.14049745 209.29469752 209.44869757 209.60289741
209.75689745 209.91109753 210.06509733 210.21929741 210.37329745
210.52749753 210.68149734 210.83569741 210.98979735 211.14389753
211.29799747 211.45209742 211.60619736 211.76029754 211.91439748
212.06849742 212.22259736 212.3766973 212.53079748 212.68489742
212.83899736 212.99309731 213.14719748 213.30129743 213.45539737
213.60949731 213.76359749 213.91769743 214.07179737 214.22589731
214.37999749 214.53409743 214.68819737 214.84229732 214.9963975 ]
and
I/[mA] = [0.00029328 0.00029863 0.0003056 0.00030855 0.00031173 0.00031905
0.00032301 0.00032645 0.00033343 0.00034102 0.00034388 0.00034984
0.00035995 0.00036629 0.0003718 0.0003836 0.00039228 0.00040075
0.00041476 0.00042947 0.00044306 0.00046112 0.00048421 0.00050812
0.00053865 0.00057995 0.00063175 0.00069877 0.00080024 0.00095775
0.00122753 0.00171086 0.00241396 0.00320765 0.00400408 0.00482864
0.00551815 0.0059437 0.00628901 0.00688835 0.0078873 0.00813825
0.00775796 0.00745258 0.00673803 0.00577352 0.00494863 0.00480672
0.00642391 0.01105442 0.015974 0.01828982 0.02221775 0.02472684
0.02345115 0.02287854 0.01666988 0.01464315 0.01418325 0.01811977
0.01616298 0.00990621 0.00939982 0.01120861 0.01151347 0.01050372
0.01188029 0.01532736 0.01868608 0.0198799 0.01629252 0.01215194
0.01057014 0.01099351 0.01372425 0.01875543 0.02380865 0.02363272
0.01890809 0.02055336 0.02916847 0.029096 0.0198015 0.01346152
0.00950942 0.00812947 0.00680537 0.00581626 0.00525828 0.00483008
0.00449103 0.00422481 0.00390258 0.00366777 0.00361393 0.00361808
0.00358995 0.00359514 0.00360841 0.00360258 0.00361546 0.00364085
0.00364237 0.00367725 0.00368028 0.003709 0.00369773 0.0037285
0.0037514 0.00376652 0.00377703 0.00380757 0.00380812 0.00381768
0.00384515 0.00385362 0.00388399 0.00389525 0.00390544 0.00391151
0.00391097 0.0039572 0.00395626 0.00397375 0.00400601 0.00401037
0.00402053 0.00405869 0.00406987 0.00410532]
they have these shapes: (130,) and (130,)
And for the base, the vectors have shapes: (130,) and (130,)
Checking what we’ve got
[10]:
from matplotlib import pyplot as plt
fig, ax1 = plt.subplots()
ax1.plot(t_strip, I_strip)
ax1.set_xlabel("time / [s]")
ax1.set_ylabel("current / [mA]")
ax1.set_title("stripping current")
[10]:
Text(0.5, 1.0, 'stripping current')
[11]:
fig, ax2 = plt.subplots()
ax2.plot(t_base, I_base, label="base")
ax2.plot(t_strip, I_strip, label="strip")
ax2.legend()
ax2.set_xlabel("time / [s]")
ax2.set_ylabel("current / [mA]")
ax2.set_title("strip and base current vs time")
[11]:
Text(0.5, 1.0, 'strip and base current vs time')
[12]:
v_strip = co_strip.grab_for_t("potential", t_strip)
v_base = co_strip.grab_for_t("potential", t_base)
fig, ax = plt.subplots()
ax.plot(v_base, I_base, color="k", label="base")
ax.plot(v_strip, I_strip, color="g", label="strip")
ax.legend()
ax.set_xlabel("potential / [V]")
ax.set_ylabel("current / [mA]")
[12]:
Text(0, 0.5, 'current / [mA]')
[13]:
tspan_base = [311.5, 331.5]
t_base, I_base = co_strip.grab("raw_current", tspan=tspan_base)
v_base = co_strip.grab_for_t("potential", t_base)
fig, ax = plt.subplots()
ax.plot(v_base, I_base, color="k", label="base")
ax.plot(v_strip, I_strip, color="g", label="strip")
ax.legend()
ax.set_xlabel(co_strip.U_name)
ax.set_ylabel("current / [mA]")
[13]:
Text(0, 0.5, 'current / [mA]')
And do the integration!
[14]:
Q_strip = np.trapz(I_strip, t_strip) * 1e-3 # converts mC --> C
Q_base = np.trapz(I_base, t_base) * 1e-3
Q_CO_ox = Q_strip - Q_base
from ixdat.constants import FARADAY_CONSTANT
# CO + H2O --> CO2 + 2(H+ + e-)
n_CO_ox = Q_CO_ox / (FARADAY_CONSTANT * 2)
print(f"charge passed = {Q_CO_ox*1e6} uC, corresponding to {n_CO_ox*1e9} nmol of CO oxidized")
charge passed = 94.95604644970163 uC, corresponding to 0.49207503544863207 nmol of CO oxidized
Method 2: Sweep selection and integrate()
[15]:
co_strip.plot_measurement(
J_name="cycle"
)
[15]:
[<Axes: xlabel='time / [s]', ylabel='$U_{RHE}$ / [V] $_{ohm. corr.}$'>,
<Axes: ylabel='cycle'>]
[16]:
co_strip.redefine_cycle(start_potential=0.3, redox=False)
co_strip.plot_measurement(J_name="cycle")
[16]:
[<Axes: xlabel='time / [s]', ylabel='$U_{RHE}$ / [V] $_{ohm. corr.}$'>,
<Axes: ylabel='cycle'>]
[17]:
co_strip[1].plot()
[17]:
<Axes: xlabel='$U_{RHE}$ / [V] $_{ohm. corr.}$', ylabel='J / [mA cm$^{-2}$]'>
The code below selects two cycles from the CO stripping experiment.
The code in the next bloc calculates the amount of CO according to:
$n_{CO} = \frac{1}{2 \mathcal{F}} \int{0.6 V{RHE}}^{1.0 V_{RHE}} ( I_{strip} - I_{base} ) :nbsphinx-math:`mathrm{d}`t $
[18]:
stripping_cycle = co_strip[1]
base_cycle = co_strip[2]
ax = stripping_cycle.plot(color="green", label="strip")
base_cycle.plot(ax=ax, color="black", label="base")
ax.legend()
ax.get_figure().tight_layout()
ax.get_figure().savefig("02_two_cycles.png")
[19]:
vspan = [0.6, 1.0]
stripping_sweep = stripping_cycle.select_sweep(vspan=vspan)
base_sweep = base_cycle.select_sweep(vspan=vspan)
stripping_sweep # to show what you get from this
[19]:
CyclicVoltammogram(id=5, name='extended_platinum_ec')
[20]:
ax = stripping_sweep.plot(color="g")
base_sweep.plot(color="k", ax=ax)
[20]:
<Axes: xlabel='$U_{RHE}$ / [V] $_{ohm. corr.}$', ylabel='J / [mA cm$^{-2}$]'>
[21]:
Q_strip = stripping_sweep.integrate("raw_current", ax="new") * 1e-3
Q_base = base_sweep.integrate("raw_current", ax="new") * 1e-3
Q_CO_ox = Q_strip - Q_base
n_CO_ox = Q_CO_ox / (FARADAY_CONSTANT * 2)
print(f"charge passed = {Q_CO_ox*1e6} uC, corresponding to {n_CO_ox*1e9} nmol of CO oxidized")
charge passed = 95.14178417627072 uC, corresponding to 0.49303755339038363 nmol of CO oxidized
Method 3: CyclicVoltammagramDiff
[22]:
stripping_cycle = co_strip[1]
base_cycle = co_strip[2]
ax = stripping_cycle.plot(color="g")
base_cycle.plot(ax=ax, color="k")
[22]:
<Axes: xlabel='$U_{RHE}$ / [V] $_{ohm. corr.}$', ylabel='J / [mA cm$^{-2}$]'>
[23]:
cv_diff = stripping_cycle.diff_with(base_cycle)
cv_diff.plot()
C:\Users\Søren\git\ixdat\src\ixdat\techniques\analysis_tools.py:92: RuntimeWarning: invalid value encountered in divide
v_scan_behind = (v - v_behind) / (t - t_behind)
C:\Users\Søren\git\ixdat\src\ixdat\techniques\analysis_tools.py:93: RuntimeWarning: invalid value encountered in divide
v_scan_ahead = (v_ahead - v) / (t_ahead - t)
[23]:
<Axes: xlabel='$U_{RHE}$ / [V] $_{ohm. corr.}$', ylabel='J / [mA cm$^{-2}$]'>
[24]:
cv_diff.plot_diff()
[24]:
<Axes: >
[25]:
cv_diff.plot_measurement()
[25]:
[<Axes: xlabel='time / [s]', ylabel='$U_{RHE}$ / [V] $_{ohm. corr.}$'>,
<Axes: ylabel='J / [mA cm$^{-2}$]'>]
[26]:
Q_CO_ox = cv_diff.integrate("raw_current", vspan=[0.6, 1.0]) * 1e-3 # 1e-3 converts mC --> C
n_CO_ox = Q_CO_ox / (FARADAY_CONSTANT * 2)
print(f"charge passed = {Q_CO_ox*1e6} uC, corresponding to {n_CO_ox*1e9} nmol of CO oxidized")
charge passed = 95.08391830687209 uC, corresponding to 0.49273768465321294 nmol of CO oxidized
Your turn!
[27]:
# oxide_reduction = CyclicVoltammagram.read(data_directory / "oxide_reduction.csv", reader="ixdat")
if True: # Set this to False for offline work (requires you have downloaded the data file.)
oxide_reduction = CyclicVoltammogram.read_url(
"https://raw.githubusercontent.com/ixdat/tutorials/43e85d07254e67e950f9c7081ffe1fa7f053cc08/L3_data_structure/exports/oxide_reduction.csv",
reader="ixdat"
)
else:
oxide_reduction = CyclicVoltammogram.read(
"./data/oxide_reduction.csv", reader="ixdat"
)
oxide_reduction.calibrate(A_el=0.196, R_Ohm=100)
oxide_reduction.tstamp += oxide_reduction.t[0]
oxide_reduction.plot_measurement()
oxide_reduction.plot(tspan=[300, 800])
skipping the following line:
ixdat version = 0.2.0dev
skipping the following line:
backend_name = BackendBase(none, address=none)
[27]:
<Axes: xlabel='$U_{RHE}$ / [V] $_{ohm. corr.}$', ylabel='J / [mA cm$^{-2}$]'>