-
Notifications
You must be signed in to change notification settings - Fork 0
/
plot.py
2126 lines (1918 loc) · 81.1 KB
/
plot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
""" Functions for making plots with IV data """
import inspect
import logging
import os
from collections import deque
from functools import wraps
from inspect import signature
from numbers import Number
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.widgets import SpanSelector, RectangleSelector, AxesWidget
import ivtools
import ivtools.analyze
log = logging.getLogger('plots')
def arrowpath(x, y, ax=None, **kwargs):
'''
Make a quiver style plot along a path
Draws one arrow per pair of data points
Should use interpolation or downsampling beforehand so the arrows are not too small
'''
if ax is None:
ax = plt.gca()
qkwargs = dict(scale_units='xy', angles='xy', scale=1, width=.005)
if 'c' in kwargs:
qkwargs['color'] = kwargs['c']
# only pass these keywords through
kws = ['alpha', 'scale', 'scale_units', 'width', 'headwidth', 'headlength',
'headaxislength', 'minshaft', 'minlength', 'color', 'pivot', 'label',
'clim', 'cmap', 'linestyle', 'zorder']
for k,v in kwargs.items():
if k in kws:
qkwargs[k] = v
quiv = ax.quiver(x[:-1], y[:-1], x[1:]-x[:-1], y[1:]-y[:-1], **qkwargs)
return quiv
def plot_multicolor(x, y, c=None, cmap='rainbow', vmin=None, vmax=None, linewidth=2, ax=None, **kwargs):
'''
Line plot whose color changes along its length
for n datapoints there are n-1 segments, so c should have length n-1
but if c longer, all points after n-1 are ignored
TODO: can we make multicolor work as expected when used as plotfunc for plotiv?
plotiv(data, plotfunc=plot_multicolor, c='time') should multicolor all the lines according to time.
but plotiv doesn't know whether to interpret the keyword c normally or to evaluate data[c] and pass that..
'''
from matplotlib.collections import LineCollection
if ax is None:
fig, ax = plt.subplots()
cm = plt.get_cmap(cmap)
if c is None:
#c = np.arange(len(x))
colors = cm(np.linspace(0, 1, len(x)))
else:
# be able to scale/clip the range of colors using vmin and vmax (like imshow)
cmin = np.min(c)
cmax = np.max(c)
if vmin is None:
vmin = cmin
if vmax is None:
vmax = cmax
scaledc = (c - vmin) / (vmax - vmin)
colors = cm(np.clip(scaledc, 0, 1))
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lc = LineCollection(segments, cmap=plt.get_cmap(cmap), joinstyle='round', capstyle='round')
#lc.set_array(c)
lc.set_color(colors)
lc.set_linewidth(linewidth)
valid_properties = lc.properties().keys()
valid_kwargs = {k:w for k,w in kwargs.items() if k in valid_properties}
lc.set(**valid_kwargs)
ax.add_collection(lc)
ax.autoscale()
return lc
def plot_multicolor_speed(x, y, t=None, cmap='rainbow', vmin=None, vmax=None, ax=None, **kwargs):
'''
Multicolor line that is colored by how far consecutive datapoints are from each other
I wrote this for representing time on an I-V plot.
If the samples are equally spaced in time, then the color indicates how fast I,V is changing
Does not look that great when there aren't a lot of samples.
TODO: we could do some kind of color interpolation in that case. Interpolate x,y,c and smooth c only
can't just interpolate to a huge number of timesteps,
because then we have to plot millions of line segments, mostly where "speed" is low
You can interpolate more when speed is higher, then pass the array of t to this function
'''
# absolute distance may not work if x and y have different units
# to account for scale, we divide by the range of the input data
xrange = np.max(x) - np.min(x)
yrange = np.max(y) - np.min(y)
speed = np.sqrt(np.gradient(x/xrange)**2 + np.gradient(y/yrange)**2)
if t is not None:
speed /= np.gradient(t)
kwargs = {k:v for k,v in kwargs.items() if k != 'c'}
plot_multicolor(x, y, c=speed, cmap=cmap, vmin=vmin, vmax=vmax, ax=ax, **kwargs)
return speed
def plotiv(data, x='V', y='I', c=None, ax=None, maxsamples=500000, cm='jet', xfunc=None, yfunc=None,
plotfunc=plt.plot, autotitle=False, labels=None, labelfmt=None, colorbyval=True,
hold=False, **kwargs):
'''
IV loop plotting which can handle single or multiple loops.
the point is mainly to do coloring and labeling in a nice way.
data structure should be dict-like or list-like of dict-like
Can plot any column vs any other column
Automatically labels the axes, with name and units if they are in the data structure
Can assign a colormap to the lines
if you want a single color for all lines, use the "color" keyword
Can transform the x and y data by passing functions to xfunc and/or yfunc arguments
if x and y specify a scalar value, then just one scatter plot is made
if x is None, then y is plotted vs range(len(y))
maxsamples : downsample to this number of data points if necessary
kwargs passed through to ax.plot (can customize line properties this way)
Maybe unexpected behavior: A new figure is created if ax=None
Can pass an arbitrary plotting function, which defaults to plt.plot
Could then define some other plots that take IV data and reuse plotiv functionality.
'''
if hold:
# you can type hold=1 instead of ax=plt.gca()
# or use hplotiv()
fig = plt.gcf()
ax = plt.gca()
if ax is None:
fig, ax = plt.subplots()
# check if plotfunc uses any of the same keywords as plotiv
plotiv_args = inspect.getfullargspec(plotiv).args
plotfunc_args = inspect.getfullargspec(plotfunc).args
overlap_args = set(plotiv_args) & set(plotfunc_args)
# We know how to deal with these ones
overlap_args -= set(('x', 'y', 'c', 'ax'))
if any(overlap_args):
log.warning(f'the following args are used by both plotiv and plotfunc, and will not pass through: {overlap_args}')
# might be one curve, might be many
dtype = type(data)
assert dtype in (list, dict, pd.Series, pd.DataFrame)
# Convert to a list of dict-like, so we can use a consistent syntax below
if dtype in (dict, pd.Series):
data = [data]
elif dtype == pd.DataFrame:
# actually a list of series is enough..
# otherwise you will cause errors when functions are passed as arguments that expect Series notation
# e.g. y=lambda y: y.V/y.I
indices, data = zip(*data.iterrows())
#data = data.to_dict(orient='records')
lendata = len(data)
##### Line coloring #####
if 'color' in kwargs:
# color keyword overrides everything
colors = [kwargs['color']] * len(data)
# Don't pass it through to the plot function, because passing c and color is an error now
del kwargs['color']
elif lendata > 1:
# There are several loops
# Pick colors for each line
# you can either pass a list of colors, or a colormap
if isinstance(cm, list):
colors = cm
else:
if isinstance(cm, str):
# Str refers to the name of a colormap
cmap = plt.cm.get_cmap(cm)
elif type(cm) in (mpl.colors.LinearSegmentedColormap, mpl.colors.ListedColormap):
cmap = cm
# TODO: add vmin and vmax arguments to stretch the color map
if c is None:
colors = [cmap(c) for c in np.linspace(0, 1, len(data))]
elif type(c) is str:
cdata = np.array([d[c] for d in data])
if colorbyval:
# color by value of the column given
cmax = np.max(cdata)
cmin = np.min(cdata)
normc = (cdata - cmin) / (cmax - cmin)
colors = cmap(normc)
else:
# this means we want to color by the category of the value in the column
# Should put in increasing order, but equally spaced on the color map,
# not proportionally spaced according to the value of the data column
uvals, category = np.unique(cdata, return_inverse=True)
colors = cmap(category / max(category))
else:
# It should be either a list of colors or a list of values
# Cycle through it if it's not long enough
firstval = next(iter(c))
if hasattr(firstval, '__iter__'):
#it's a list of strings, or of RGB values or something
colors = [c[i%len(c)] for i in range(lendata)]
else:
# It's probably an array of values? Map them to colors
normc = (c - np.min(c)) / (np.max(c) - np.min(c))
colors = cmap(normc)
else:
# Use default color cycling
colors = [None]
##### Line labeling #####
if labels is not None:
if type(labels) is str:
# TODO: allow passing a list of strings, which can label by multiple values
# but there is an ambiguity if the length of that list happens to be the
# same as the length of the data..
# label by the key with this name
if type(data) is pd.DataFrame:
label_list = list(data[labels])
else:
label_list = [d[labels] for d in data]
else:
# otherwise we will iterate through labels directly (so you can pass a list of labels)
# make np.nan count as None (not labelled)
label_list = list(map(lambda v: None if (isinstance(v, Number) and np.isnan(v)) else v, labels))
assert len(label_list) == len(data)
# reformat the labels in case they are numbers
if labelfmt:
label_list = list(map(lambda v: None if v is None else format(v, labelfmt), label_list))
else:
# even if we did not specify labels, we will still iterate through a list of labels
# Make them all None (unlabeled)
label_list = [None] * len(data)
# Drop repeat labels that have the same line style, because we don't need hundreds of repeat labeled objects
# right now only the color identifies the line style
# Python loop style.. will not be efficient, but it's the first solution I thought of.
lineset = set()
if lendata > 1:
for i in range(len(data)):
l = label_list[i]
cc = colors[i]
if type(cc) is np.ndarray:
# need a hashable type...
cc = tuple(cc)
if (l,cc) in lineset:
label_list[i] = None
else:
lineset.add((l,cc))
##### Come up with axis labels #####
if type(y) == str:
yname = y
elif hasattr(y, '__call__'):
yname = y.__name__
elif hasattr(y, '__iter__'):
# Don't know if this is a good idea
yname = '[{}, ..., {}]'.format(y[0], y[-1])
else:
raise Exception('I do not know wtf you are trying to plot')
if x is None:
xname = None
elif type(x) == str:
xname = x
elif hasattr(x, '__call__'):
xname = x.__name__
elif hasattr(x, '__iter__'):
# Don't know if this is a good idea
xname = '[{}, ..., {}]'.format(x[0], x[-1])
else:
raise Exception('I do not know wtf you are trying to plot')
defaultunits = {'V': ('Voltage', 'V'),
'Vcalc': ('Device Voltage', 'V'),
'Vd': ('Device Voltage', 'V'),
'I': ('Current', 'A'),
'G': ('Conductance', 'S'),
'R': ('Resistance', '$\Omega$'),
't': ('Time', 's'),
None: ('Data Point', '#')}
longnamex, unitx = x, '?'
longnamey, unity = y, '?'
if x in defaultunits.keys():
longnamex, unitx = defaultunits[x]
if y in defaultunits.keys():
longnamey, unity = defaultunits[y]
# Overwrite the label guess with value from dict if it exists
# Only consider the first data -- hopefully there are not different units passed at once
iv0 = data[0]
if ('longnames' in iv0.keys()) and (type(iv0['longnames']) == dict):
if x in iv0['longnames'].keys():
longnamex = iv0['longnames'][x]
if y in iv0['longnames'].keys():
longnamey = iv0['longnames'][y]
if ('units' in iv0.keys()) and (type(iv0['units']) == dict):
if x in iv0['units'].keys():
unitx = iv0['units'][x]
if y in iv0['units'].keys():
unity = iv0['units'][y]
xlabel = longnamex
if unitx != '?':
xlabel += f' [{unitx}]'
ylabel = longnamey
if unity != '?':
ylabel += f' [{unity}]'
if xfunc is not None:
xlabel = '{}({})'.format(xfunc.__name__, xlabel)
if yfunc is not None:
ylabel = '{}({})'.format(yfunc.__name__, ylabel)
# We will label the axes at the end, in case the plotfunc tries to set its own labels
##### Make the lines #####
lines = []
for iv, c, l in zip(data, colors, label_list):
ivtype = type(iv)
if ivtype not in (dict, pd.Series):
# what the F, you passed a list with something weird in it
log.error('plotiv did not understand the input datatype {}'.format(ivtype))
continue
## construct the x and y arrays that you actually want to plot
# Can pass non dict keys to plot on the x,y axes (func, list..)
if type(y) == str:
Y = iv[y]
elif hasattr(y, '__call__'):
# can pass a function as y, this will be called on the whole data structure
Y = y(iv)
else:
Y = y
if hasattr(Y, '__iter__'):
lenY = len(Y)
Yscalar = False
else:
lenY = 1
Yscalar = True
if x is None:
X = np.arange(lenY)
elif type(x) == str:
X = iv[x]
elif hasattr(x, '__call__'):
X = x(iv)
else:
X = x
if hasattr(X, '__iter__'):
lenX = len(X)
Xscalar = False
else:
lenX = 1
Xscalar = True
if xfunc is not None:
X = xfunc(X)
if yfunc is not None:
Y = yfunc(Y)
# X and Y should be the same length, if they are not, truncate one
if lenX != lenY:
log.warning('_plot_single_iv: X and Y arrays are not the same length! Truncating the longer one.')
if lenX > lenY:
X = X[:lenY]
lenX = lenY
else:
Y = Y[:lenX]
lenY = lenX
if maxsamples is not None and maxsamples < lenX:
# Down sample data
log.warning('Downsampling data for plot!!')
step = int(lenX/maxsamples)
X = X[np.arange(0, lenX, step)]
Y = Y[np.arange(0, lenY, step)]
if Xscalar and Yscalar:
# there's only one datapoint per iv loop
# the way this is set up, we plot one line per iv loop
# so we cannot easily connect the points in the plot
# Will be invisible if e.g. plotfunc == plt.plot and there's no marker
#if plotfunc == plt.plot
# plotfunc = plt.scatter
pass
plotfunc_kwargs = dict(c=c, label=l)
if 'ax' in plotfunc_args:
# If the plotfunc takes an axis argument, pass it through,
plotfunc_kwargs['ax'] = ax
else:
# otherwise have to assume it plots on the current axis..
plt.sca(ax)
# all plotiv kwargs get passed through to plotfunc, even if they overwrite something (i.e. label)
plotfunc_kwargs.update(kwargs)
if ('x' not in plotfunc_args) or ('y' not in plotfunc_args):
# stupid plotfunc didn't label x and y keywords, assume they are the first two arguments
newline = plotfunc(X, Y, **plotfunc_kwargs)
else:
newline = plotfunc(x=X, y=Y, **plotfunc_kwargs)
# probably going to be a list of length 1 lists...
lines.append(newline)
# Use EngFormatter if the plotted values are small or large
xlims = np.array(ax.get_xlim())
if any(xlims > 1e3) or all(xlims < 1e-1):
ax.xaxis.set_major_formatter(mpl.ticker.EngFormatter())
ylims = np.array(ax.get_ylim())
if any(ylims > 1e3) or all(ylims < 1e-1):
ax.yaxis.set_major_formatter(mpl.ticker.EngFormatter())
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
# put on a legend if there are labels
if labels and ((type(labels) is str) or any(labels)):
leg = ax.legend()
if type(labels) is str:
leg.set_title(labels)
elif hasattr(labels, 'name'):
leg.set_title(labels.name)
if autotitle:
auto_title(data, keys=None, ax=ax)
# should I really return this? usually I don't assign the values and then they get cached by ipython forever
# Can always get them with plt.gca()...
# return ax, line
return
@wraps(plotiv)
def hplotiv(*args, **kwargs):
'''
Cute and fast way to plot on the current axis
inspired by Julias plot!()
prepended an h because it's easy in the console to go to the beginning of a linewith ctrl-a
'''
plotiv(*args, **kwargs, hold=True)
## Linearized plots for conduction mechanisms
def schottky_plot(data, V='V', I='I', T=None):
# Linearizes schottky mechanism
# log(I) or log(I)/T^2 vs sqrt(v)
# Should I use ln on the data or use the log scale?
fig, ax = plt.subplots()
ax.set_yscale('log')
if T is not None:
# Assuming data is a dataframe. Will kick myself later.
data = data.assign(**{'I/T2': data['I'] / data['T']**2})
plotiv(data, V, 'I/T2', xfunc=np.sqrt, ax=ax)
ax.set_ylabel(f'{I} / {T}$^2$')
else:
plotiv(data, V, I, xfunc=np.sqrt, ax=ax)
ax.set_ylabel(f'{I}')
ax.set_xlabel(f'sqrt({V})')
def poole_frenkel_plot(data, V='V', I='I', T=None):
# Linearizes P-F mechanism
# log(G) or log(G)/T^2 vs sqrt(v)
# Should I use ln on the data or use the log scale?
data = data.assign(G=data[I]/data[V])
fig, ax = plt.subplots()
ax.set_yscale('log')
if T is not None:
# Assuming data is a dataframe. Will kick myself later.
data = data.assign(**{'G/T2': data['G'] / data['T']**2})
plotiv(data, V, 'G/T2', xfunc=np.sqrt, ax=ax)
ax.set_ylabel(f'G / {T}$^2$')
else:
plotiv(data, V, 'G', xfunc=np.sqrt, ax=ax)
ax.set_ylabel('G')
ax.set_xlabel(f'sqrt({V})')
def arrhenius_plot(data, V='V', I='I', T='T', numv=20, minv=None, maxv=None, cmap=plt.cm.viridis, **kwargs):
# Thermal activation plot -- needs some work though
# log(I) or log(G) vs 1000/T
# This is a little tricky because voltage values need to be interpolated in general
# If I is multi-valued in voltage then it can be a pain in the ass to interpolate
# for each interpolated value of V, we plot a line
# Not using plotiv because I couldn't think of the smart way to "pivot" the nested dataframe
# Should output the "interpolated pivot" data for fitting
plt.figure()
if maxv is None:
maxv = np.max(data[V].apply(np.max))
if minv is None:
minv = 0.05
vs = np.linspace(minv, maxv, numv)
colors = cmap(np.linspace(0, 1, len(vs)))
fits = []
for v,c in zip(vs, colors):
#it = ivtools.analyze.interpiv(data, v, column=V, fill_value=np.nan, findmonotonic=False)
it = ivtools.analyze.interpiv(data, v, column=V)
it = it.dropna(0, how='any', subset=[I,T])
plt.plot(1000/it[T], it[I], marker='.', color=c, label=format(v, '.2f'), **kwargs)
plt.yscale('log')
#notnan = ~it['I'].isnull()
#fits.append(polyfit(1/it['T'][notnan], log(it['G'][notnan]), 1))
#fitx = np.linspace(1/300, 1/81)
#color = ax.lines[-1].get_color()
#plt.plot(fitx, np.polyval(fits[-1], fitx), color=color, alpha=.7)
#ax.lines[-1].set_label(None)
#plt.legend(title='Device Voltage')
colorbar_manual(minv, maxv, cmap=cmap, label='Applied Voltage [V]')
plt.xlabel('Ambient Temperature [K] (scale 1/T)')
plt.ylabel('Current [A]')
formatter = mpl.ticker.FuncFormatter(lambda x, y: format(1000/x, '.0f'))
plt.gca().xaxis.set_major_formatter(formatter)
def auto_title(data, keys=None, ax=None):
'''
Label an axis to identify the device.
Quickly written, needs improvement
'''
if ax is None:
ax = plt.gca()
if type(data) is pd.DataFrame:
meta = data.iloc[0]
else:
meta = data
def safeindex(data, key):
if key in data:
return data[key]
else:
return '?'
if keys is None:
# Default behavior
idkeys = ['dep_code','sample_number','module','device']
id = '_'.join([format(safeindex(meta, idk)) for idk in idkeys])
otherkeys = ['layer_1', 'thickness_1', 'width_nm', 'R_series']
othervalues = [safeindex(meta, k) for k in otherkeys]
# use kohm if necessary
if othervalues[3] != '?' and othervalues[3] >= 1000:
othervalues[3] = str(int(othervalues[3]/1000)) + 'k'
formatstr = '{}, {}, t={}nm, w={}nm, Rs={}$\Omega$'
title = formatstr.format(id, *othervalues)
else:
title = ', '.join(['{}:{}'.format(k, safeindex(meta, k)) for k in keys])
ax.set_title(title)
return title
def plot_R_states(data, v0=.1, v1=None, **kwargs):
resist_states = ivtools.analyze.resistance_states(data, v0, v1)
resist1 = resist_states[0]
resist2 = resist_states[1]
if type(resist1) is pd.Series:
cycle1 = resist1.index
cycle2 = resist2.index
else:
cycle1 = cycle2 = range(len(resist1))
fig, ax = plt.subplots()
scatterargs = dict(s=10, alpha=.8, edgecolor='none')
scatterargs.update(kwargs)
ax.scatter(cycle1, resist1, c='royalblue', **scatterargs)
ax.scatter(cycle2, resist2, c='seagreen', **scatterargs)
#ax.legend(['HRS', 'LRS'], loc=0)
engformatter('y', ax)
ax.set_xlabel('Cycle #')
ax.set_ylabel('Resistance [$\\Omega$]')
def violinhist(data, x, range=None, bins=50, alpha=.8, color=None, logbin=True, logx=True, ax=None,
label=None, fixscale=None, sharescale=True, hlines=True, vlines=True, **kwargs):
'''
histogram version of violin plot (when there's not a lot of data so the KDE looks weird)
Can handle log scaling the x-axis, which plt.violinplot cannot do
widths are automatically scaled, attempting to make them visible and not overlapping
data should be a list of arrays of values
kwargs go to plt.bar
This was pretty difficult to write -- mostly because I want the log ticks..
TODO: could extend to make a real violin plot by increasing # of bins, adding some gaussian noise to the data (dithering), and doing line plots
'''
if ax is None:
ax = plt.gca()
if color is None:
color = ax._get_lines.get_next_color()
# sort data
order = np.argsort(x)
x = np.array(x)[order]
data = [data[o] for o in order]
if range is None:
#range = np.percentile(np.concatenate(data), (1,99))
alldata = np.concatenate(data)
range = (np.min(alldata), np.max(alldata))
# values will be converted back to linear scale before plotting
# so that we can use the log-scale axes
if logbin:
data = [np.log(d) for d in data]
range = np.log(range)
ax.set_yscale('log')
if logx:
x = np.log(x)
ax.set_xscale('log')
dx = np.diff(x)
# Calculate stats
a = np.array
n = a([len(d) for d in data])
means = a([np.mean(d) for d in data])
medians = a([np.median(d) for d in data])
maxs = a([np.max(d) for d in data])
mins = a([np.min(d) for d in data])
p99s = a([np.percentile(d, 99) for d in data])
p01s = a([np.percentile(d, 1) for d in data])
# Calculate the histograms
hists = []
for d,xi in zip(data, x):
edges = np.linspace(*range, bins+1)
hist, edges = np.histogram(d, bins=edges)
#if logbin:
# normalize to account for different bin widths
# this only needs to be done if we are putting a log binned dataset back onto a linear scale! we are not!
# hist = hist / np.diff(np.exp(edges))
hists.append((hist, edges))
# Figure out how to scale the bin heights
if fixscale:
# fixscale overrides everything, useful for manual tuning or if you need to plot many
# different violinhists on the same plot and want all the bin heights to be consistent
hists = [(h*fixscale, e) for h,e in hists]
else:
# we don't want violins to overlap, and we don't want one to look much bigger than any other
maxscale = 0.49
maxamp = np.min(dx) * maxscale
if sharescale:
# Usually I will want 1 sample to correspond to the same height everywhere (sharescale)
# scale all hists by the same factor so that the globally maximum bin reaches maxamp
maxbin = np.max([h for h,e in hists])
hists = [(h*maxamp/maxbin, e) for h,e in hists]
else:
# scale every hist to maxscale
hists = [(h*maxamp/np.max(h), e) for h,e in hists]
# return data to normal scale by overwriting. This is awful.
if logx:
x = np.exp(x)
hists = [(np.exp(hist), edges) for (hist, edges) in hists]
if logbin:
hists = [(hist, np.exp(edges)) for (hist, edges) in hists]
maxs = np.exp(maxs)
mins = np.exp(mins)
means = np.exp(means)
medians = np.exp(medians)
p99s = np.exp(p99s)
p01s = np.exp(p01s)
range = np.exp(range)
# Plot the histograms
for (hist, edges), xi in zip(hists, x):
heights = np.diff(edges)
if logx:
ax.barh(edges[:-1], xi*hist - xi, height=heights, align='edge', left=xi, color=color, alpha=alpha, linewidth=1, label=label, **kwargs)
label = None # only label the first one
ax.barh(edges[:-1], xi/hist - xi, height=heights, align='edge', left=xi, color=color, alpha=alpha, linewidth=1, label=label, **kwargs)
else:
ax.barh(edges[:-1], hist, height=heights, align='edge', left=xi, color=color, alpha=alpha, linewidth=1, label=label, **kwargs)
label = None # only label the first one
ax.barh(edges[:-1], -hist, height=heights, align='edge', left=xi, color=color, alpha=alpha, linewidth=1, label=label, **kwargs)
# Plot hlines (why not just a plt.box? I forgot why.)
if hlines:
barwidth = np.min(dx * .2)
midscale = .5
if logx:
# I don't understand how I ever ended up with this code..
#ax.hlines([mins, means ,maxs], x*np.exp(-barwidth) - x, x*np.exp(barwidth) - x, colors=color)
#ax.hlines(mins, x*np.exp(-barwidth), x*np.exp(barwidth), colors=color)
#ax.hlines(maxs, x*np.exp(-barwidth), x*np.exp(barwidth), colors=color)
ax.hlines(p01s, x*np.exp(-barwidth), x*np.exp(barwidth), colors=color)
ax.hlines(p99s, x*np.exp(-barwidth), x*np.exp(barwidth), colors=color)
ax.hlines(medians, x*np.exp(-barwidth*midscale), x*np.exp(barwidth*midscale), colors=color)
else:
#ax.hlines([mins, means ,maxs], x-barwidth, x+barwidth, colors=color)
ax.hlines(mins, x-barwidth, x+barwidth, colors=color)
ax.hlines(maxs, x-barwidth, x+barwidth, colors=color)
ax.hlines(medians, x-barwidth*.5, x+barwidth*.5, colors=color)
if vlines:
if vlines == 'full':
for xx in x:
ax.axvline(xx, color=color)
else:
ax.vlines(x, mins, maxs, colors=color)
# only label the x axis where there are histograms
ax.xaxis.set_ticks(x)
ax.xaxis.set_ticklabels(x)
ax.xaxis.set_major_formatter(mpl.ticker.ScalarFormatter())
ax.xaxis.set_minor_formatter(mpl.ticker.NullFormatter())
r0, r1 = range
if logbin:
ax.set_ylim(r0, r1)
else:
m = (r0 + r1) / 2
ax.set_ylim((r0-m)*1.05 + m, (r1-m)*1.05 +m)
def violinhist_fromdf(df, col, xcol, **kwargs):
histd = []
histx = []
for k, g in df.groupby(xcol):
histx.append(k)
histd.append(g[col].values)
violinhist(histd, histx, **kwargs)
def grouped_hist(df, col, groupby=None, range=None, bins=30, logx=True, ax=None):
'''
Histogram where the bars are split into colors by group
I don't know if this is a good idea or not. maybe for < 4 groups
'''
if ax is None:
ax = plt.gca()
if range is None:
# every subset needs to have the same range
if logx:
range = np.nanpercentile(df[col][df[col] > 0], (0, 100))
else:
range = np.nanpercentile(df[col], (0, 100))
params = []
hists = []
for k, g in df.groupby(groupby):
if logx:
x = np.log10(g[col][g[col] > 0])
logrange = [np.log10(v) for v in range]
hist, edges = np.histogram(x[~np.isnan(x)], bins=bins, range=logrange)
if any(hist < 0):
log.error('wtf')
edges = 10**edges
ax.set_xscale('log')
else:
hist, edges = np.histogram(g[col][~np.isnan(g[col])], bins=bins, range=range)
params.append(k)
hists.append(hist)
# edges will all be the same
widths = np.diff(edges)
heights = np.stack(hists)
bottoms = np.vstack((np.zeros(bins), np.cumsum(heights, 0)[:-1]))
tops = bottoms + heights
for p,bot,height in zip(params, bottoms, heights):
ax.bar(edges[:-1], height, widths, align='edge', bottom=bot, label=p, edgecolor='white')
ax.legend(title=groupby)
ax.set_xlabel(col)
ax.set_ylabel('N')
def paramplot(df, x, y, parameters, yerr=None, cmap=plt.cm.gnuplot, labelformatter=None,
sparseticks=True, xlog=False, ylog=False, sortparams=False, paramvals=None,
ax=None, **kwargs):
'''
line plot y vs x, grouping lines by any number of parameters
Can choose a subset of the parameter values to plot, and the colors will be the same as if the
subset was not passed. does that make any sense? sorry.
'''
if ax is None:
fig, ax = plt.subplots()
else:
fig = ax.get_figure()
fig.set_tight_layout(True)
if xlog:
ax.set_xscale('log')
if ylog:
ax.set_yscale('log')
if type(parameters) == str:
parameters = [parameters]
grp = df.groupby(parameters, sort=sortparams)
ngrps = len(grp)
colors = cmap(np.linspace(.1, .9, ngrps))
colordict = {k:c for k,c in zip(grp.groups.keys(), colors)}
for k, g in grp:
if paramvals is None or k in paramvals:
# Figure out how to label the lines
if type(k) == tuple:
if labelformatter is not None:
label = labelformatter.format(*k)
else:
label = ', '.join(map(str, k))
else:
if labelformatter is not None:
label = labelformatter.format(k)
else:
label = str(k)
plotkwargs = dict(color=colordict[k],
marker='.',
label=label)
plotkwargs.update(kwargs)
plotg = g.sort_values(by=x)
ax.plot(plotg[x], plotg[y], **plotkwargs)
if yerr is not None:
ax.errorbar(plotg[x], plotg[y], plotg[yerr], color=colordict[k], label=None)
if sparseticks:
# Only label the values present
ux = np.sort(df[x].unique())
ax.xaxis.set_ticks(ux)
ax.xaxis.set_ticklabels(ux)
ax.xaxis.set_minor_formatter(mpl.ticker.NullFormatter())
ax.legend(loc=0, title=', '.join(parameters))
ax.set_xlabel(x)
ax.set_ylabel(y)
return fig, ax
def plot_channels(chdata, ax=None, alpha=.8, **kwargs):
'''
Plot the channel data of picoscope
Includes an indication of the measurement range used
TODO: don't convert data to V (float), just change the axis?
'''
if ax is None:
fig, ax = plt.subplots()
# Colors match the code on the picoscope
# Yellow is too hard to see
colors = dict(A='Blue', B='Red', C='Green', D='Gold')
channels = ['A', 'B', 'C', 'D']
# Remove the previous range indicators
for c in ax.collections: c.remove()
def iterdata():
if type(chdata) in (dict, pd.Series):
return [(0,chdata),]
elif type(chdata) == pd.DataFrame:
return chdata.reset_index(drop=True).iterrows()
else:
# should be list
return enumerate(chdata)
maxval = {8:32512, 10:32704, 12:32736}
for i,data in iterdata():
res = data.get('resolution')
if res is None: res = 8
for c in channels:
if c in data.keys():
# Do we need to convert to voltage? There's not a good way to tell for sure!
# 8-bit channel data may have been smoothed, in which case it may have been converted to float
if data[c].dtype == np.int8:
# Should definitely be 8-bit values, not yet converted to V
# Convert to voltage for plot
chplotdata = data[c] / 127 * data['RANGE'][c] - data['OFFSET'][c]
elif data[c].dtype == np.int16:
# What to do here (slightly) depends on the true resolution of the data
chplotdata = data[c] / maxval[res] * data['RANGE'][c] - data['OFFSET'][c]
elif 'smoothing' in data:
# I am going to assume these are smoothed values, not converted to V yet
# Sorry future self for the inevitable case when this not true..
# e.g. if you get data with ps.get_data(..., raw=False), then smooth it, then send it here
if res == 8:
# wrong if for some reason we still have the 16-bit representation
chplotdata = data[c] / 127 * data['RANGE'][c] - data['OFFSET'][c]
else:
chplotdata = data[c] / maxval[res] * data['RANGE'][c] - data['OFFSET'][c]
else:
chplotdata = data[c]
if 'sample_rate' in data:
# If sample rate is available, plot vs time
x = ivtools.analyze.maketimearray(data, c)
ax.set_xlabel('Time [s]')
ax.xaxis.set_major_formatter(mpl.ticker.EngFormatter())
else:
x = range(len(data[c]))
ax.set_xlabel('Data Point')
chcoupling = data['COUPLINGS'][c]
choffset = data['OFFSET'][c]
chrange = data['RANGE'][c]
if i == 0:
ax.plot(x, chplotdata, color=colors[c], label=f'{c} ({chcoupling})', alpha=alpha, **kwargs)
# lightly indicate the channel range
ax.fill_between((0, np.max(x)), -choffset - chrange, -choffset + chrange, alpha=0.1, color=colors[c])
else:
ax.plot(x, chplotdata, color=colors[c], label=None, alpha=alpha, **kwargs)
ax.legend(title='Channel')
ax.set_ylabel('Voltage [V]')
def colorbar_manual(vmin=0, vmax=1, cmap='jet', ax=None, cax=None, **kwargs):
''' Normally you need a "mappable" to create a colorbar on a plot. This function lets you create one manually. '''
if ax is None:
ax = plt.gca()
if hasattr(vmin, '__iter__'):
# I think you meant to send in the values directly instead of min and max
vmax = np.max(vmin)
vmin = np.min(vmin)
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
sm = mpl.cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
# Sometimes you want to specify the axis for the colorbar itelf.
cb = plt.colorbar(sm, ax=ax, cax=cax, **kwargs)
return cb
def mypause(interval):
''' plt.pause calls plt.show, which steals focus on some systems. Use this instead '''
if interval > 0:
backend = plt.rcParams['backend']
if backend in mpl.rcsetup.interactive_bk:
figManager = mpl._pylab_helpers.Gcf.get_active()
if figManager is not None:
canvas = figManager.canvas
if canvas.figure.stale:
canvas.draw()
canvas.start_event_loop(interval)
return
def mybreakablepause(interval):
''' pauses but allows you to press ctrl-c if the pause is long '''
subinterval = .5
npause, remainder = divmod(interval, subinterval)
for _ in range(int(npause)):
mypause(subinterval)
mypause(remainder)
def plot_cumulative_dist(data, ax=None, **kwargs):
''' Because I always forget how to do it'''
if ax is None:
fig, ax = plt.subplots()
ax.plot(np.sort(data), np.arange(len(data))/len(data), **kwargs)
def plot_ivt(d, phaseshift=14, fig=None, V='V', I='I', **kwargs):
''' A not-so-refined subplot of current and voltage vs time'''
if fig is None:
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
else:
axs = fig.get_axes()
if len(axs) == 2:
ax1, ax2 = axs
elif len(axs) == 0:
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
if 't' not in d:
d['t'] = ivtools.analyze.maketimearray(d)
ax1.plot(d['t'], d[V], c='blue', label='V')
ax2.plot(d['t'] - phaseshift* 1e-9, d[I], c='green', label='I')
ax2.set_ylabel('Current [A]', color='green')
ax1.set_ylabel('Applied Voltage [V]', color='blue')
ax1.set_xlabel('Time [s]')
ax1.xaxis.set_major_formatter(mpl.ticker.EngFormatter())
ax2.yaxis.set_major_formatter(mpl.ticker.EngFormatter())
# Could be a module, but I want it to keep its state when the code is reloaded
class InteractiveFigs(object):
'''
A class to manage the figures used for automatic plotting of IV data while it is measured.
it contains a list of plotting functions that each get called when new data arrives
via self.newline(data) or self.updateline(data)
Data also goes through a list of preprocessing functions (self.preprocessing) before being passed
to the plotting functions. This is used e.g. for smoothing/downsampling input data
Right now we are limited to one axis per figure ... could be extended.
can have several plotting functions per axis though, I think?
'''
# TODO: save/load configurations to disk?
def __init__(self, n:'rows'=2, m:'cols'=None, clear_state=False):
backend = mpl.get_backend()
if not backend.lower().startswith('qt'):
raise Exception('You need to use qt backend to use interactive figs!')
# Formerly had just one argument "n" which meant total number of plots.
# This is so any old code won't break:
if m is None:
rows = 2
cols = n // 2
else:
rows = n
cols = m
n = rows * cols
statename = self.__class__.__name__
if statename not in ivtools.class_states:
ivtools.class_states[statename] = {}
self.__dict__ = ivtools.class_states[statename]
if not self.__dict__ or clear_state:
# Find nice sizes and locations for the figures
# Need to get monitor information. Only works in windows ...
# Borders of the figure window depend on the system, matplotlib can't access it
# make a figure and ask windows what the size is before closing it
import win32gui
fig = plt.figure('what')