image scan (NCM)
Python
Code implemented to operate according to the sequence for acquiring NCM images. Default parameters are optimized for the NX10 system and the 'AC160 TS' cantilever. [dependency] - SmartRemote
main
1 file
image scan (NCM)..py
ncm_image.py
23.3 KB
image scan (NCM)..py
23896 bytes
from time import sleep
from glob import glob
from json import dumps
from SmartRemote import SmartRemote
class AFM(SmartRemote):
def __init__(self):
super().__init__()
def clear_channels(self):
""" Clearing all channels"""
mesg=(
"chs = spm.scan.channels.names() \n\
for (var i=0; i < chs.length; i++){ \n\
print('Remove: '+chs[i]) \n\
spm.scan.channels.remove(chs[i]) \n\
}"
)
reply = self.run(mesg)
return reply['result']
def add_channel(self, ch:str='ChannelZHeight'):
"""Add channles
Args:
ch (str, optional): channel name. Defaults to 'ChannelZHeight.
"""
reply = self.run(f"spm.scan.channels.add(\'{ch}\')")
def set_head_mode(self, mode:str='contact'):
"""Set head mode
Args:
mode (str, mode): contact, ncm, tapping. Defaults to 'contact'.
"""
reply = self.run(f'spm.head.setMode(\'{mode}\')')
return reply['result']
def set_scan_geometry(self, pixel_width:int=256, pixel_height:int=256, \
width:int=20, height:int=20,\
offset_x:int=0, offset_y:int=0, rotation:int=0):
"""Set Scan geometry"""
geometry = {"pixelHeight": pixel_height,
"pixelWidth": pixel_width,
"width": width,
"height": height,
"offsetX": offset_x,
"offsetY": offset_y,
"rotation": rotation}
json_geometry = dumps(geometry,indent=2)
reply = self.run(f'spm.scan.setScanGeometry({json_geometry})')
return reply['result']
def set_scan_option(self, sine_scan:bool=False,over_scan:bool=True, \
over_scan_percent:int=5, tow_way:bool=True, \
det_driven:bool=False, force_slope_correction:bool=False, \
interlace:bool=False,slow_scan:str='end',
bias_reduction_lower:float=-1000,
bias_reduction_reduction:float=0.8,
bias_reduction_upper:float=1000,
bias_reduction_use:bool=False):
"""Set Scan option"""
option = {'biasReduction': {'lower': bias_reduction_lower,
'reduction': bias_reduction_reduction,
'upper':bias_reduction_upper,
'use':bias_reduction_use},
'detDriven':det_driven,
'forceSlopeCorrection': force_slope_correction,
'interlace': interlace,
'overScan': {'enable':over_scan,'percent':over_scan_percent},
'sineScan': sine_scan,
'skipScan': {'applied':'scan2Only','height':0.2, 'rate':2,'skipped':'always'},
'slowScan': slow_scan,
'twoWay': tow_way}
json_option = dumps(option,indent=2)
reply = self.run(f'spm.scan.options= {json_option}')
return reply['result']
def set_scan_rate(self, rate:float=1.):
"""Set scan Rate
Args:
rate (float, optional): Hz. Defaults to 1..
Returns:
str: Done
"""
reply = self.run(f'spm.scan.rate = {rate}')
return reply['result']
def enable_xy_servo(self, mode:str='on'):
""" XY servo on/off
Args:
mode (str, optional): on/off. Defaults to 'on'.
"""
reply = self.run(f'spm.xyservo.mode=\'{mode}\'')
return reply['result']
def enable_z_servo(self, mode:str='true'):
assert mode in ['true', 'false']
reply = self.run(f'spm.zservo.enable = {mode}')
return reply['result']
def set_z_servo(self, gain:list=[1,1,0.5,0.5], setpoint:float=1):
"""Set Z servo parameter
Args:
gain (list, optional): _description_. Defaults to [1,1,0.5,0.5].
setpoint (float, optional): _description_. Defaults to 1.
"""
gain_dict = {'z+': gain[0],
'z-': gain[1],
'p':gain[2],
'i':gain[3]}
json_gain = dumps(gain_dict)
reply = self.run('spm.zservo.enable = true\n\
spm.zservo.setpoint.normalized = true')
reply = self.run(f'spm.zservo.gain = {json_gain}\n\
spm.zservo.setpoint.normValue = {setpoint}')
return reply['result']
def set_setpoint(self, setpoint_value:float=15):
reply = self.run(f'spm.zservo.setpoint.value = {setpoint_value}')
return reply['value']
def get_normalized_zservo_setpoint(self):
"""Set Z servo set point (nm)"""
reply = self.run('spm.zservo.nomalized = false')
reply = self.run(f'spm.zservo.setpoint.value')
return reply['value']
def set_normalized_zservo_setpoint(self, setpoint:float):
"""Set Z servo position (nm)"""
reply = self.run('spm.zservo.enable = true\n\
spm.zservo.setpoint.normalized = false')
reply = self.run(f'spm.zservo.setpoint.value = {setpoint}')
return reply['result']
def set_data_location(self, base_dir:str='C:\\SpmData', sub_dir:str='zeroscan' , file_name:str=''):
"""Set data Location
Args:
base_dir (_type_, optional): basedir path. Defaults to 'C:\SpmData'.
file_name (str, optional): file name. Defaults to 'ZeroScan'.
"""
loc = {'baseDir': base_dir,
'subDir': f'{sub_dir}',
'cameraSave': False,
'direction': 'auto',
'fileName' : file_name,
'fileSuffix':' %1_%N_%G',
'jpegSave': False,
'precision':1,
}
json_loc = dumps(loc)
reply = self.run(f'spm.dataLocation={json_loc}')
return reply['result']
def set_approach_option(self, fast_speed:float=600.0, quick_speed:float=100.0, \
slow_speed:float = 10.0, incremental_speed:float=10, \
fast_error_threshold:int=97, error_threshold:int=95, \
target_pos:float=0.0, focus_on_cantilever:bool=True):
"""This is an aggregation of approach option parameters.
It includes quick speed, slow speed, incremental speed, error threshold, target position and approach type.
Args:
fast_speed (float, optional): fast approach speed (Micormeter/second). Defaluts to 600.0
quick_speed (float, optional): quick approach speed (Micrometer/second). Defaults to 100.0.
slow_speed (float, optional): slow approach speed (Micrometer/second). Defaults to 100.0.
incremental_speed (float, optional): incremental approach speed (Micrometer/second). Defaults to 10.0
target_pos (float, optional): Z position target (Micrometer). Defaults to 0.0.
"""
option = {'fastSpeed' : fast_speed,
'quickSpeed': quick_speed,
'slowSpeed' : slow_speed,
'incrementalSpeed': incremental_speed,
'fastErrorThreshold' : fast_error_threshold,
'errorThreshold' : error_threshold,
'targetPos': target_pos,
'focusOnCantilever': focus_on_cantilever
}
json_option = dumps(option)
reply = self.run(f'spm.approach.setOption({json_option})')
def start_approach(self, mode:str='q+s'):
"""Start approach
Args:
mode (str, optional): "quick", "q" for quick approach
"quickAndsafe", "q+s" for quick-and-safe approach
"incremental", "inc" for incremental approach
"incrementalzscanner", "incz" for incremental-zscanner approach .
Defaults to "q+s".
"""
reply = self.run(f'spm.approach.start(\'{mode}\')')
return reply['result']
def start_image_scan(self):
reply = self.run('spm.scan.startImageScan()')
return reply['result']
def stop_scan(self):
reply = self.run('spm.scan.stop()')
return reply['result']
def trigger_image_scan(self):
"""Not waiting until the scan is finished."""
reply = self.run('spm.scan.triggerImageScan()')
return reply['result']
def moveto_xy_stage(self, target_x:float, target_y:float,\
norm_speed_x:float, norm_speed_y:float):
reply = self.run(f'spm.xystage.moveTo({target_x}, {target_y}, {norm_speed_x}, {norm_speed_y})')
return reply['result']
def moveto_z_stage(self,target:float,norm_speed:float):
reply = self.run(f'spm.zstage.moveTo({target}, {norm_speed})')
return reply['result']
def moveto_focus_stage(self,target:float,norm_speed:float):
reply = self.run(f'spm.focusstage.moveTo({target}, {norm_speed})')
return reply['result']
def lift_z_stage(self,dist:float):
""" Lift Z stage
Args:
dist (float): Micrometer
Raises:
ValueError: Negative Number
"""
try:
if dist < 0: raise ValueError('ERROR: Negative number')
except ValueError as e:
print(e)
return False
reply = self.run(f'spm.zstage.move({dist},1)')
return reply['result']
def reset_xy_stage(self):
"""reset XY stage"""
reply = self.run('spm.xystage.reset()')
return reply['result']
def reset_z_stage(self):
"""reset Z stage"""
reply = self.run('spm.zstage.reset()')
return reply['result']
def reset_focus_stage(self):
"""Reset Focus stage"""
reply = self.run('spm.focusstage.reset()')
return reply['result']
# spm.ncm.sweep(startFreq, endFreq, drive)
def ncm_sweep(self, start_freq:float=100 * 1000, end_freq:float=300*1000, drive:float=25.0):
reply = self.run(f'spm.ncm.sweep({start_freq}, {end_freq}, {drive})')
return reply['result']
def ncm_sweep_auto(self, target_amp:float=25.0, start_freq:float=10.0, \
end_freq:float=1000, init_drive:float=25.0): #
"""Starts ncm sweep with auto options
Args:
target_amp (float): Target amplitude (nm)
start_freq (float): The start value of the initial frequency range (Hz)
end_freq (float): The last value of the initial frequency range (Hz)
init_drive (float): The drive strength (%)
"""
reply = self.run(f'spm.ncm.sweepAuto({target_amp}, {start_freq}, {end_freq}, {init_drive})')
return reply['result']
def ncm_sweep_auto_full_range(self):
"""Starts ncm sweep on full_range """
reply = self.run('spm.ncm.sweepAutoFullRange()')
return reply['result']
def set_z_scanner_range(self,percent:int=70):
reply = self.run(f'spm.zscanner.changeRangeTo({percent})')
return reply['result']
def set_xy_scanner_range(self,percent:int=100):
reply = self.run(f'spm.xyscanner.changeRangeTo({percent})')
return reply['result']
def get_ncm_fullFrequency_range_start(self):
reply = self.run('spm.ncm.fullFrequencyRange.start')
return reply['value']
def get_ncm_fullFrequency_range_end(self):
reply = self.run('spm.ncm.fullFrequencyRange.end')
return reply['value']
def get_scan_option(self,param:str):
"""_summary_
Args:
param (str): biasReduction.lower (int),
biasReduction.reduction (float),
biasReduction.upper (int),
biasRedcution.use (bool),
detDriven (bool),
forceSlopeCorrection (bool),
interlace (bool),
overScan.enable (true),
overScan.percent (int),
sineScan (bool),
skipScan.applied (str),
skipScan.height (float),
skipScan.rate (float),
skipScan.skipped (str),
slowScan (str),
twoWay (bool),
"""
reply = self.run(f'__parts__.scan.options().{param}')
return reply['value']
def get_scan_option_all(self):
param_list = ['biasReduction.lower',
'biasReduction.reduction',
'biasReduction.upper',
'biasRedcution.use',
'detDriven',
'forceSlopeCorrection',
'interlace',
'overScan.enable',
'overScan.percent',
'sineScan',
'skipScan.applied',
'skipScan.height',
'skipScan.rate',
'skipScan.skipped',
'slowScan',
'twoWay' ]
result_dict = {}
for param in param_list:
value = self.get_scan_option(param)
result_dict[param] = value
return result_dict
def get_scan_geometry(self,param:str):
"""_summary_
Args:
param (str): direction (str),
height (float),
offsetX (float),
offsetY (float),
pixelHeight (int),
pixelWidth (int),
rotation (float),
width (float)
"""
reply = self.run(f'__parts__.scan.geometry().{param}')
return reply['value']
def get_scan_geometry_all(self):
param_list = ['direction',
'height',
'offsetX',
'offsetY',
'pixelHeight',
'pixelWidth',
'rotation',
'width']
result_dict = {}
for param in param_list:
value = self.get_scan_geometry(param)
result_dict[param] = value
return result_dict
def get_approach_option(self,param:str):
"""_summary_
Args:
param (str): quickSpeed (float),
slowSpeed (float),
incrementalSpeed (float), # 아직 안 됨.
errorThreshold (int),
targetPos (float)
"""
reply = self.run(f'__parts__.approach.{param}()')
return reply['value']
def get_approach_option_all(self):
param_list = ['fastSpeed',
'quickSpeed',
'slowSpeed',
'fastErrorThreshold',
'errorThreshold',
'targetPos',
'focusOnCantilever']
result_dict = {}
for param in param_list:
value = self.get_approach_option(param)
result_dict[param] = value
return result_dict
def set_scan_geometry_dict(self,dict_geo):
self.set_scan_geometry(pixel_width = int(dict_geo['pixelWidth']),
pixel_height = int(dict_geo['pixelHeight']),
width = int(dict_geo['width']),
height = int(dict_geo['height']),
offset_x = int(dict_geo['offsetX']),
offset_y = int(dict_geo['offsetY']),
rotation = int(dict_geo['rotation']))
def set_scan_option_dict(self,dict_opt):
self.set_scan_option(sine_scan = True if dict_opt['sineScan'] == 'true' else False,
over_scan = True if dict_opt['overScan.enable'] == 'true' else False,
over_scan_percent = int(dict_opt['overScan.percent']),
tow_way = True if dict_opt['twoWay'] == 'true' else False,
det_driven = True if dict_opt['detDriven'] =='true' else False,
force_slope_correction = True if dict_opt['forceSlopeCorrection'] == 'true' else False,
interlace = True if dict_opt['interlace'] == 'true' else False,
slow_scan = str(dict_opt['slowScan']))
def set_approach_option_dict(self,dict_opt):
self.set_approach_option(fast_speed = float(dict_opt['fastSpeed']),
quick_speed = float(dict_opt['quickSpeed']),
slow_speed = float(dict_opt['slowSpeed']),
incremental_speed = float(10),
fast_error_threshold = int(dict_opt['fastErrorThreshold']),
error_threshold = int(dict_opt['errorThreshold']),
target_pos = float(dict_opt['targetPos']),
focus_on_cantilever = True if dict_opt['focusOnCantilever'] =='true' else False)
class NCM_Image():
def __init__(self, afm:AFM=None):
self.afm = afm if afm else AFM()
self.base_dir = ''
self.sub_dir = 'NCM Image'
self.file_name = 'RAW'
self.parameter = {
"gain": [
1, 1, 1, 1],
"xy_scanner_range": 100,
"z_scanner_range": 70,
"pixels": [256, 256],
"scan_rate": 0.5,
"scan_size": [20, 20],
"xy_stage_move_to_enable": False,
"xy_stage_move_to_x": 0.0,
"xy_stage_move_to_y": 0.0,
"xy_stage_move_to_lift_z": 3000.0,
"amplitude_setpoint_enable": False,
"amplitude": 20,
"setpoint": 14,
"frequency_range_enable": False,
"frequency_range_start": 0,
"frequency_range_end": 5000000
}
def move_to(self):
if self.parameter['xy_stage_move_to_enable']:
z = self.parameter['xy_stage_move_to_lift_z']
x = self.parameter['xy_stage_move_to_x']
y = self.parameter['xy_stage_move_to_y']
try:
self.afm.lift_z_stage(z)
except:
print('Lift Fail')
return None
self.afm.moveto_xy_stage(x,y,1,1)
else:
pass
def set(self):
self.afm.set_data_location(self.base_dir, self.sub_dir, self.file_name)
self.afm.set_head_mode('ncm')
self.afm.clear_channels()
self.afm.add_channel('ChannelNcmAmplitude')
self.afm.add_channel('ChannelNcmPhase')
self.afm.add_channel('ChannelErrorSignal')
self.afm.add_channel('ChannelZDriveOrTopography')
self.afm.stop_scan()
self.afm.set_z_scanner_range(self.parameter['z_scanner_range'])
self.afm.set_xy_scanner_range(self.parameter['xy_scanner_range'])
self.bck_scan_geometry = self.afm.get_scan_geometry_all()
self.bck_scan_option = self.afm.get_scan_option_all()
self.bck_approach_option = self.afm.get_approach_option_all()
self.afm.set_scan_geometry(self.parameter['pixels'][0],self.parameter['pixels'][1],
self.parameter['scan_size'][0], self.parameter['scan_size'][0],
)
self.afm.set_scan_option(over_scan_percent=5)
self.afm.set_scan_rate(self.parameter['scan_rate'])
self.afm.enable_xy_servo()
self.afm.enable_z_servo()
self.afm.set_z_servo(self.parameter['gain'], )
if self.parameter['amplitude_setpoint_enable']:
self.sub_dir += '\amplitude_enable'
if self.parameter['frequency_range_enable']:
start_freq = self.parameter['frequency_range_start']
end_freq = self.parameter['frequency_range_end']
self.afm.ncm_sweep_auto(self.parameter['amplitude'] , start_freq, end_freq)
else:
start_freq = self.afm.get_ncm_fullFrequency_range_start()
end_freq = self.afm.get_ncm_fullFrequency_range_end()
self.afm.ncm_sweep_auto(self.parameter['amplitude'] , start_freq, end_freq)
else:
self.sub_dir += '\amplitude_auto'
self.afm.ncm_sweep_auto_full_range()
self.afm.set_approach_option()
def unset(self):
self.afm.set_scan_option_dict(self.bck_scan_option)
self.afm.set_scan_geometry_dict(self.bck_scan_geometry)
self.afm.set_scan_option_dict(self.bck_scan_option)
def approach(self):
self.afm.start_approach('q+s')
self.afm.lift_z_stage(10)
if self.parameter['amplitude_setpoint_enable']:
if self.parameter['frequency_range_enable']:
start_freq = self.parameter['frequency_range_start']
end_freq = self.parameter['frequency_range_end']
self.afm.ncm_sweep_auto(self.parameter['amplitude'] , start_freq, end_freq)
self.afm.set_setpoint(self.parameter['setpoint'])
else:
start_freq = self.afm.get_ncm_fullFrequency_range_start()
end_freq = self.afm.get_ncm_fullFrequency_range_end()
self.afm.ncm_sweep_auto(self.parameter['amplitude'] , start_freq, end_freq)
self.afm.set_setpoint(self.parameter['setpoint'])
else:
self.afm.ncm_sweep_auto_full_range()
self.afm.start_approach('q+s')
def run(self):
self.afm.trigger_image_scan()
isscan = True
while isscan:
test = self.afm.query_scan_status()
if test == 'true':isscan=True;
else:isscan=False
sleep(2)
def done(self):
self.afm.lift_z_stage(100)
def stop(self):
self.afm.stop_scan()
def analyze(self):
find_dir = self.base_dir + '\\' + self.sub_dir
tiff_list = glob(find_dir + '/*.tiff')
for tiff_path in tiff_list:
tiff_name = tiff_path.split('\\')[-1]
tmp_name = tiff_name.split('.')[0]
condition_1 = tmp_name.split('_')[-3] == 'Z Height'
condition_2 = tmp_name.split('_')[-2] == 'Forward'
tmp_name_list = tiff_name.split('_')
tmp_name = ''
for s in tmp_name_list[1:]: tmp_name += '_' + s
tmp_name = tmp_name.split('.')[0]
tmp_name = tmp_name[2:]
if condition_1 & condition_2:
print(tiff_path)
if __name__ == "__main__":
ncm_image = NCM_Image()
ncm_image.base_dir = 'C:\\SpmData'
ncm_image.sub_dir = 'NCM Image Test'
ncm_image.file_name = 'NCM_Test'
ncm_image.set()
ncm_image.move_to()
ncm_image.approach()
ncm_image.run()
ncm_image.done()
ncm_image.unset()
ncm_image.analyze()
Comments (0)
No comments yet. Be the first to comment!