An Electrochemical Impedance Spectrum in 3D

This notebook contains a visualization of a simple equivalent circuit, -R-(RC)-, in 3D where the axes used corresponds to the frequency range used to obtain the data (in this case for calculation of the frequency dependent impedance of the circuit), and the real and imaginary parts of the complex impedance.

Common visualizations of this kind of data include a complex plane plot (a Nyquist plot) and a plot of the impedance modulus (or the real and imaginary components) versus the logarithm of the frequency (a Bode plot), which corresponds to the wall projections in the 3D plot below.


In [2]:
#python dependencies
#uncomment the following line if using Python 2
#from __future__ import division
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

# This is the 3D plotting toolkit
from mpl_toolkits.mplot3d import Axes3D

In [3]:
#change the default font set (matplotlib.rc)
mpl.rc('mathtext', fontset='stixsans', default='regular')

#increase text size somewhat
mpl.rcParams.update({'axes.labelsize':12, 'font.size': 12})

In [4]:
#set up notebook for inline plots
%matplotlib inline

In [5]:
#generate a frequency range

def freq_range(fstart = 9685000, fstop = 0.0968, pts_per_decade = 12):
    """returns a frequency range in terms as a numpy array, 
       between fstart [Hz] and fstop [Hz], with a set number
       of points per decade (defaults to 12)"""
    decades = np.log10(fstart)-np.log10(fstop)
    pts_total = np.around(decades*pts_per_decade)
    frange = np.logspace(np.log10(fstop),np.log10(fstart), pts_total, endpoint=True)
    return frange

def freq_to_ang_freq(frange):
    """returns the angular frequency in rad/s, input is frequency in Hz,
       works on arrays as well as single numbers"""
    w = 2*np.pi*frange #angular frequency
    return w

w = freq_to_ang_freq(freq_range())

In [6]:
#define parameters for a -R-(RC)- circuit

R0 = 5 #Ohm
R1 = 10 #Ohm
C1 = 10e-6 #F

In [7]:
#define the impedance of the elements in the circuit and add them up
Z_R0 = R0
Z_R1 = R1
Z_C1 = -1j/(w*C1) #capacitive reactance

#R1 and C1 connected in parallel
Z_RC = 1/(1/Z_R1 + 1/Z_C1) #parallel connection

#Z_R0 and Z_RC connected in series
Z = Z_R0 + Z_RC

In [8]:
#2D plot (Nyquist / Complex Plane)

fig = plt.figure()
ax1 = fig.add_subplot(111, aspect='equal')
ax1.plot(Z.real, -1*Z.imag, marker='o',ms=5, mec='b', mew=0.7, mfc='none')
ax1.set_xlim(0,20)
ax1.set_ylim(0,6)
ax1.set_xlabel('Z$_{real}$ [$\Omega$]')
ax1.set_ylabel('-Z$_{imag}$ [$\Omega$]')


Out[8]:
<matplotlib.text.Text at 0x7f269a9dc240>

In [9]:
max(np.log10(freq_range())), min(np.log10(freq_range()))


Out[9]:
(6.9860996250551297, -1.0141246426916064)

In [10]:
#3D plot (Z.real, Z.imag, f)

fig = plt.figure()
ax = plt.axes(projection='3d')
#ax = fig.gca(projection='3d')

x = Z.real
z = -1*Z.imag
y = np.log10(freq_range())

#add 3D curve
ax.plot(x, y, z, marker='o', color='b', mfc='none', mec='b', ms=4)
ax.set_xlabel('Z$_{real}$ [$\Omega$]')
ax.set_zlabel('-Z$_{imag}$ [$\Omega$]')
ax.set_ylabel('log (f/Hz)')

#set axes equal between x and z
#not sure how to do this in 3D, works with ax.axis('equal') or ax.set_aspect('equal') in 2D... 

#set axis limits
ax.set_zlim(0, 6)
ax.set_ylim(-2,7)
xlims = ax.get_xlim()

#add wall projections by plotting z vs y, x vs y, and x vs z
ax.plot(x,7*np.ones(len(x)),z, color = 'grey', ls='-', alpha=0.5, marker='', ms=4) #x vs z
ax.plot(xlims[0]*np.ones(len(x)),y,z, color = 'grey', ls='-', alpha=0.5, marker='', ms=4) #z vs y
ax.plot(x,y,np.zeros(len(x)), color = 'grey', ls='-', alpha=0.5, marker='', ms=4) #x vs y

#ax.view_init(30, 90)
fig.tight_layout()


JR Johansson's excellent version information extension (see here for install and usage instructions):


In [11]:
%load_ext version_information

In [13]:
%version_information numpy, matplotlib


Out[13]:
SoftwareVersion
Python3.5.2 64bit [GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]
IPython5.1.0
OSLinux 4.4.0 53 generic x86_64 with debian stretch sid
numpy1.10.4
matplotlib1.5.1
Fri Dec 23 12:53:01 2016 GMT