From bff9c825090c8c2507cc734444ab8f02003a1cb6 Mon Sep 17 00:00:00 2001 From: unknown <asoalexandros@gmail.com> Date: Wed, 11 Dec 2024 12:19:09 +0100 Subject: [PATCH] Memristor ini and Automatic stop of endurance! --- hp4155/memristor (Version 4.1)/help.py | 559 ++++++++++++++ hp4155/memristor (Version 4.1)/help_pulse.py | 463 ++++++++++++ hp4155/memristor (Version 4.1)/memristor.py | 692 ++++++++++++++++++ .../memristor_buttons.ipynb | 86 +++ hp4155/memristor (Version 4.1)/schematic.png | Bin 0 -> 17440 bytes 5 files changed, 1800 insertions(+) create mode 100644 hp4155/memristor (Version 4.1)/help.py create mode 100644 hp4155/memristor (Version 4.1)/help_pulse.py create mode 100644 hp4155/memristor (Version 4.1)/memristor.py create mode 100644 hp4155/memristor (Version 4.1)/memristor_buttons.ipynb create mode 100644 hp4155/memristor (Version 4.1)/schematic.png diff --git a/hp4155/memristor (Version 4.1)/help.py b/hp4155/memristor (Version 4.1)/help.py new file mode 100644 index 0000000..18e87e7 --- /dev/null +++ b/hp4155/memristor (Version 4.1)/help.py @@ -0,0 +1,559 @@ +""" +This is a python file containing all the important functions for memristor measurement + +Available Functions + +measurements in the HP4155a +plot results +create data frame +ini file decoder +enabing and disabling widgets for jupyter(lists) +""" + +import sys +sys.path.insert(0, '..') #append parent directory + +import hp4155a +import matplotlib.pyplot as plt + +import tkinter as tk +from tkinter import filedialog +import tkinter.messagebox + +import numpy as np +from IPython.display import display, clear_output +import pandas as pd +from datetime import datetime +import ipywidgets as widgets +import time +import os + + +#contact check between two SMUs (i,j) +def contact_check(i,j,device): + smu = [1,2,3,4] + device.measurement_mode('SAMP') + parameters ={ + 'mode' : 'LIN', + 'hold': 0, + 'interval':2e-3, + 'points': 1, + 'filter': 'OFF', + 'value':0.01, #voltage value + 'comp':0.1 #compliance value + } + + device.setup_sampling(parameters) + + device.auto_sampling_time('ON') + device.integration_time('MED') + + smu_v = device.smu_dict() + smu_v.update( + vname = f'V{i}', + iname = f'I{i}', + mode = 'V', + func = 'CONS' + ) + device.setup_smu(i,smu_v) + + smu_ground = device.smu_dict() + smu_ground.update( + vname =f'V{j}', + iname = f'I{j}', + mode = 'COMM', + func='CONS' + ) + device.setup_smu(j,smu_ground) + + #one smu is measuring + #one smu is ground + + #set voltage and compliance + device.setup_smu_sampling(i,parameters) + + #smus to remove + smu_disable = smu.copy() + smu_disable.remove(i) + smu_disable.remove(j) + + for number in smu_disable: + device.smu_disable(number) + + device.user_function(f'R{i}{j}','OHM',f'V{i}/I{i}') + device.display_variable('X','@TIME') + device.display_variable('Y1',f'R{i}{j}') + device.single_measurement() + while device.operation_completed() == False: + time.sleep(2) + + R = device.return_values(f'R{i}{j}')[0] #only the first value + print(f"R{i}{j}:{'{:.2e}'.format(R)} Ohm") + device.del_user_functions() + device.autoscaling() + return R + + +#these are all the sampling checks +def regular_contact_check(device): + resistances = {} + for i in range(1,4): # iterate through smus 1-4 + for j in range(4,i,-1): + """ + We have the following pairs in order + 1-4,1-3,1-2,2-4,2-3,3-4 + """ + R=contact_check(i,j,device) + resistances[f"{i}-{j}"] = R + + #convert dictionary to df + df = pd.DataFrame(resistances.items(), columns=['SMU pair', 'Resistance (Ohm)']) + return df + +def EBL(device): + # EBL are SMUs 1-4 and 2-3 + resistances = {} + for i,j in zip(range(1,3),range(4,2,-1)): #loop simultaneously 1-4,2-3 pairs + R = contact_check(i,j,device) + resistances[f"{i}-{j}"] = R + + #convert dictionary to df + df = pd.DataFrame(resistances.items(), columns=['SMU pair', 'Resistance (Ohm)']) + return df + +def OL(device): + # OL smu 3-4,1-2 + resistances= {} + for i,j in zip(range(3,0,-2),range(4,1,-2)): #loop simultaneously 3-4 , 1-2 pairs + R = contact_check(i,j,device) + resistances[f"{i}-{j}"] = R + + #convert dictionary to df + df = pd.DataFrame(resistances.items(), columns=['SMU pair', 'Resistance (Ohm)']) + return df + +#double sweep from start to stop and then from start to stop +def sweep(start,stop,step,comp,integration,device): #step cannot be negative + if start < stop and step < 0 : + step = -step + elif start > stop and step > 0 : + step = -step + + smu_v = device.smu_dict() + smu_ground = device.smu_dict() + parameters = device.var1_dict() + + smu_v.update( + iname = 'I2', + vname = 'V2', + mode = 'V', + func = 'VAR1' + ) + smu_ground.update( + iname ='I4', + vname = 'V4', + mode = 'COMM', + func = 'CONS' + ) + parameters.update( + mode ='DOUB', + start = start, + stop = stop, + step = step, + comp = comp, + pcomp = 0 + ) + + #disable smus 1 and 3 + device.measurement_mode('SWE') + device.smu_disable(1) + device.smu_disable(3) + + device.setup_smu(2,smu_v) + device.setup_smu(4,smu_ground) + device.setup_var1(parameters) + device.integration_time(integration) + + #display variables + device.display_variable('X','V2') + device.display_variable('Y1','I2') + + #execute measurement + device.single_measurement() + while device.operation_completed()==False: + time.sleep(2) + + device.autoscaling() + + #return values + V=device.return_values('V2') + I=device.return_values('I2') + + #convert the list to np.array to return the absolute values for the logarithmic scale + V = np.array(V) + I = np.array(I) + + #return all values to the function + return V, I + +#sampling check +def sampling_check(voltage,device): + + parameters ={ + 'mode' : 'LIN', + 'hold': 0, + 'interval':2e-3, + 'points': 5, + 'filter': 'OFF', + 'value':voltage, #voltage value + 'comp':0.1 #compliance value + } + smu_v = device.smu_dict() + smu_ground = device.smu_dict() + + + smu_v.update( + iname = 'I2', + vname = 'V2', + mode = 'V', + func = 'CONS' + ) + smu_ground.update( + iname ='I4', + vname = 'V4', + mode = 'COMM', + func = 'CONS' + ) + + device.measurement_mode('SAMP') + device.smu_disable(1) + device.smu_disable(3) + device.setup_smu(2,smu_v) + device.setup_smu(4,smu_ground) + + device.setup_smu_sampling(2,parameters) + device.setup_sampling(parameters) + + device.integration_time('LONG') + + #remove total sampling time + device.auto_sampling_time('ON') + + device.user_function('R','OHM','V2/I2') + + device.display_variable('X','@INDEX') + device.display_variable('Y1','R') + device.single_measurement() + while device.operation_completed() == False: + time.sleep(2) + + index = np.array(device.return_values('@INDEX')) + R = np.array(device.return_values('R')) + R_mean = np.average(R) + device.del_user_functions() + device.autoscaling() + + # Plot the results + fig,ax = plt.subplots() + + ax.set_title(f"Average Resistance(Sampling Check):{'{:.2e}'.format(R_mean)} Ohm") + ax.set_yscale('log') + ax.set_ylabel('Resistance (Ohm)') + ax.set_xlabel('Sampling Index') + ax.set_xticks(index) + ax.scatter(index,np.absolute(R),label = f"Voltage={voltage}V") + ax.legend() + display(fig) + + return R_mean + +#new (retention) +def retention(voltage,period,duration,device): + parameters ={ + 'mode' : 'LIN', + 'hold': 0, + 'interval':2e-3, + 'points': 0, + 'filter': 'OFF', + 'value':voltage, #voltage value + 'comp':0.1 #compliance value + } + smu_v = device.smu_dict() + smu_ground = device.smu_dict() + + smu_v.update( + iname = 'I2', + vname = 'V2', + mode = 'V', + func = 'CONS' + ) + smu_ground.update( + iname ='I4', + vname = 'V4', + mode = 'COMM', + func = 'CONS' + ) + + device.measurement_mode('SAMP') + device.smu_disable(1) + device.smu_disable(3) + device.setup_smu(2,smu_v) + device.setup_smu(4,smu_ground) + + device.setup_smu_sampling(2,parameters) + + device.integration_time('LONG') + device.total_sampling_time(duration) + + if int(duration/period)+1<=10001: + parameters.update(points=int(duration/period)+1) + else: + parameters.update(points = 'MAX') + device.setup_sampling(parameters) + + device.integration_time('MED') + device.user_function('R','OHM','V2/I2') + device.user_function('ABSR','OHM', 'ABS(R)') + + + device.display_variable('X','@TIME') + device.display_variable('Y1','ABSR') + device.axis_scale('Y1','LOG') + device.display_variable_min_max('Y1','MIN',10) + device.display_variable_min_max('Y1','MAX',10**8) + + device.single_measurement() + while device.operation_completed() == False: + time.sleep(2) + + + TIME = device.return_values('@TIME') + R = device.return_values('R') + TIME = np.array(TIME) + R = np.array(R) + device.del_user_functions() + device.autoscaling() + return TIME,R + + +#plot sweep results +def plot_sweep(x,y,title): + #plot results + fig, (ax1, ax2) = plt.subplots(2,sharex=True,figsize=(8,6)) #the plots share the same x axis + fig.suptitle(title) + ax1.set_title('Linear I') + ax1.set(xlabel='Voltage(V)',ylabel='Current(A)') + ax2.set_title('Logarithmic I') + ax2.set(xlabel='Voltage(V)',ylabel='Current(A)') + ax2.set_yscale('log') + + ax1.plot(x,y) + ax2.plot(x,np.absolute(y)) + fig.tight_layout() + display(fig) + +def plot_retention(x,y): + fig, ax = plt.subplots() + fig.suptitle('Retention') + ax.set(xlabel='time(s)',ylabel='Resistance(Ohm)') + ax.set_yscale('log') + ax.set_xscale('linear') + ax.plot(x,y) + display(fig) + + +def create_data_frame(x,y): + header = ['V(V)','ABSV(V)',"I(A)",'ABSI(A)',"R(Ohm)"] + data = {header[0]:x,header[1]:np.absolute(x),header[2]:y,header[3]:np.absolute(y),header[4]:np.divide(x,y)} + df = pd.DataFrame(data) + return df + +def create_retention_data_frame(x,y): + header = ['Time(s)','R(Ohm)'] + data = {header[0]:x,header[1]:y} + df = pd.DataFrame(data) + return df + + +#write results to file +def write_to_file(file,title:list,df): + #append escape character after each element + index = 1 + while index <= len(title): + title.insert(index,"\n") + index = index+2 + + #write to file + with open(file,'a') as f: + f.writelines(title) + f.write("\n") + f.write(df.to_string()) + f.write("\n\n") + +#### new functions ############## +def disable_widgets(widgets_list): + for widget in widgets_list: + widget.disabled = True + +def change_state(widgets_list): + for widget in widgets_list: + widget.disabled = not widget.disabled + +def enable_widgets(widgets_list): + for widget in widgets_list: + widget.disabled = False + +#a check values function +def check_values(step,set_voltage,reset_voltage): + valid = True + + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + if step > abs(set_voltage) or step > abs(reset_voltage) or step==0:#invalid parameter setting + valid = False + tkinter.messagebox.showerror(message="Invalid parameter setting!") + + #now if the set-reset voltages have the same polarity show a warning + elif set_voltage*reset_voltage>0: + valid = tk.messagebox.askokcancel(message="Set-Reset voltages have the same polarity. Continue?") + + else: + pass + + root.destroy() + return valid + + +def information_box(information): + #open dialog and hide the main window + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + #display meaagebox + tkinter.messagebox.showinfo(message=information) + root.destroy() + +#choose directory to save measurement results +#and check if you have access +def check_writable(folder): + filename = "test.txt" + file = os.path.join(folder,filename) + + #protection against removing existing file in python + i=1 + while os.path.exists(file): + filename=f"test{i}.txt" + file = os.path.join(folder,filename) + try: + with open(file,'a'): + writable = True + os.remove(file) + except: + writable = False + information_box(f"{folder} is not writable!") + + return writable + +def choose_folder(): + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + #choose nonemty folder + folder = tk.filedialog.askdirectory() + + while folder == '': + folder = tk.filedialog.askdirectory() + + #check if writable in a while loop + writable=check_writable(folder) + + while writable == False: + #choose a correct folder + folder = tk.filedialog.askdirectory() + + while folder == '': + folder = tk.filedialog.askdirectory() + + #check writable if not repeat + writable=check_writable(folder) + + root.destroy() + return folder + + +def upload_results(source_file,target_file,target_file_dir): + """ + New function (UPLOAD RESULTS) + IMPORTANT FOR ALL MEASUREMENTS + THE RESULTS ARE MOVED FROM SOURCE FILE TO TARGET FILE EVEN LOCALLY + """ + while True: + try: + with (open(source_file,'r') as source,open(target_file,'a') as target): + target.write(source.read()) + os.remove(source_file) + return source_file,target_file,target_file_dir + except: + information_box(f"{target_file} is no longer accessible. Please change directory") + target_file_dir = choose_folder() + filename = os.path.basename(target_file) + target_file =os.path.join(target_file_dir,filename) + #and then try again + + +#ini file functions +def save_as_ini(default_filename): + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + file = filedialog.asksaveasfilename(defaultextension=".ini", filetypes=[("Ini files","*.ini")],title = "save as ini",initialfile =default_filename) + + #check if the file path is correct(.txt) + while file.endswith(".ini") == False: + #open again filedialog with error message box + answer=tk.messagebox.askyesno(message='Do you want to cancel the ini file Save?') + if answer == True: + raise Exception("Ini File Operation aborted!") + else: + file = filedialog.asksaveasfilename(defaultextension=".ini", filetypes=[("Ini files","*.ini")],title = "save as ini",initialfile =default_filename) + root.destroy() + return file + +def load_ini(): + root = tk.Tk() + root.withdraw() + root.lift() #show window above all other applications + + root.attributes("-topmost", True)#window stays above all other applications + + + file = filedialog.askopenfilename(filetypes=[("Ini files","*.ini")],title ='Select ini file') + while file.endswith(".ini") == False: + #open again filedialog with error message box + answer=tk.messagebox.askyesno(message='Do you want to cancel the ini file load?') + if answer == True: + raise Exception("Ini File Operation aborted!") + else: + file = filedialog.askopenfilename(filetypes=[("Ini files","*.ini")],title = "Select ini file") + root.destroy() + return file + + + + + + + \ No newline at end of file diff --git a/hp4155/memristor (Version 4.1)/help_pulse.py b/hp4155/memristor (Version 4.1)/help_pulse.py new file mode 100644 index 0000000..a519559 --- /dev/null +++ b/hp4155/memristor (Version 4.1)/help_pulse.py @@ -0,0 +1,463 @@ +import sys +sys.path.insert(0, '..') #append parent directory + +import ipywidgets as widgets +import tkinter as tk +from tkinter import filedialog +import tkinter.messagebox +import os +from datetime import datetime +import matplotlib.pyplot as plt +import numpy as np +import module +import time +import pandas as pd +from IPython.display import display, clear_output + +#widgets interactivity + +def add_widgets_to_list(source_dictionary,target_list): + for widget in source_dictionary.values(): + target_list.append(widget) + + +def check_pulse(dictionary): + #check if number of pulses is ok + if dictionary['pulses'].value < 0: + dictionary['pulses'].value = -dictionary['pulses'].value + elif dictionary['pulses'].value==0: + dictionary['pulses'].value = 1 + else: + pass + + # Restriction: pulse period ≥ pulse width + 4 ms + if dictionary['period'].value < dictionary['width'].value+4e-3: + dictionary['period'].value = dictionary['width'].value+4e-3 + + +#sweep pulse measurement +def sweep_meas(dictionary,device): + smu_v = device.smu_dict() + smu_ground = device.smu_dict() + parameters = device.var1_dict() + + smu_v.update( + iname = 'I2', + vname = 'V2', + mode = 'VPULSE', + func = 'VAR1' + ) + smu_ground.update( + iname ='I4', + vname = 'V4', + mode = 'COMM', + func = 'CONS' + ) + parameters.update( + mode ='SING', + start = dictionary['start'].value, + stop = dictionary['stop'].value, + step = (dictionary["stop"].value-dictionary["start"].value)/(dictionary["pulses"].value-1), #define the number of steps given specific pulses + comp = dictionary['comp'].value, + pcomp = 0, + base = dictionary["base"].value, + width = dictionary["width"].value, + period= dictionary["period"].value + ) + device.smu_disable(1) + device.smu_disable(3) + + + device.measurement_mode("SWE") + device.setup_smu(2,smu_v) + device.setup_smu(4,smu_ground) + device.setup_var1(parameters) + + device.display_variable("X","V2") + device.display_variable("Y1",'I2') + + device.range_mode(4,"AUTO") + device.range_mode(2,"AUTO") + + device.setup_pulse(parameters) + + device.integration_time(dictionary["integration"].value) + + t0 = time.time() + device.single_measurement() + while device.operation_completed()== False: + pass + + t1 = time.time() + # get the execution time + elapsed_time = t1 - t0 + device.autoscaling() + + I_i=device.return_values("I2") + V_i=device.return_values("V2") + R_i = np.divide(V_i,I_i) + + + expected_time = dictionary["period"].value*dictionary["pulses"].value + + times = (elapsed_time,expected_time) + values = (V_i,I_i,R_i) + + del device + + return times,values + +def plot_sweep_pulse(values): + fig, ax1 = plt.subplots() + color = 'tab:red' + + ax1.set_xlabel("V(V)") + ax1.set_ylabel("I(A)",color=color) + ax1.set_yscale('log') + + ax1.plot(values[0],np.abs(values[1]),color=color) + ax1.tick_params(axis ='y', labelcolor = color,which = 'both') + + # Adding Twin Axes + ax2 = ax1.twinx() + color = 'tab:green' + + ax2.set_ylabel("R(Ohm)",color = color) + ax2.plot(values[0],np.abs(values[2]),color = color) + + ax2.tick_params(axis ='y', labelcolor = color,which = 'both') + ax2.set_yscale('log') + + fig.suptitle("Sweep Pulse Measurement Results") + + display(fig) + +def save_sweep(folder,sample_dict,values,times,sweep_dict): + filename = f"{sample_dict['series'].value}_{sample_dict['field'].value}_{sample_dict['dut'].value}.txt" + + file = os.path.join(folder,filename) + + with open(file,"a") as f: + date = str(datetime.today().replace(microsecond=0)) + f.write(f"Sweep Pulse Measurement at {date}"+"\n") + f.write(f"period(s):{sweep_dict['period'].value}"+"\n") + f.write(f"width(s):{sweep_dict['width'].value}"+"\n") + f.write(f"base value(V):{sweep_dict['base'].value}"+"\n") + f.write(f"execution time(s):{times[0]}"+"\n") + f.write(f"expected time(s):{times[1]}"+"\n") + f.write(f"number of pulses:{sweep_dict['pulses'].value}"+"\n") + f.write(f"current compliance(A):{sweep_dict['comp'].value}"+"\n") + f.write(f"voltage:{sweep_dict['start'].value}V to {sweep_dict['stop'].value}V"+"\n") + f.write(f"integration time:{sweep_dict['integration'].value}"+"\n\n") + + zipped = list(zip(values[0],values[1], values[2])) + df = pd.DataFrame(zipped, columns=['VPULSE(V)', 'IPULSE(A)', 'RPULSE(Ohm)']) + + f.write("Results Sweep Pulse:\n") + f.write(df.to_string()) + f.write("\n\n\n") + +def constant_meas(dictionary,device): + smu_v = device.smu_dict() + smu_ground = device.smu_dict() + sweep_params = device.var1_dict() + smu_help = device.smu_dict() #this is the uncontacted smu + + smu_v.update( + iname = 'I2', + vname = 'V2', + mode = 'V', + func = 'CONS' + ) + smu_help.update( + iname = 'I3', + vname = 'V3', + mode = 'VPULSE', + func = 'VAR1' + ) + smu_ground.update( + iname ='I4', + vname = 'V4', + mode = 'COMM', + func = 'CONS' + ) + sweep_params.update( + mode ='SING', + start = 0, + stop = 10, + step = 10/(dictionary["pulses"].value-1), #define the number of steps given specific pulses + comp = 0.1, + pcomp = 0, + base = dictionary["base"].value, + width = dictionary["width"].value, + period= dictionary["period"].value + ) + #the constant smu + cons = { + 'value':dictionary["voltage"].value, + 'comp':dictionary["comp"].value + } + + device.measurement_mode("SWE") + device.smu_disable(1) + device.setup_smu(2,smu_v) + device.setup_smu(3,smu_help) + device.setup_smu(4,smu_ground) + device.setup_var1(sweep_params) + device.setup_pulse(sweep_params) + device.setup_cons_smu(2,cons) + + device.user_function('R','OHM','V2/I2') + + device.display_variable("X","@INDEX") + device.display_variable("Y1",'R') + + device.range_mode(4,"AUTO") + device.range_mode(2,"AUTO") + device.range_mode(3,"AUTO") + + + device.integration_time(dictionary["integration"].value) + + device.variables_to_save(['@INDEX','V2','I2','R']) + + t0 = time.time() + device.single_measurement() + while device.operation_completed()== False: + pass + + t1 = time.time() + # get the execution time + elapsed_time = t1 - t0 + + + I_i=device.return_values("I2") + V_i=device.return_values("V2") + R_i = device.return_values('R') + + + expected_time = dictionary["period"].value*dictionary["pulses"].value + + times = (elapsed_time,expected_time) + values = (V_i,I_i,R_i) + + device.del_user_functions() + device.autoscaling() + + return times,values + +def plot_constant_pulse(values): + index =[] + for i in range(len(values[0])): + index.append(i+1) + + fig, ax1 = plt.subplots() + color = 'tab:red' + + ax1.set_xlabel("Index(Pulse number)") + ax1.set_ylabel("I(A)",color=color) + ax1.set_yscale('log') + + ax1.plot(index,np.abs(values[1]),color=color,label = "Voltage(V):"+str(min(values[0]))) + ax1.tick_params(axis ='y', labelcolor = color,which = 'both') + + # Adding Twin Axes + ax2 = ax1.twinx() + color = 'tab:green' + + ax2.set_ylabel("R(Ohm)",color = color) + ax2.plot(index,np.abs(values[2]),color=color) + ax2.set_yscale('log') + + ax2.tick_params(axis ='y', labelcolor = color,which = 'both') + + fig.suptitle("Constant Pulse Measurement Results") + ax1.set_title("Voltage:"+str(min(values[0]))+"V") + + display(fig) + +def save_constant(folder,sample_dict,values,times,cons_dict): + filename = f"{sample_dict['series'].value}_{sample_dict['field'].value}_{sample_dict['dut'].value}.txt" + + file = os.path.join(folder,filename) + + with open(file,"a") as f: + date = str(datetime.today().replace(microsecond=0)) + f.write(f"Constant Pulse Measurement at {date}"+"\n") + f.write(f"period(s):{cons_dict['period'].value}"+"\n") + f.write(f"width(s):{cons_dict['width'].value}"+"\n") + f.write(f"base value(V):{cons_dict['base'].value}"+"\n") + f.write(f"execution time(s):{times[0]}"+"\n") + f.write(f"expected time(s):{times[1]}"+"\n") + f.write(f"number of pulses:{cons_dict['pulses'].value}"+"\n") + f.write(f"current compliance(A):{cons_dict['comp'].value}"+"\n") + f.write(f"constant voltage:{cons_dict['voltage'].value}V"+"\n") + f.write(f"integration time:{cons_dict['integration'].value}"+"\n\n") + + zipped = list(zip(values[0],values[1], values[2])) + df = pd.DataFrame(zipped, columns=['VPULSE(V)', 'IPULSE(A)', 'RPULSE(Ohm)']) + + f.write("Results Constant Pulse:\n") + f.write(df.to_string()) + f.write("\n\n\n") + +import ipywidgets as widgets + +#sample interface + +style = {'description_width': 'initial'} + +def constant_pulse(): + voltage = widgets.BoundedFloatText( + value = 10, + min = -100, + max = 100, + step = 1, + description = 'Constant Voltage(V):', + style=style, + ) + + comp = widgets.BoundedFloatText( + value = 0.1, + min = -0.1, + max = 0.1, + step = 0.01, + description = 'Compliance(A):', + style=style, + ) + + pulses = widgets.IntText( + value = 100, + description = 'Number of Pulses:', + style=style, + ) + period = widgets.BoundedFloatText( + value = 5e-3, + min = 5e-3, + max = 1, + step = 5e-3, + description ='Pulse Period(s):', + style=style, + + ) + width = widgets.BoundedFloatText( + value = 5e-4, + min = 5e-4, + max = 1e-1, + step= 5e-4, + description ='Pulse Width(s):', + style=style, + ) + base = widgets.BoundedFloatText( + value = 0, + min = -100, + max = 100, + step = 1, + description = 'Base Voltage(V):', + style=style + ) + + integration =widgets.Dropdown( + options=['SHORt', 'MEDium', 'LONG'], + value='MEDium', + description='Integration:', + style=style + ) + + pulse_parameters = widgets.VBox([pulses,period,width,base]) + smu_parameters = widgets.VBox([voltage,comp,integration]) + + constant_pulse_widgets = widgets.HBox([smu_parameters,pulse_parameters]) + constant_pulse_dict = { + 'voltage': voltage, + 'comp':comp, + 'pulses':pulses, + 'period':period, + 'width':width, + 'base':base, + 'integration':integration + } + return constant_pulse_widgets,constant_pulse_dict + +def sweep_pulse(): + start_voltage = widgets.BoundedFloatText( + value = 0, + min = -100, + max = 100, + step = 1, + description = 'Start Voltage(V):', + style=style, + ) + stop_voltage = widgets.BoundedFloatText( + value = 15, + min = -100, + max = 100, + step = 1, + description = 'Stop Voltage(V):', + style=style, + ) + + comp = widgets.BoundedFloatText( + value = 0.1, + min = -0.1, + max = 0.1, + step = 0.01, + description = 'Compliance(A):', + style=style, + ) + + pulses = widgets.IntText( + value = 100, + description = 'Number of Pulses:', + style=style, + ) + period = widgets.BoundedFloatText( + value = 5e-3, + min = 5e-3, + max = 1, + step = 5e-3, + description ='Pulse Period(s):', + style=style, + + ) + width = widgets.BoundedFloatText( + value = 5e-4, + min = 5e-4, + max = 1e-1, + step= 5e-4, + description ='Pulse Width(s):', + style=style, + ) + base = widgets.BoundedFloatText( + value = 0, + min = -100, + max = 100, + step = 1, + description = 'Base Voltage(V):', + style=style + ) + + integration =widgets.Dropdown( + options=['SHORt', 'MEDium', 'LONG'], + value='MEDium', + description='Integration:', + style=style + ) + + pulse_parameters = widgets.VBox([pulses,period,width,base]) + smu_parameters = widgets.VBox([start_voltage,stop_voltage,comp,integration]) + + sweep_pulse_widgets = widgets.HBox([smu_parameters,pulse_parameters]) + sweep_pulse_dict = { + 'start': start_voltage, + 'stop':stop_voltage, + 'comp':comp, + 'pulses':pulses, + 'period':period, + 'width':width, + 'base':base, + 'integration':integration + } + return sweep_pulse_widgets,sweep_pulse_dict + + diff --git a/hp4155/memristor (Version 4.1)/memristor.py b/hp4155/memristor (Version 4.1)/memristor.py new file mode 100644 index 0000000..a42a905 --- /dev/null +++ b/hp4155/memristor (Version 4.1)/memristor.py @@ -0,0 +1,692 @@ +### this is the new memrstor measurement (set and reset as many times as the user wants and full sweeps with a button) +from help import * +import ipywidgets as widgets +from keyboard import add_hotkey,remove_hotkey +import configparser + +# pulsed libraries +from help_pulse import * + +#create temporary file to store the results localy +temp_file= os.path.join(os.getcwd(),'tempfile.txt') + +# the three naming fields + +sample_series= widgets.Text( + value= '', + placeholder ='Enter text here:', + description = 'sample series:', + style = {'description_width': 'initial'} + ) + +field = widgets.Text( + value= '', + placeholder ='Enter text here:', + description = 'Field:', + style = {'description_width': 'initial'}, + ) + +DUT = widgets.Text( + value= '', + placeholder ='Enter text here:', + description = 'DUT:', + style = {'description_width': 'initial'}, + ) + + +#choose a new folder button +new_folder = widgets.Button(description='change folder') + +image = widgets.Image( + value=open("schematic.png", "rb").read(), + format='png', + width=300, + height=100, +) + +contact_check = widgets.Button(description = 'CONTACT CHECK') +qcc = widgets.Button(description = 'QUICK CONTACT CHECK',layout=widgets.Layout(width='80%'),style={"button_width": "auto"}) +qcc_select = widgets.RadioButtons(description = 'QCC type:',options = ['EBL','OL']) + +vertical1 = widgets.VBox([sample_series,field,DUT,new_folder,contact_check,qcc,qcc_select]) +vertical2 = widgets.VBox([image]) +all_text_boxes = widgets.HBox([vertical1,vertical2]) + + + +#first series of parameters +step = widgets.BoundedFloatText( + value=0.01, + min=0, + max=100, + step=0.01, + description='Step(V):', +) + +integration_time=widgets.Dropdown( + options=['SHORt', 'MEDium', 'LONG'], + value='MEDium', + description='Integration:', + #style = {'description_width': 'initial'}, +) + +sampling=widgets.Checkbox(description='sampling check') + +auto_qcc = widgets.Checkbox( + description = 'Auto QCC after Reset', + style = {'description_width': 'initial'}, + value = True +) + +# THE BUTTONS +#create buttons as it shown in the how_buttons_look +set=widgets.Button(description='SET') +reset=widgets.Button(description='RESET') +full=widgets.Button(description='FULL SWEEP') +number = widgets.BoundedIntText(value=1,min=1,max=sys.maxsize,step=1,description='full sweeps:',disabled=False) #number of measuremts for the full sweep +retention_button=widgets.Button(description='RETENTION') +export_ini_button = widgets.Button(description = 'Export as ini') +import_ini_button = widgets.Button(description='Import from ini') + + + +#parameter boxes +Vset=widgets.BoundedFloatText( + value=1, + min=-100, + max=100, + step=0.1, + description='Voltage(V):', +) + +#parameter buttons +CC_vset=widgets.BoundedFloatText( + value=1e-3, + min=-0.1, + max=0.1, + step=0.01, + description= 'Comp(A):', +) + +#parameter buttons +Vreset=widgets.BoundedFloatText( + value=-1, + min=-100, + max=100, + step=0.1, + description='Voltage(V):', +) + +#parameter buttons +CC_vreset=widgets.BoundedFloatText( + value=1e-3, + min=-0.1, + max=0.1, + step=0.01, + description='Comp(A):', +) + +Vretention=widgets.BoundedFloatText( + value=1, + min=-100, + max=100, + step=1, + description='Voltage(V):', +) + +period=widgets.BoundedFloatText( + value=1, + min=2e-3, + max=65.535, + step=1, + description='Period(s):', +) + +duration=widgets.BoundedFloatText( + value=60, + min=60e-6, + max=1e11, + step=1, + description='Duration(s):', +) + +# for automatic stop of endurance +auto_stop = widgets.Checkbox( + description = 'Auto QCC after Reset', + style = {'description_width': 'initial'}, + value = False +) + +threshold = widgets.FloatText( + description = "Stop Condition: R(HRS)/R(LRS)<", + style = {'description_width': 'initial'} + value = 1000 +) + +#align a button with a checkbox or integer bounded texts horizontaly +line0 = widgets.HBox([step,integration_time,sampling,auto_qcc]) +line1 = widgets.HBox([set,Vset,CC_vset]) +line2 = widgets.HBox([reset,Vreset,CC_vreset]) +line3 = widgets.HBox([full,number,auto_stop,threshold]) +line4 = widgets.HBox([retention_button,Vretention,period,duration]) + +#pack them into a single vertical box +all = widgets.VBox([line0,line1,line2,line3,line4]) +output = widgets.Output() + + +#display all at the end +display(all_text_boxes) + +cons_widgets,cons_dict = constant_pulse() +sweep_widgets,sweep_dict = sweep_pulse() + +sweep_button = widgets.Button(description = "SWEEP PULSE") +cons_button = widgets.Button(description = "CONSTANT PULSE") + + +children = [all,widgets.VBox([sweep_widgets,sweep_button]),widgets.VBox([cons_widgets,cons_button])] +titles = ["Regular","Sweep Pulse","Constant Pulse"] +tab = widgets.Tab() +tab.children = children +tab.titles = titles + +display(tab) +display(widgets.HBox([import_ini,button,export_ini_button])) +display(output) + +all_widgets=[sweep_button,cons_button,sample_series,field,DUT,set,reset,full,new_folder,retention_button,contact_check,qcc,qcc_select,Vset,CC_vset,Vreset,CC_vreset,step,integration_time,number,sampling,Vretention,period,duration,auto_qcc,auto_stop,threshold,export_ini_button,import_ini_button] +add_widgets_to_list(cons_dict,all_widgets) +add_widgets_to_list(sweep_dict,all_widgets) + +# The regular dictionary for ini +regular_parameters={ + 'step': step, + 'integration': integration, + 'set_voltage': Vset, + 'set_compliance': CC_vset, + 'reset_voltage': Vreset, + 'reset_compliance':CC_vreset, + 'number_of_cycles': number, + 'threshold':threshold, + 'retention_voltage':Vretention, + 'retention_period': period, + 'retention_duration':duration +} + +device = hp4155a.HP4155a('GPIB0::17::INSTR') +device.reset() +device.disable_not_smu() + +#choose folder directory +folder=choose_folder() #here buttons dont work yet! + +def on_contact_check_clicked(b): + global folder,temp_file + with output: + clear_output() + change_state(all_widgets) + device.inst.lock_excl() + + filename=f"{sample_series.value}_{field.value}_{DUT.value}.txt" + file = os.path.join(folder,filename) + + R = regular_contact_check(device) + date = str(datetime.today().replace(microsecond=0)) + title = [f"Full Contact Check at {date}"] + + write_to_file(temp_file,title,R) + + #upload results + temp_file,file,folder=upload_results(temp_file,file,folder) + + information_box("Contact Check Completed") + device.inst.unlock() + + change_state(all_widgets) + +def on_qcc_clicked(b): + global folder,temp_file + with output: + clear_output() + change_state(all_widgets) + device.inst.lock_excl() + + filename=f"{sample_series.value}_{field.value}_{DUT.value}.txt" + file = os.path.join(folder,filename) + device.inst.lock_excl() + + if qcc_select.value == 'EBL': + R = EBL(device) + else: # OL + R = OL(device) #df + + date = str(datetime.today().replace(microsecond=0)) + title = [f"Quick Contact Check ({qcc_select.value}) at {date}"] + + write_to_file(temp_file,title,R) + + #upload results + temp_file,file,folder=upload_results(temp_file,file,folder) + + information_box("Quick Contact Check Completed") + + device.inst.unlock() + + change_state(all_widgets) + + +def on_set_button_clicked(b): + global folder,temp_file + with output: + #disable buttons + change_state(all_widgets) + + filename=f"{sample_series.value}_{field.value}_{DUT.value}.txt" + file = os.path.join(folder,filename) + + #lock the device + device.inst.lock_excl() + + clear_output() + + #check values + valid = check_values(step.value,Vset.value,Vreset.value) + + + if valid == True: + if sampling.value == True: #do sampling set before set process(100mV) + R_mean_before= sampling_check(-0.01,device) + + + #execute measurement,plot results and save them + V12,I12 = sweep(0,Vset.value,step.value,CC_vset.value,integration_time.value,device) + plot_sweep(V12,I12,'SET') + df = create_data_frame(V12,I12) + display(df) + + + if sampling.value == True: #do sampling set after set process(10mV) + R_mean_after = sampling_check(0.01,device) + + date = str(datetime.today().replace(microsecond=0)) + title = [f"SET Memristor at {date}",f"Set Voltage={Vset.value}V",f"current compliance={CC_vset.value}A"] + if sampling.value == True: + title.extend([f"R(Ohm) Before/After",f"{R_mean_before} {R_mean_after}"]) + write_to_file(temp_file,title,df) + + #upload results + temp_file,file,folder=upload_results(temp_file,file,folder) + + #show messagebox + information_box("Measurement finished!") + + #unlock device + device.inst.unlock() + + change_state(all_widgets) + +def on_reset_button_clicked(b): + global folder,temp_file + with output: + change_state(all_widgets) + + filename=f"{sample_series.value}_{field.value}_{DUT.value}.txt" + file = os.path.join(folder,filename) + + #lock device + device.inst.lock_excl() + + clear_output() + + #check values + valid = check_values(step.value,Vset.value,Vreset.value) + + + if valid == True: + if sampling.value == True: #do sampling set before reset process(10mV) + R_mean_before = sampling_check(0.01,device) + + #execute measurement,plot results and save them + V34,I34 = sweep(0,Vreset.value,step.value,CC_vreset.value,integration_time.value,device) + plot_sweep(V34,I34,'RESET') + df = create_data_frame(V34,I34) + display(df) + + if sampling.value == True: #do sampling set after reset process(100mV) + R_mean_after = sampling_check(-0.01,device) + + date = str(datetime.today().replace(microsecond=0)) + title =[f"RESET Memristor at {date}",f"Reset Voltage={Vreset.value}V",f"current compliance={CC_vreset.value}A"] + if sampling.value == True: + title.extend([f"R(Ohm) Before/After",f"{R_mean_before} {R_mean_after}"]) + write_to_file(temp_file,title,df) + + #Quick Contact Check after reset Process + if auto_qcc.value == True: + if qcc_select.value == 'EBL': + R=EBL(device) + else: # OL + R=OL(device) + + title = [f"Automatic Quick Contact Check({qcc_select.value}) after Reset"] + write_to_file(temp_file,title,R) + + #upload results + temp_file,file,folder=upload_results(temp_file,file,folder) + + #show messagebox + information_box("Measurement finished!") + + #unlock device + device.inst.unlock() + + change_state(all_widgets) + +def on_full_button_clicked(b): + global folder,temp_file + with output: + change_state(all_widgets) + + filename=f"{sample_series.value}_{field.value}_{DUT.value}.txt" + file = os.path.join(folder,filename) + + # lock device + device.inst.lock_excl() + + clear_output() + + #check values + valid = check_values(step.value,Vset.value,Vreset.value) + date = str(datetime.today().replace(microsecond=0)) + + if valid == True: + with open(temp_file,'a') as f: + header =[f"{number.value} full sweeps with parameters:",f"Set Voltage = {Vset.value}V",f"Current compliance set = {CC_vset.value}A",f"Reset Voltage = {Vreset.value}V",f"Current compliance reset = {CC_vreset.value}A"] + + fig, (ax1, ax2) = plt.subplots(2,sharex=True,figsize=(8,6)) #the plots share the same x axis + fig.suptitle('FULL SWEEP') + ax1.set_title('Linear I') + ax1.set(xlabel='Voltage(V)',ylabel='Current(A)') + ax2.set_title('Logarithmic I') + ax2.set(xlabel='Voltage(V)',ylabel='Current(A)') + ax2.set_yscale('log') + + stop = False + + def break_loop(): + nonlocal stop + stop = True + #help list with the resistances + resistances = [] + + add_hotkey("esc",break_loop) + #execute number of measurements + for i in range(number.value):#here it is easier to implement the sampling checks + clear_output(wait = True) + if i>0: + display(fig) + if sampling.value == True: #before set(100mv) + R_mean_init = sampling_check(-0.01,device) + resistances.append(R_mean_init) + + V12,I12 = sweep(0,Vset.value,step.value,CC_vset.value,integration_time.value,device) #set + plot_sweep(V12,I12,f"SET Iteration {i+1}") + + #after set/before reset + if sampling.value == True: #before set(10mv) + R_mean_set = sampling_check(0.01,device) # Here HRS + resistances.append(R_mean_set) + if auto_stop.value == True and abs(R_mean_set/R_mean_init)< abs(threshold).value: + stop = True + + V34,I34 = sweep(0,Vreset.value,step.value,CC_vreset.value,integration_time.value,device) #reset + plot_sweep(V34,I34,f"RESET Iteration {i+1}") + + + #after reset + if sampling.value == True:#-0.1V + R_mean_reset = sampling_check(-0.01,device) #here LRS + resistances.append(R_mean_reset) + if auto_stop.value == True and abs(R_mean_set/R_mean_reset)< abs(threshold).value: + stop = True + + #Quick Contact Check after reset Process + if auto_qcc.value == True: + if qcc_select.value == 'EBL': + R = EBL(device) + else: # OL + R = OL(device) + + #butterfly curve + V=np.concatenate((V12,V34)) + I=np.concatenate((I12,I34)) + + #create data frame and save to file + df = create_data_frame(V,I) + display(df) + if i == 0 : + header.extend([f"{i+1} Iteration"]) + title = header.copy() + else: + title = [f"{i+1} Iteration"] + if sampling.value == True: + title.extend([f"R(Ohm) INIT/SET/RESET",f"{R_mean_init} {R_mean_set} {R_mean_reset}"]) + + write_to_file(temp_file,title,df) + + if auto_qcc.value == True: + title= [f"Quick Contact Check({qcc_select.value}) after Reset"] + write_to_file(temp_file,title,R) + + #plot results + ax1.plot(V,I) + ax2.plot(V,np.absolute(I)) + fig.tight_layout() + + #check for loop termination + if stop == True: + clear_output(wait= True) + time.sleep(2) + display(fig) + information_box("Endurance stopped after esc (manually) or automatically!") + f.write("endurance stopped!\n\n") + break + else: + clear_output(wait = True) + time.sleep(2) + display(fig) + information_box("Endurance completed!") + f.write("endurance completed!\n\n") + + remove_hotkey('esc') + stop = False + + #plot resistances if sampling value == True or len(resistances) !=0 + if len(resistances)!=0: + indexes = np.arange(1,len(resistances)+1) + resistances = np.absolute(resistances) + + fig, ax = plt.subplots() + + fig.suptitle('Average Resistances from sampling checks') + ax.set(xlabel='Index',ylabel='Resistance(Ohm)',yscale='log') + ax.scatter(indexes,resistances) + display(fig) + + + #upload results + temp_file,file,folder=upload_results(temp_file,file,folder) + + #unlock the device + device.inst.unlock() + change_state(all_widgets) + + +#new_folder clicked +def on_new_folder_button_clicked(b): + global folder + with output: + change_state(all_widgets) + + folder = choose_folder() #choose new folder + + change_state(all_widgets) + +def on_retention_button_clicked(b): + global folder,temp_file + with output: + + change_state(all_widgets) + + + device.inst.lock_excl() + + clear_output() + + filename=f"{sample_series.value}_{field.value}_{DUT.value}.txt" + file = os.path.join(folder,filename) + + #execute measurement + t,R=retention(Vretention.value,period.value,duration.value,device) + plot_retention(t,R) + df=create_retention_data_frame(t,R) + date = str(datetime.today().replace(microsecond=0)) + title =[f"Retention Memristor at {date}",f"Voltage={Vretention.value}V",f"period={period.value}s",f"duration={duration.value}s"] + + write_to_file(temp_file,title,df) + #upload results + temp_file,file,folder=upload_results(temp_file,file,folder) + #show messagebox + information_box("Measurement finished!") + + device.inst.unlock() + + change_state(all_widgets) + + +def on_sweep_button_clicked(b): + with output: + clear_output() + change_state(all_widgets) + check_pulse(sweep_dict) + + sample_dict= { + 'series':sample_series, + 'field':field, + 'dut':DUT + } + + times,values = sweep_meas(sweep_dict,device) + plot_sweep_pulse(values) + save_sweep(folder,sample_dict,values,times,sweep_dict) + change_state(all_widgets) + + +def on_constant_button_clicked(b): + with output: + global first + clear_output() + change_state(all_widgets) + + check_pulse(sweep_dict) + + sample_dict= { + 'series':sample_series, + 'field':field, + 'dut':DUT + } + + times,values = constant_meas(cons_dict,device) + plot_constant_pulse(values) + save_constant(folder,sample_dict,values,times,cons_dict) + change_state(all_widgets) + +def on_export_ini_clicked(b): + with output: + change_state(all_widgets) + config = configparser.ConfigParser() + default_filename = 'memristor.ini' + try: + file = save_as_ini(default_filename) + with open(file,'w') as configfile: + config.add_section('ALL VALUES ARE IN SI-UNITS!') + config.add_section('IT IS RECOMMENDED TO CHANGE THE INI FILE FROM THE INTERFACE AND DO NOT CHANGE ANY VALUES MANUALLY') + + #Regular Parameters + config.add_section('Set-Reset-Endurance-Retention') + for parameter,widget in regular_parameters.items(): + config.set('Set-Reset-Endurance-Retention',parameter,str(widget.value)) + + # Sweep_pulse + config.add_section('Sweep Pulse') + for parameter,widget in sweep_dict.items(): + config.set('Sweep Pulse',parameter,str(widget.value)) + + # Constant Pulse + config.add_section('Constant Pulse') + for parameter,widget in cons_dict.items(): + config.set('Constant Pulse',parameter,str(widget.value)) + + config.write(configfile) + except Exception as e: + information_box(e) + + change_state(all_widgets) + +def on_import_ini_clicked(b): + with output: + disable_widgets(all_widgets) + #load values to the interface + config = configparser.ConfigParser() + try: + file = load_ini() + except Exception as e: + information_box(e) + enable_widgets(all_widgets) + return + + try: + #read the values from each section + config.read(file) + + #Regular + for parameter,widget in regular_parameters.items(): + widget.value = config.get('Set-Reset-Endurance-Retention',parameter) + + #Sweep Pulse + for parameter,widget in sweep_dict.items(): + widget.value = config.get('Sweep Pulse',parameter) + for parameter,widget in cons_dict.items(): + widget.value = config.get('Constant Pulse',parameter) + + information_box("all parameters loaded succesfully") + except Exception as error: + if type(error).__name__ =='NoSectionError': + information_box(f"{error}.Explanation: Section(header) [section] does not exist. Create a new ini file or compare it with functional ini files!") + elif type(error).__name__=='NoOptionError': + information_box(f'{error}.Explanation: The variable name before the equal sign is not recognized. Create a new ini file or compare it with functional ini files!') + elif type(error).__name__ == 'TraitError': + information_box(f'{error}.Explanation: Invalid Parameter Setting. Check if you set an invalid value!') + elif type(error).__name__ =="DuplicateOptionError": + information_box(f"{error}. Explaination: The section contains the setted parameter more than once!") + else: + information_box(f"A {type(error).__name__} has occurred. Create A new ini file") + change_state(all_widgets) + + +#link buttons to widgets (pulsed) +sweep_button.on_click(on_sweep_button_clicked) +cons_button.on_click(on_constant_button_clicked) + +#link buttons with functions +set.on_click(on_set_button_clicked) +reset.on_click(on_reset_button_clicked) +full.on_click(on_full_button_clicked) +new_folder.on_click(on_new_folder_button_clicked) +retention_button.on_click(on_retention_button_clicked) +contact_check.on_click(on_contact_check_clicked) +qcc.on_click(on_qcc_clicked) + +import_ini_button.on_click(on_import_ini_clicked) +export_ini_button.on_click(on_export_ini_clicked) diff --git a/hp4155/memristor (Version 4.1)/memristor_buttons.ipynb b/hp4155/memristor (Version 4.1)/memristor_buttons.ipynb new file mode 100644 index 0000000..c9aa7e1 --- /dev/null +++ b/hp4155/memristor (Version 4.1)/memristor_buttons.ipynb @@ -0,0 +1,86 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "b913930d-b120-4e59-8a42-d9eecb526a61", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "04df3def923445eb95f28afa67430db0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(VBox(children=(Text(value='', description='sample series:', placeholder='Enter text here:', sty…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0eeebb7ea7f64c97814087e1c195f32b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Tab(children=(VBox(children=(HBox(children=(BoundedFloatText(value=0.01, description='Step(V):', step=0.01), D…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ff84a98e2896443cbf3470c994e9b383", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%run memristor.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1047e606-d5cb-420b-892f-766226339854", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/hp4155/memristor (Version 4.1)/schematic.png b/hp4155/memristor (Version 4.1)/schematic.png new file mode 100644 index 0000000000000000000000000000000000000000..7a3a144bfd6c17374838b2d9222e57173fe2fcd8 GIT binary patch literal 17440 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 literal 0 HcmV?d00001 -- GitLab