In [1]:
%run basics
%matplotlib
In [2]:
def gfalternate_main(ds_tower,ds_alt,alternate_info,label_tower_list=[]):
'''
This is the main routine for using alternate data to gap fill drivers.
'''
mode = "quiet" #"verbose"
ts = alternate_info["time_step"]
startdate = alternate_info["startdate"]
enddate = alternate_info["enddate"]
print "Gap filling using alternate data: "+startdate+" to "+enddate
# close any open plot windows
if len(plt.get_fignums())!=0:
for i in plt.get_fignums():
if i!=0: plt.close(i)
# read the control file again
cfname = ds_tower.globalattributes["controlfile_name"]
cf = qcio.get_controlfilecontents(cfname,mode="quiet")
# do any QC checks
qcck.do_qcchecks(cf,ds_tower,mode="quiet")
# update the ds.alternate dictionary
qcgf.gfalternate_updatedict(cf,ds_tower,ds_alt)
# get local pointer to the datetime series
ldt_tower = ds_tower.series["DateTime"]["Data"]
attr_ldt_tower = ds_tower.series["DateTime"]["Attr"]
si_tower = qcutils.GetDateIndex(ldt_tower,str(alternate_info["startdate"]),ts=ts,default=0)
ei_tower = qcutils.GetDateIndex(ldt_tower,str(alternate_info["enddate"]),ts=ts,default=len(ldt_tower)-1)
# now loop over the variables to be gap filled using the alternate data
if len(label_tower_list)==0:
label_tower_list = list(set(alternate_info["series_list"]))
for fig_num,label_tower in enumerate(label_tower_list):
alternate_info["label_tower"] = label_tower
label_composite = label_tower+"_composite"
alternate_info["label_composite"] = label_composite
# read the tower data and check for gaps
data_tower,flag_tower,attr_tower = qcutils.GetSeriesasMA(ds_tower,label_tower,si=si_tower,ei=ei_tower)
alternate_info["min_points"] = int(len(data_tower)*alternate_info["min_percent"]/100)
# check to see if we have any gaps to fill
if not qcgf.gfalternate_gotgaps(data_tower,label_tower,mode=mode):
# if there are no gaps, fill the composite series with tower data so coverage lines will plot
ds_tower.series[label_composite]["Data"][si_tower:ei_tower+1] = numpy.ma.filled(data_tower,c.missing_value)
ds_tower.series[label_composite]["Flag"][si_tower:ei_tower+1] = flag_tower
continue
# initialise a dictionary to hold the data
data_dict = {}
stats_dict = {}
data_dict[label_tower] = {"attr":attr_tower,"output_list":[label_tower]}
stats_dict[label_tower] = {"startdate":alternate_info["startdate"],"enddate":alternate_info["enddate"]}
# get a list of the output names for this tower series
label_output_list = qcgf.gfalternate_getlabeloutputlist(ds_tower,label_tower)
# loop over the outputs for this tower series
for label_output in label_output_list:
alternate_info["label_output"] = label_output
# update the alternate_info dictionary
qcgf.gfalternate_update_alternate_info(ds_tower,alternate_info)
# get a local pointer to the alternate data structure
ds_alternate = ds_alt[ds_tower.alternate[label_output]["file_name"]]
ldt_alternate = ds_alternate.series["DateTime"]["Data"]
attr_ldt_alternate = ds_alternate.series["DateTime"]["Attr"]
# get the indices of the start and end datetimes
qcgf.gfalternate_getdateindices(ldt_tower,ldt_alternate,alternate_info,"exact")
# check that the start and end times match
if not qcgf.gfalternate_startendtimesmatch(ldt_tower,ldt_alternate,alternate_info,mode=mode): continue
# get the tower data, check we have enough points and if we have, update the data_dict
si = alternate_info["tower"]["si"]; ei = alternate_info["tower"]["ei"]
data_dict["DateTime"] = {"data":ldt_tower[si:ei+1]}
data_tower,flag_tower,attr_tower = qcutils.GetSeriesasMA(ds_tower,label_tower,si=si,ei=ei)
if not qcgf.gfalternate_gotminpoints(data_tower,alternate_info,label_tower,mode=mode): pass
# looks like we have enough to continue so load the dictionaries
stats_dict[label_output] = {"startdate":alternate_info["startdate"],"enddate":alternate_info["enddate"]}
stats_dict[label_composite] = {"startdate":alternate_info["startdate"],"enddate":alternate_info["enddate"]}
data_dict[label_tower]["data"] = data_tower
data_dict[label_output] = {"data":numpy.ma.masked_all_like(data_tower),
"fitcorr":numpy.ma.masked_all_like(data_tower),
"attr":attr_tower,"source":alternate_info["source"]}
if label_composite not in data_dict.keys():
data_dict[label_composite] = {"data":numpy.ma.masked_all_like(data_tower),
"fitcorr":numpy.ma.masked_all_like(data_tower),
"attr":attr_tower}
data_dict[label_tower]["output_list"].append(label_composite)
# get the alternate series that has the highest correlation with the tower data
alternate_var_list = qcgf.gfalternate_getalternatevaratmaxr(data_dict,ds_alternate,alternate_info)
# loop over alternate variables
for label_alternate in alternate_var_list:
alternate_info["label_alternate"] = label_alternate
# get the raw alternate data
si = alternate_info["alternate"]["si"]; ei = alternate_info["alternate"]["ei"]; mode = alternate_info["getseries_mode"]
data_alternate,flag_alternate,attr_alternate = qcutils.GetSeriesasMA(ds_alternate,label_alternate,si=si,ei=ei,mode=mode)
# skip this alternate variable if there are not enough points
if not qcgf.gfalternate_gotminpoints(data_alternate,alternate_info,label_alternate,mode=mode): continue
if not qcgf.gfalternate_gotdataforgaps(data_dict[alternate_info["label_output"]]["data"],
data_alternate,label_alternate,mode=mode): continue
# this alternate series can fill gaps, let's continue
stats_dict[label_output][label_alternate] = {"startdate":alternate_info["startdate"],"enddate":alternate_info["enddate"]}
if label_output not in data_dict[label_tower]["output_list"]:
data_dict[label_tower]["output_list"].append(label_output)
data_dict[label_output][label_alternate] = {"data":data_alternate,"attr":attr_alternate}
gfalternate_getlagcorrecteddata(ds_alternate,data_dict,stats_dict,alternate_info)
qcgf.gfalternate_getfitcorrecteddata(data_dict,stats_dict,alternate_info)
qcgf.gfalternate_loadoutputdata(ds_tower,data_dict,alternate_info)
# check to see if we have alternate data for this whole period, if so there is no reason to continue
si = alternate_info["tower"]["si"]; ei = alternate_info["tower"]["ei"]
ind_tower = numpy.where(abs(ds_tower.series[label_output]["Data"][si:ei+1]-float(c.missing_value))<c.eps)[0]
if len(ind_tower)==0: break
# we have completed the loop over the alternate data for this output
# now do the statistics, diurnal average and daily averages for this output
qcgf.gfalternate_getoutputstatistics(data_dict,stats_dict,alternate_info)
diel_avg = qcgf.gfalternate_getdielaverage(data_dict,alternate_info)
# plot the gap filled data
pd = qcgf.gfalternate_initplot(data_dict,alternate_info)
# reserve figure number 0 for the coverage lines/progress plot
fig_num = fig_num+1
qcgf.gfalternate_plotcomposite(fig_num,data_dict,stats_dict,diel_avg,alternate_info,pd)
# make sure this processing step gets written to the global attribute "Functions"
if "GapFillFromalternate" not in ds_tower.globalattributes["Functions"]:
ds_tower.globalattributes["Functions"] = ds_tower.globalattributes["Functions"]+", GapFillFromalternate"
In [3]:
def gfalternate_getlagcorrecteddata(ds_alternate,data_dict,stats_dict,alternate_info):
label_tower = alternate_info["label_tower"]
label_output = alternate_info["label_output"]
label_alternate = alternate_info["label_alternate"]
if alternate_info["lag"].lower()=="yes":
data_tower = data_dict[label_tower]["data"]
data_alternate = data_dict[label_output][label_alternate]["data"]
maxlags = alternate_info["max_lags"]
minpoints = alternate_info["min_points"]
lags,corr = get_laggedcorrelation(data_tower,data_alternate,maxlags,minpoints)
nLags = numpy.argmax(corr) - alternate_info["max_lags"]
si = alternate_info["alternate"]["si"] - nLags
ei = alternate_info["alternate"]["ei"] - nLags
data,flag,attr = qcutils.GetSeriesasMA(ds_alternate,label_alternate,si=si,ei=ei,mode="pad")
data_dict[label_output][label_alternate]["lagcorr"] = data
stats_dict[label_output][label_alternate]["nLags"] = nLags
else:
data_dict[label_output][label_alternate]["lagcorr"] = numpy.ma.copy(data_dict[label_output][label_alternate]["data"])
stats_dict[label_output][label_alternate]["nLags"] = int(0)
In [4]:
def get_laggedcorrelation(x_in,y_in,maxlags,minpoints):
"""
Calculate the lagged cross-correlation between 2 1D arrays.
Taken from the matplotlib.pyplot.xcorr source code.
PRI added handling of masked arrays.
"""
if numpy.ma.isMA(x_in)!=numpy.ma.isMA(y_in):
raise ValueError('qcts.get_laggedcorrelation: one of x or y is a masked array, the other is not')
lags = numpy.arange(-maxlags,maxlags+1)
if numpy.ma.isMA(x_in) and numpy.ma.isMA(y_in):
mask = numpy.ma.mask_or(x_in.mask,y_in.mask)
x = numpy.ma.array(x_in,mask=mask)
y = numpy.ma.array(y_in,mask=mask)
if numpy.ma.count(x)<minpoints:
#log.error('qcts.get_laggedcorrelation: x or y all masked')
corr = numpy.zeros(len(lags))
return lags,corr
x = numpy.ma.compressed(x)
y = numpy.ma.compressed(y)
nx = len(x)
if nx!=len(y):
raise ValueError('qcts.get_laggedcorrelation: x and y must be equal length')
corr = numpy.correlate(x, y, mode=2)
corr/= numpy.sqrt(numpy.dot(x,x) * numpy.dot(y,y))
if maxlags is None: maxlags = nx - 1
if maxlags >= nx or maxlags < 1:
raise ValueError('qcts.get_laggedcorrelation: maglags must be None or strictly positive < %d'%nx)
corr = corr[nx-1-maxlags:nx+maxlags]
return lags,corr
In [5]:
def gfalternate_loadoutputdata(ds_tower,data_dict,alternate_info):
label_tower = alternate_info["label_tower"]
label_output = alternate_info["label_output"]
label_composite = alternate_info["label_composite"]
label_alternate = alternate_info["label_alternate"]
data_tower = data_dict[label_tower]["data"]
if numpy.ma.count(data_tower)<alternate_info["min_points"] and alternate_info["fit_type"]!="replace": return
si = alternate_info["tower"]["si"]
ei = alternate_info["tower"]["ei"]
ind = numpy.where((numpy.ma.getmaskarray(data_dict[label_output]["data"])==True)&
(numpy.ma.getmaskarray(data_dict[label_output][label_alternate]["data"])==False))[0]
#ind = numpy.ma.where((numpy.ma.getmaskarray(data_dict[label_output]["data"])==True)&
#(numpy.ma.getmaskarray(data_dict[label_output][label_alternate]["data"])==False))[0]
data_dict[label_output]["data"][ind] = data_dict[label_output][label_alternate]["data"][ind]
data_dict[label_output]["fitcorr"][ind] = data_dict[label_output][label_alternate]["fitcorr"][ind]
ind = numpy.where((numpy.ma.getmaskarray(data_dict[label_composite]["data"])==True)&
(numpy.ma.getmaskarray(data_dict[label_output][label_alternate]["data"])==False))[0]
#ind = numpy.ma.where((numpy.ma.getmaskarray(data_dict[label_composite]["data"])==True)&
#(numpy.ma.getmaskarray(data_dict[label_output][label_alternate]["data"])==False))[0]
data_dict[label_composite]["data"][ind] = data_dict[label_output][label_alternate]["data"][ind]
data_dict[label_composite]["fitcorr"][ind] = data_dict[label_output][label_alternate]["fitcorr"][ind]
ds_tower.series[label_composite]["Data"][si:ei+1][ind] = numpy.ma.filled(data_dict[label_output][label_alternate]["fitcorr"][ind],c.missing_value)
ds_tower.series[label_composite]["Flag"][si:ei+1][ind] = numpy.int32(20)
ind = numpy.where((abs(ds_tower.series[label_output]["Data"][si:ei+1]-float(c.missing_value))<c.eps)&
(numpy.ma.getmaskarray(data_dict[label_output][label_alternate]["fitcorr"])==False))[0]
ds_tower.series[label_output]["Data"][si:ei+1][ind] = numpy.ma.filled(data_dict[label_output][label_alternate]["fitcorr"][ind],c.missing_value)
ds_tower.series[label_output]["Flag"][si:ei+1][ind] = numpy.int32(20)
In [6]:
# preliminary set up from OzFluxQC.do_l4qc
cfname = "../controlfiles/SturtPlains/all/L4_ipynb.txt"
cf = qcio.get_controlfilecontents(cfname)
infilename = qcio.get_infilenamefromcf(cf)
ds3 = qcio.nc_read_series(infilename)
ds3.globalattributes['controlfile_name'] = cf['controlfile_name']
In [7]:
# set up from qcls.do_l4qc
ds4 = qcio.copy_datastructure(cf,ds3)
ds4.cf = cf
ds_alt = {}
for ThisOne in cf["Drivers"].keys():
if ThisOne not in ds4.series.keys(): log.error("Series "+ThisOne+" not in data structure"); continue
# interpolate over short gaps
qcts.InterpolateOverMissing(ds4,series=ThisOne,maxlen=3)
# parse the control file for information on how the user wants to do the gap filling
qcgf.GapFillParseControlFile(cf,ds4,ThisOne,ds_alt)
In [8]:
# now the work starts, code from qcgf.GapFillFromAlternate (but without the GUI) and qcgf.gfalternate_run
ds4.returncodes["alternate"] = "normal"
if "alternate" not in dir(ds4): print "alternate not in ds"
ldt_tower = ds4.series["DateTime"]["Data"]
startdate = ldt_tower[0]
enddate = ldt_tower[-1]
alternate_info = {"overlap_startdate":startdate.strftime("%Y-%m-%d %H:%M"),
"overlap_enddate":enddate.strftime("%Y-%m-%d %H:%M"),
"startdate":startdate.strftime("%Y-%m-%d"),
"enddate":enddate.strftime("%Y-%m-%d")}
# following code from qcgf.gfalternate_run
# load GUI options into alternate_info
alternate_info["peropt"] = 3
alternate_info["show_plots"] = True
alternate_info["auto_complete"] = True
alternate_info["min_percent"] = int(50)
alternate_info["site_name"] = ds4.globalattributes["site_name"]
alternate_info["time_step"] = int(ds4.globalattributes["time_step"])
alternate_info["nperhr"] = int(float(60)/alternate_info["time_step"]+0.5)
alternate_info["nperday"] = int(float(24)*alternate_info["nperhr"]+0.5)
alternate_info["max_lags"] = int(float(12)*alternate_info["nperhr"]+0.5)
alternate_info["tower"] = {}
alternate_info["alternate"] = {}
series_list = [ds4.alternate[item]["label_tower"] for item in ds4.alternate.keys()]
alternate_info["series_list"] = series_list
print "Gap filling "+str(list(set(series_list)))+" using alternate data"
nDays = int(90)
#if len(alt_gui.startEntry.get())!=0: alternate_info["startdate"] = alt_gui.startEntry.get()
#if len(alt_gui.endEntry.get())!=0: alternate_info["enddate"] = alt_gui.endEntry.get()
#alternate_info["startdate"]="2012-01-01"
#alternate_info["enddate"]="2012-03-01"
alternate_info["gui_startdate"] = alternate_info["startdate"]
alternate_info["gui_enddate"] = alternate_info["enddate"]
startdate = dateutil.parser.parse(alternate_info["startdate"])
gui_enddate = dateutil.parser.parse(alternate_info["gui_enddate"])
overlap_enddate = dateutil.parser.parse(alternate_info["overlap_enddate"])
enddate = startdate+dateutil.relativedelta.relativedelta(days=nDays)
enddate = min([overlap_enddate,enddate,gui_enddate])
alternate_info["enddate"] = datetime.datetime.strftime(enddate,"%Y-%m-%d")
stopdate = min([overlap_enddate,gui_enddate])
if "plot_path" in ds4.cf["Files"]: alternate_info["plot_path"] = ds4.cf["Files"]["plot_path"]
In [9]:
# loop over start and end dates
#label_tower_list=["Ah","Ta"]
while startdate<stopdate:
# following code from gfalternate_main
gfalternate_main(ds4,ds_alt,alternate_info)
#gfalternate_plotcoveragelines(ds_tower)
startdate = enddate
enddate = startdate+dateutil.relativedelta.relativedelta(days=nDays)
run_enddate = min([stopdate,enddate])
alternate_info["startdate"] = startdate.strftime("%Y-%m-%d")
alternate_info["enddate"] = run_enddate.strftime("%Y-%m-%d")
In [11]:
# following code from gfalternate_autocomplete
mode="verbose"
ldt_tower = ds4.series["DateTime"]["Data"]
ts = alternate_info["time_step"]
si = qcutils.GetDateIndex(ldt_tower,alternate_info["gui_startdate"],ts=ts,default=0)
ei = qcutils.GetDateIndex(ldt_tower,alternate_info["gui_enddate"],ts=ts,default=len(ldt_tower))
ldt_tower = ldt_tower[si:ei+1]
nRecs = len(ldt_tower)-1
label_tower_list = list(set(alternate_info["series_list"]))
data_all = {}
for label_tower in label_tower_list:
label_composite = label_tower+"_composite"
not_enough_points = False
data_composite,flag_composite,attr_composite = qcutils.GetSeriesasMA(ds4,label_composite,si=si,ei=ei)
data_tower,flag_tower,attr_tower = qcutils.GetSeriesasMA(ds4,label_tower,si=si,ei=ei)
data_all[label_tower] = data_tower
data_merged = numpy.ma.copy(data_tower)
idx = numpy.where((numpy.ma.getmaskarray(data_tower)==True)&(numpy.ma.getmaskarray(data_composite)==False))[0]
data_merged[idx] = data_composite[idx]
mask_merged = numpy.ma.getmaskarray(data_merged)
gapstartend = qcutils.contiguous_regions(mask_merged)
if len(gapstartend)==0:
if mode.lower()!="quiet":
msg = " autocomplete: no gaps in "+label_tower+", skipping ..."
print msg
# code from here is new addition
gotdataforgap = [False]*len(gapstartend)
output_list = [item for item in ds4.alternate.keys() if label_tower in item]
for label_output in output_list:
alt_filename = ds4.alternate[label_output]["file_name"]
ds_alternate = ds_alt[alt_filename]
alt_series_list = [item for item in ds_alternate.series.keys() if "_QCFlag" not in item]
alt_series_list = [item for item in alt_series_list if label_tower in item]
ldt_alt = ds_alternate.series["DateTime"]["Data"][si:ei+1]
mask_alt = numpy.ma.masked_all(len(ldt_alt))
for label_alternate in alt_series_list:
data_alt,flag_alt,attr_alt = qcutils.GetSeriesasMA(ds_alternate,label_alternate,si=si,ei=ei)
data_all[label_alternate] = data_alt
for n,gap in enumerate(gapstartend):
result = qcgf.gfalternate_gotdataforgaps(data_tower[gap[0]:gap[1]],data_alt[gap[0]:gap[1]],
label_alternate,mode="quiet")
gotdataforgap[n] = gotdataforgap[n] or result
#print label_tower,gotdataforgap
# code from here comes from gfalternate_autocomplete
for n,gap in enumerate(gapstartend):
# except for this line which is new
if not gotdataforgap[n]:
if mode.lower()!="quiet":
gap_startdate = ldt_tower[gap[0]].strftime("%Y-%m-%d %H:%M")
gap_enddate = ldt_tower[gap[0]].strftime("%Y-%m-%d %H:%M")
msg = " autocomplete: no alternate data for "+gap_startdate+" to "+gap_enddate
print msg
continue
if mode.lower()!="quiet":
gap_startdate = ldt_tower[gap[0]].strftime("%Y-%m-%d %H:%M")
gap_enddate = ldt_tower[gap[1]].strftime("%Y-%m-%d %H:%M")
msg = " autocomplete: "+label_tower+" gap is "+gap_startdate+" to "+gap_enddate
print msg
min_points = int((gap[1]-gap[0])*alternate_info["min_percent"]/100)
num_good_points = (gap[1] - gap[0])
num_points_list = data_all.keys()
for label in data_all.keys():
if numpy.ma.count(data_all[label][gap[0]:gap[1]])<min_points:
num_points_list.remove(label)
continue
num_good_points = min([num_good_points,numpy.ma.count(data_all[label][gap[0]:gap[1]])])
#num_good_points = numpy.ma.count(data_tower[gap[0]:gap[1]])
while num_good_points<min_points:
gap[0] = max(0,gap[0] - alternate_info["nperday"])
gap[1] = min(nRecs,gap[1] + alternate_info["nperday"])
if gap[0]==0 and gap[1]==nRecs:
print " autocomplete: Unable to find enough good points in "+label_tower
not_enough_points = True
if not_enough_points: break
min_points = int((gap[1]-gap[0])*alternate_info["min_percent"]/100)
num_good_points = (gap[1] - gap[0])
for label in num_points_list:
num_good_points = min([num_good_points,numpy.ma.count(data_all[label][gap[0]:gap[1]])])
#num_good_points = numpy.ma.count(data_tower[gap[0]:gap[1]])
if not_enough_points: break
if mode.lower()!="quiet":
gap_startdate = ldt_tower[gap[0]].strftime("%Y-%m-%d %H:%M")
gap_enddate = ldt_tower[gap[1]].strftime("%Y-%m-%d %H:%M")
print " autocomplete: "+label_tower+" gap fill period is "+gap_startdate+" to "+gap_enddate
alternate_info["startdate"] = ldt_tower[gap[0]].strftime("%Y-%m-%d")
alternate_info["enddate"] = ldt_tower[gap[1]].strftime("%Y-%m-%d")
print "I will do a plot here",label_tower,alternate_info["startdate"],alternate_info["enddate"]
#gfalternate_main(ds4,ds_alt,alternate_info,label_tower_list=[label_tower])
# following code to be added to gfalternate_autocomplete
# this code checks to see if there is data available to fill the remaining gaps identified
In [12]:
ldt_tower=ds4.series["DateTime"]["Data"]
ldt_aws=ds_alt['../../Sites/SturtPlains/Data/AWS/SturtPlains_AWS.nc'].series["DateTime"]["Data"]
ldt_bios=ds_alt['../../Sites/SturtPlains/Data/BIOS2/SturtPlains_BIOS2.nc'].series["DateTime"]["Data"]
sdate="2012-02-10 03:00"
edate="2012-02-10 06:00"
si=qcutils.GetDateIndex(ldt_tower,sdate)
ei=qcutils.GetDateIndex(ldt_tower,edate)
print ldt_tower[si],ldt_aws[si],ldt_bios[si]
print ldt_tower[ei],ldt_aws[ei],ldt_bios[ei]
Ah,f,a=qcutils.GetSeriesasMA(ds4,"Ah")
Ah_composite,f,a=qcutils.GetSeriesasMA(ds4,"Ah_composite")
Ah_aws,f,a=qcutils.GetSeriesasMA(ds4,"Ah_aws")
Ah_bios2,f,a=qcutils.GetSeriesasMA(ds4,"Ah_bios2")
print "Ah",Ah[si:ei],Ah.mask[si:ei]
print "Ah_composite",Ah_composite[si:ei],Ah_composite.mask[si:ei]
print "Ah_aws",Ah_aws[si:ei],Ah_aws.mask[si:ei]
print "Ah_bios2",Ah_bios2[si:ei],Ah_bios2.mask[si:ei]
Ah_aws_0,f,a=qcutils.GetSeriesasMA(ds_alt['../../Sites/SturtPlains/Data/AWS/SturtPlains_AWS.nc'],"Ah_0")
Ah_aws_1,f,a=qcutils.GetSeriesasMA(ds_alt['../../Sites/SturtPlains/Data/AWS/SturtPlains_AWS.nc'],"Ah_1")
print "Ah_aws_0",Ah_aws_0[si:ei],Ah_aws_0.mask[si:ei]
print "Ah_aws_1",Ah_aws_1[si:ei],Ah_aws_1.mask[si:ei]
Ah_bios,f,a=qcutils.GetSeriesasMA(ds_alt['../../Sites/SturtPlains/Data/BIOS2/SturtPlains_BIOS2.nc'],"Ah")
print "Ah_bios",Ah_bios[si:ei],Ah_bios.mask[si:ei]
In [ ]: