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!

Snippet Information
Author: jungyu.lee
Language: Python
Created: Oct 28, 2025
Updated: 0 minutes ago
Views: 46
Stars: 1

Links & Files

Additional Files (1):