Hooks
Hooks allow you to add or replace key components of Scanbot's functionality where system-specific customisation is required.
The ability to do this without rewriting source code means you can pull Scanbot updates without losing your custom functionality. For example, Scanbot monitors a camera feed when tracking the motion of the STM
head during automated operation; a task that may require a light to be switched on or off at times. Obviously, the code needed to achieve this will vary from system to system. This is where the user-written hook
hk_light.py
would be called by Scanbot to perform such a task. This page details all the hooks currently available to Scanbot. Feel free to raise an issue on the GitHub page if you require a hook at a certain
point in Scanbot's source code. All hooks must be saved in the directory ~/scanbot/scanbot/
prior to running pip install .
from the top level directory.
hk_light
This hook is called by any function that requires control over the STM light when monitoring the camera feed.
- Inputs: None
- Outputs: None
- Error handling: Raise an
Exception
if unable to turn the light on or off
Structure:
hk_light.py must contain two functions: turn_on
and turn_off
. An example is shown below:
def turn_on():
# Code to turn light on
# If unsuccessful:
# Raise Exception
# If successful:
print("Light on!")
def turn_off():
# Code to turn light off
# If unsuccessful:
# Raise Exception
# If successful:
print("Light off!")
hk_survey
This hook can be called at the completion of every scan in a survey
or survey2
when the -hk_survey
option is set.
- Inputs:
- Raw scan data
- .sxm filename
- A Python dictionary containing metadata about the scan
- Outputs: None
- Error handling: None
Structure:
hk_survey.py must contain the function run
. An example is shown below:
def run(scan_data,filename,metadata):
# Code to process the image (e.g. plane fit, filtering, etc.)
# Code to save the image or send the image to a server
# Code to do anything you want
hk_classifier
This hook can called at the completion of every scan in a survey
or survey2
when the -autotip
and -hk_classifier
options are set.
Its purpose is to use an alternative image classifier when assessing the quality of completed STM images and to decide when Scanbot should reshape the tip.
- Inputs:
- Raw scan data
- .sxm filename
- Record of all previous classifications - a list of dictionaries
- Outputs:
- A python dictionary containing classification labels. The only mandatory label is "tipShape". This is what tells Scanbot whether to execute a tip shape or not.
- Error handling: None
Structure: hk_classifier.py must contain the function run
. An example is shown below:
def run(scan_data,filename,classification_hist):
# Code to process the image (e.g. plane fit, filtering, etc.)
# API call to external classifier (e.g. AI agent)
classification = {"tipChanges": <output from classifier>, # Example trait to keep track of
"unstable": <output from classifier>, # Example trait to keep track of
"multipleTips": <output from classifier>, # Example trait to keep track of
# Check the classification history to see if we've had a bad run of images
if(num_bad_images > threshold):
classification["tipShape"] = True # Tell Scanbot to reshape the tip
else:
classification["tipShape"] = False # Tell Scanbot not to reshape the tip
return classification
hk_tipShape
This hook can be called from the auto_tip_shape command, prior to performing a tip shaping action, when the -hk_tipShape
option is set.
Its purpose is to adjust the tip shaping parameters based on either the image of the tip's imprint or the size and symmetry scores assigned to the imprint by Scanbot.
- Inputs:
- Image of the region prior to making the imprint
- Image of the tip's imprint
- 1D array containing all the tip shaping properties. See nanonisTCP.TipShaper.
- Array containing target size (nm2 - smaller = better) and circularity (0 = asymmetric, 1 = perfect circle) scores of the imprint.
- Array containing actual size and circularity scores assigned by Scanbot
- history: a variable to pass anything out at the end of the function, which will be passed back in on the next iteration. This is a way to keep track of what's happened in previous attempts.
- Outputs:
- 1D array containing the modified tip shaping parameters.
- History variable that contains anything you would like to keep track of. This will be passed back into the hook on the next iteration.
- Error handling: None
Structure: hk_tipShape.py must contain the funtion run
. An example is show below:
def run(cleanImage, tipImprint, tipShapeProps, target, actual, history):
# Code to filter tipImprint
# API call to RL Agent passing in tipImprint and history to determine next tip-shaping parameters.
# Adjust relevant tip shaping parameters
tipShapeProps[3] = agent.z1 # Amount to plunge the tip into the surface (m)
tipShapeProps[5] = agent.bias # Bias applied while the tip is in the suface (V)
tipShapeProps[7] = agent.z2 # Amount to pull tip out of the surface (m)
record = {"z1": agent.z1, # Make a record of this tip-shaping attempt
"bias": agent.bias,
"z2": agent.z2,
"imprint": agent.imprint)
history.append(record) # Append it to the running history so it can be taken into account on the next iteration
return tipShapeProps, history # Return the updated tip shaping properties
hk_commands (coming soon to web app version)
This hook is used to implement custom written commands into Scanbot. To enable it, set hk_commands=1
in scanbot_config.ini
.
Commands in hk_commands.py that have the same name as any existing Scanbot command will take priority.
Structure: hk_commands.py must contain the class hk_commands
and contain the class variable commands
. commands
is a python dictionary
that maps the command name (key), as called by the user, to the function that handles it.
Scanbot commands can be run either in the main thread or in a separate thread. The below Python code shows the implementation of two similar commands,
the first, change_bias
, is a scenario where threading may be unnecessary, while in the second, change_bias2
, it could be useful.
To test this script:
- Set
hk_commands=1
inscanbot_config.ini
- Copy and paste the below code to ~/scanbot/scanbot/hk_commands.py
- Restart Scanbot
python scanbot_interface.py -c
- Try running
help change_bias
or simplychange_bias -V=1
hk_commands.py
from nanonisTCP.Bias import Bias # Import the NanonisTCP Bias Module. Used to control the tip bias
import global_ # Import the global variables
import time # Import time for sleeping
class hk_commands(object):
def __init__(self,interface): # Set up the constructor like this
self.interface = interface # Reference to scanbot_interface
self.initCommandDict() # Initialise the dictionary containing a list of custom commands
def initCommandDict(self):
self.commands = { # A dictionary that maps commands with function names.
'change_bias' : self.changeBias, # An example command without threading. Here, the user will call change_bias
'change_bias2' : self.changeBias2, # An example command with threading. Here, the user will call change_bias2
}
def changeBias(self,user_args,_help=False):
"""
This function changes the bias in Nanonis. It performs this task
without the use of multi-threading which means it cannot Scanbot will
be busy until the task is complete. This is fine for commands that will
not interfere with other threaded tasks (e.g. taking control of the
motors while a survey is running). Tasks that are not threaded can run
while a threaded task is already running. Tasks that are not threaded
cannot be stopped until complete (i.e. running 'stop' command will not
work).
Parameters
----------
user_args : arguments passed in by the user when calling the command
_help : flag set when user calls "help change_bias"
"""
arg_dict = {'-V' : ['0', lambda x: float(x), "(float) Set the tip bias."],
'-arg2' : ['some str', lambda x: str(x), "(str) An example user argument that defaults to 'some str' is a string."]}
if(_help): return arg_dict # This will get called when the user runs the command 'help exUnthreaded'
error,user_arg_dict = self.interface.userArgs(arg_dict,user_args) # This will validate args V and arg2 as float() and str(), respectively
if(error):
return error + "\nRun ```help change_bias``` if you're unsure." # Inform the user of any errors in their input
V,arg2 = self.interface.unpackArgs(user_arg_dict) # Unpack the arguments
if(V == 0): # Validation
errorMessage = "Cannot set bias to zero!"
return errorMessage # Return with error
NTCP,connection_error = self.interface.scanbot.connect() # Connect to nanonis via TCP
if(connection_error):
return connection_error # Return error message if there was a problem connecting
biasModule = Bias(NTCP) # The NanonisTCP Bias module
biasModule.Set(V) # Set the bias in Nanonis
self.interface.sendReply("Bias set to " + str(V) + "! arg2 = " + arg2) # This is how you send a reply.
self.interface.scanbot.disconnect(NTCP) # Remember to free up the TCP port
return # Return None for success
def changeBias2(self,user_args,_help=False):
"""
This function will change the bias in Nanonis by threading the function
"threadedFunction" after validating user input. Only one threaded
function can run at a time. If the user tries to run a second threaded
function, they will be presented with an error. This is handled by the
global flags in global_. Threaded tasks run in the background and can
be stopped by the user running the "stop" command.
Parameters
----------
user_args : arguments passed in by the user when calling the command
_help : flag set when user calls "help change_bias2"
"""
arg_dict = {'-V' : ['0', lambda x: float(x), "(float) Set the tip bias."],
'-wait' : ['10', lambda x: int(x), "(int) Seconds to wait after changing bias."]}
if(_help): return arg_dict # This will get called when the user runs the command 'help exUnthreaded'
error,user_arg_dict = self.interface.userArgs(arg_dict,user_args) # This will validate args V and wait as float() and int(), respectively
if(error):
return error + "\nRun ```help change_bias2``` if you're unsure." # Inform the user of any errors in their input
args = self.interface.unpackArgs(user_arg_dict) # Unpack the arguments
func = lambda : self.threadedFunction(*args) # Handle to the function to be threaded
return self.interface.threadTask(func) # Return and thread the function
def threadedFunction(self,V,wait):
"""
This function changes the bias in Nanonis and then waits a specified
amount of time.
This function is run on a new thread. It will run in the backgound
while Scanbot can still reveive commands. It should periodically check
event flags by calling 'self.interface.scanbot.checkEventFlags()'. If
True is returned, the 'stop' function has been called by the user.
Whenever returning, the global running flag should be cleared by
calling global_.running.clear().
Parameters
----------
V : Change the bias to this value
wait : Wait this many seconds after changing the bias
"""
if(V == 0): # Validation
errorMessage = "Cannot set bias to zero!"
global_.running.clear() # Free up the running flag. This must be done whenever exiting a threaded function
return errorMessage # Return with error
NTCP,connection_error = self.interface.scanbot.connect() # Connect to nanonis via TCP
if(connection_error):
global_.running.clear() # Free up the running flag. This must be done whenever exiting a threaded function
return connection_error # Return error message if there was a problem connecting
biasModule = Bias(NTCP) # The NanonisTCP Bias module
biasModule.Set(V) # Set the bias in Nanonis
self.interface.sendReply("Bias set to " + str(V)) # This is how you send a reply.
self.interface.sendReply("Waiting " + str(wait) + " seconds...") # This is how you send a reply.
seconds = 0
while(seconds < wait):
time.sleep(1)
seconds += 1 # Keep track of how many seconds have gone by
if(self.interface.scanbot.checkEventFlags() == True): # Periodically check event flags when appropriate to see if the user has called "stop"
self.interface.sendReply("Stopping early!") # This is how you send a reply.
break
self.interface.sendReply("Done!") # This is how you send a reply.
self.interface.scanbot.disconnect(NTCP) # Remember to free up the TCP port
global_.running.clear() # Free up the running flag. This must be done whenever exiting a threaded function
return