nanonis.py 14 KB
Newer Older
1
2
# -*- coding: utf-8 -*-

3
4
from __future__ import (division, print_function, absolute_import,
                        unicode_literals)
5

Chris Smith's avatar
Chris Smith committed
6
import os
7
from warnings import warn
Chris Smith's avatar
Chris Smith committed
8
import numpy as np
9
import h5py
10
11
12
13
14
15
16
17

from sidpy.sid import Translator
from sidpy.hdf.hdf_utils import write_simple_attrs

from pyUSID.io.hdf_utils import create_indexed_group, write_main_dataset,\
    write_ind_val_dsets
from pyUSID.io.write_utils import Dimension

Chris Smith's avatar
Chris Smith committed
18
from .df_utils.nanonis_utils import read_nanonis_file
19
# TODO: Adopt any missing features from https://github.com/paruch-group/distortcorrect/blob/master/afm/filereader/nanonisFileReader.py
Chris Smith's avatar
Chris Smith committed
20
21


22
class NanonisTranslatorCorrect(Translator):
Chris Smith's avatar
Chris Smith committed
23
    """
24
25
26
27
28
    Translator for Nanonis data files.

    This translator provides method to translate Nanonis data files
    (3ds, sxm, and dat) into Pycroscopy compatible HDF5 files.

Chris Smith's avatar
Chris Smith committed
29
    """
30
    def __init__(self, *args, **kwargs):
Chris Smith's avatar
Chris Smith committed
31
32
        super(Translator, self).__init__(*args, **kwargs)

33
34
35
        self.data_path = None
        self.folder = None
        self.basename = None
Chris Smith's avatar
Chris Smith committed
36
37
        self.parm_dict = None
        self.data_dict = None
38
        self.h5_path = None
Chris Smith's avatar
Chris Smith committed
39
40
41
42
43
44
45
46
47
48
49
50

    def get_channels(self):
        """
        Read the file and print the list of channels.

        Returns
        -------
        None
        """
        self._read_data(self.data_path)

        print("The following channels were found in the file:")
Chris Smith's avatar
Chris Smith committed
51
        for channel in self.parm_dict['channel_parms'].keys():
Chris Smith's avatar
Chris Smith committed
52
53
54
55
56
57
            print(channel)

        print('You may specify which channels to use when calling translate.')

        return

58
    def translate(self, filepath, data_channels=None, verbose=False):
Chris Smith's avatar
Chris Smith committed
59
        """
60
        Translate the data into a Pycroscopy compatible HDF5 file.
Chris Smith's avatar
Chris Smith committed
61
62
63

        Parameters
        ----------
64
65
        filepath : str
            Path to the input data file.
Chris Smith's avatar
Chris Smith committed
66
67
68
69
70
71
72
73
74
75
76
77
        data_channels : (optional) list of str
            Names of channels that will be read and stored in the file.
            If not given, all channels in the file will be used.
        verbose : (optional) Boolean
            Whether or not to print statements

        Returns
        -------
        h5_path : str
            Filepath to the output HDF5 file.

        """
78
79
80
81
82
83
84
85
        filepath = os.path.abspath(filepath)
        folder, basename = self._parse_file_path(filepath)

        self.data_path = filepath
        self.folder = folder
        self.basename = basename
        self.h5_path = os.path.join(folder, basename + '.h5')

Chris Smith's avatar
Chris Smith committed
86
87
88
89
        if self.parm_dict is None or self.data_dict is None:
            self._read_data(self.data_path)

        if data_channels is None:
90
            print('No channels specified. All channels in file will be used.')
Chris Smith's avatar
Chris Smith committed
91
            data_channels = self.parm_dict['channel_parms'].keys()
Chris Smith's avatar
Chris Smith committed
92
93
94
95
96
97

        if verbose:
            print('Using the following channels')
            for channel in data_channels:
                print(channel)

98
99
        if os.path.exists(self.h5_path):
            os.remove(self.h5_path)
Chris Smith's avatar
Chris Smith committed
100

101
        h5_file = h5py.File(self.h5_path, 'w')
Chris Smith's avatar
Chris Smith committed
102

103
        # Create measurement group and assign attributes
104
        meas_grp = create_indexed_group(h5_file, 'Measurement')
105
106
107
        write_simple_attrs(
            meas_grp, self.parm_dict['meas_parms']
        )
Chris Smith's avatar
Chris Smith committed
108

109
110
        # Create datasets for positional and spectroscopic indices and values
        spec_dim = self.data_dict['Spectroscopic Dimensions']
111
        pos_dims = self.data_dict['Position Dimensions']
112
113
114
115
        h5_pos_inds, h5_pos_vals = write_ind_val_dsets(meas_grp, pos_dims,
                                                       is_spectral=False)
        h5_spec_inds, h5_spec_vals = write_ind_val_dsets(meas_grp, spec_dim,
                                                         is_spectral=True)
Chris Smith's avatar
Chris Smith committed
116

117
        # Create the datasets for all the channels
118
        num_points = h5_pos_inds.shape[0]
Chris Smith's avatar
Chris Smith committed
119
        for data_channel in data_channels:
Chris Smith's avatar
Chris Smith committed
120
            raw_data = self.data_dict[data_channel].reshape([num_points, -1])
Chris Smith's avatar
Chris Smith committed
121

122
            chan_grp = create_indexed_group(meas_grp, 'Channel')
123
124
125
126
127
            data_label = data_channel
            data_unit = self.parm_dict['channel_parms'][data_channel]['Unit']
            write_simple_attrs(
                chan_grp, self.parm_dict['channel_parms'][data_channel]
            )
128
129
130
            write_main_dataset(chan_grp, raw_data, 'Raw_Data',
                               data_label, data_unit,
                               None, None,
131
132
133
134
                               h5_pos_inds=h5_pos_inds,
                               h5_pos_vals=h5_pos_vals,
                               h5_spec_inds=h5_spec_inds,
                               h5_spec_vals=h5_spec_vals)
135
            h5_file.flush()
Chris Smith's avatar
Chris Smith committed
136

137
        h5_file.close()
Chris Smith's avatar
Chris Smith committed
138
139
140
141
        print('Nanonis translation complete.')

        return self.h5_path

142
    def _read_data(self, file_path):
Chris Smith's avatar
Chris Smith committed
143
        """
144
        Extracting data and parameters from Nanonis files.
Chris Smith's avatar
Chris Smith committed
145
146
147

        Parameters
        ----------
148
        file_path : str
Chris Smith's avatar
Chris Smith committed
149
150
151
152
153
154
155
            File path to the source data file

        Returns
        -------
        None

        """
156
        data, file_ext = read_nanonis_file(file_path)
Chris Smith's avatar
Chris Smith committed
157

Chris Smith's avatar
Chris Smith committed
158
159
        header_dict = data.header
        signal_dict = data.signals
Chris Smith's avatar
Chris Smith committed
160

Chris Smith's avatar
Chris Smith committed
161
        if file_ext == '.3ds':
162
163
            parm_dict, data_dict = self._parse_3ds_parms(header_dict,
                                                         signal_dict)
Chris Smith's avatar
Chris Smith committed
164
        elif file_ext == '.sxm':
165
166
            parm_dict, data_dict = self._parse_sxm_parms(header_dict,
                                                         signal_dict)
Chris Smith's avatar
Chris Smith committed
167
        else:
168
169
            parm_dict, data_dict = self._parse_dat_parms(header_dict,
                                                         signal_dict)
Chris Smith's avatar
Chris Smith committed
170
171

        self.parm_dict = parm_dict
172
        self.data_dict = data_dict
Chris Smith's avatar
Chris Smith committed
173
174
175

        return

Chris Smith's avatar
Chris Smith committed
176
177
178
    @staticmethod
    def _parse_sxm_parms(header_dict, signal_dict):
        """
179
        Parse sxm files.
Chris Smith's avatar
Chris Smith committed
180
181
182

        Parameters
        ----------
183
184
        header_dict : dict
        signal_dict : dict
Chris Smith's avatar
Chris Smith committed
185
186
187

        Returns
        -------
188
        parm_dict : dict
Chris Smith's avatar
Chris Smith committed
189
190
191

        """
        parm_dict = dict()
192
193
194
        data_dict = dict()

        # Create dictionary with measurement parameters
195
196
197
        meas_parms = {key: value for key, value in header_dict.items()
                      if value is not None}
        info_dict = meas_parms.pop('data_info')
198
199
200
201
202
203
204
205
206
207
208
209
210
211
        parm_dict['meas_parms'] = meas_parms

        # Create dictionary with channel parameters
        channel_parms = dict()
        channel_names = info_dict['Name']
        single_channel_parms = {name: dict() for name in channel_names}
        for field_name, field_value, in info_dict.items():
            for channel_name, value in zip(channel_names, field_value):
                single_channel_parms[channel_name][field_name] = value
        for value in single_channel_parms.values():
            if value['Direction'] == 'both':
                value['Direction'] = ['forward', 'backward']
            else:
                direction = [value['Direction']]
212
        scan_dir = meas_parms['scan_dir']
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
        for name, parms in single_channel_parms.items():
            for direction in parms['Direction']:
                key = ' '.join((name, direction))
                channel_parms[key] = dict(parms)
                channel_parms[key]['Direction'] = direction
                data = signal_dict[name][direction]
                if scan_dir == 'up':
                    data = np.flip(data, axis=0)
                if direction == 'backward':
                    data = np.flip(data, axis=1)
                data_dict[key] = data
        parm_dict['channel_parms'] = channel_parms

        # Position dimensions
        num_cols, num_rows = header_dict['scan_pixels']
        width, height = header_dict['scan_range']
        pos_names = ['X', 'Y']
        pos_units = ['nm', 'nm']
        pos_vals = np.vstack([
            np.linspace(0, width, num_cols),
            np.linspace(0, height, num_rows),
        ])
        pos_vals *= 1e9
        pos_dims = [Dimension(name, unit, values) for name, unit, values
                    in zip(pos_names, pos_units, pos_vals)]
        data_dict['Position Dimensions'] = pos_dims

        # Spectroscopic dimensions
        spec_dims = Dimension('arb.', 'a. u.', np.arange(1, dtype=np.float32))
        data_dict['Spectroscopic Dimensions'] = spec_dims

244
        return parm_dict, data_dict
Chris Smith's avatar
Chris Smith committed
245
246
247
248

    @staticmethod
    def _parse_3ds_parms(header_dict, signal_dict):
        """
249
        Parse 3ds files.
Chris Smith's avatar
Chris Smith committed
250
251
252

        Parameters
        ----------
253
254
        header_dict : dict
        signal_dict : dict
Chris Smith's avatar
Chris Smith committed
255
256
257

        Returns
        -------
258
        parm_dict : dict
Chris Smith's avatar
Chris Smith committed
259
260
261

        """
        parm_dict = dict()
262
263
264
        data_dict = dict()

        # Create dictionary with measurement parameters
265
266
267
268
269
        meas_parms = {key: value for key, value in header_dict.items()
                      if value is not None}
        channels = meas_parms.pop('channels')
        for key, parm_grid in zip(meas_parms.pop('fixed_parameters')
                                  + meas_parms.pop('experimental_parameters'),
Chris Smith's avatar
Chris Smith committed
270
                                  signal_dict['params'].T):
271
272
            # Collapse the parm_grid along one axis if it's constant
            # along said axis
273
274
275
276
277
278
279
280
281
282
283
284
            if parm_grid.ndim > 1:
                dim_slice = list()
                # Find dimensions that are constant
                for idim in range(parm_grid.ndim):
                    tmp_grid = np.moveaxis(parm_grid.copy(), idim, 0)
                    if np.all(np.equal(tmp_grid[0], tmp_grid[1])):
                        dim_slice.append(0)
                    else:
                        dim_slice.append(slice(None))
                # print(key, dim_slice)
                # print(parm_grid[tuple(dim_slice)])
                parm_grid = parm_grid[tuple(dim_slice)]
285
286
            meas_parms[key] = parm_grid
        parm_dict['meas_parms'] = meas_parms
287

288
289
290
        # Create dictionary with channel parameters and
        # save channel data before renaming keys
        data_channel_parms = dict()
291
        for chan_name in channels:
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
            splitted_chan_name = chan_name.split(maxsplit=2)
            if len(splitted_chan_name) == 2:
                direction = 'forward'
            elif len(splitted_chan_name) == 3:
                direction = 'backward'
                splitted_chan_name.pop(1)
            name, unit = splitted_chan_name
            key = ' '.join((name, direction))
            data_channel_parms[key] = {'Name': name,
                                       'Direction': direction,
                                       'Unit': unit.strip('()'),
                                       }
            data_dict[key] = signal_dict.pop(chan_name)
        parm_dict['channel_parms'] = data_channel_parms

        # Add remaining signal_dict elements to data_dict
        data_dict.update(signal_dict)

        # Position dimensions
Chris Smith's avatar
Chris Smith committed
311
        nx, ny = header_dict['dim_px']
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
        if 'X (m)' in parm_dict:
            row_vals = parm_dict.pop('X (m)')
        else:
            row_vals = np.arange(nx, dtype=np.float32)

        if 'Y (m)' in parm_dict:
            col_vals = parm_dict.pop('Y (m)')
        else:
            col_vals = np.arange(ny, dtype=np.float32)
        pos_vals = np.hstack([row_vals.reshape(-1, 1),
                              col_vals.reshape(-1, 1)])
        pos_names = ['X', 'Y']
        pos_dims = [Dimension(label, 'nm', values)
                    for label, values in zip(pos_names, pos_vals.T)]
        data_dict['Position Dimensions'] = pos_dims
327
328
329
330
331
332
333
334
335
336

        # Spectroscopic dimensions
        sweep_signal = header_dict['sweep_signal']
        spec_label, spec_unit = sweep_signal.split(maxsplit=1)
        spec_unit = spec_unit.strip('()')
        # parm_dict['sweep_signal'] = (sweep_name, sweep_unit)
        dc_offset = data_dict['sweep_signal']
        spec_dim = Dimension(spec_label, spec_unit, dc_offset)
        data_dict['Spectroscopic Dimensions'] = spec_dim

337
        return parm_dict, data_dict
Chris Smith's avatar
Chris Smith committed
338
339
340
341

    @staticmethod
    def _parse_dat_parms(header_dict, signal_dict):
        """
342
        Parse dat files.
Chris Smith's avatar
Chris Smith committed
343
344
345

        Parameters
        ----------
346
347
        header_dict : dict
        signal_dict : dict
Chris Smith's avatar
Chris Smith committed
348
349
350

        Returns
        -------
351
        parm_dict : dict
Chris Smith's avatar
Chris Smith committed
352
353
354
355

        """
        pass

Chris Smith's avatar
Chris Smith committed
356
357
    def _parse_file_path(self, file_path):
        """
358
        Get the folder and base filename for the input data file.
Chris Smith's avatar
Chris Smith committed
359
360
361
362
363
364
365
366
367
368
369

        Parameters
        ----------
        file_path : str
            Path to the input data file

        Returns
        -------
        folder_path : str
            Path to the directory containing the input data file
        basename : str
370
371
            The base of the input file after stripping away the
            extension and folder from the path
Chris Smith's avatar
Chris Smith committed
372
373
374
375
376
377
378

        """
        # Get the folder and basename from the file path
        (folder_path, basename) = os.path.split(file_path)
        (basename, _) = os.path.splitext(basename)

        return folder_path, basename
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422


class NanonisTranslator(NanonisTranslatorCorrect):

    def __init__(self, filepath, *args, **kwargs):
        """
        Instantiates the translator class

        Parameters
        ----------
        filepath : str
            Path to the input data file.
        args
        kwargs
        """
        super(NanonisTranslator, self).__init__(*args, **kwargs)
        warn(
            'In the future, you will need to pass the file path to the "translate()" function instead of here',
            FutureWarning)
        self.data_path = filepath

    def translate(self, data_channels=None, verbose=False):
        """
        Translate the data into a Pycroscopy compatible HDF5 file.

        Parameters
        ----------
        filepath : str
            Path to the input data file.
        data_channels : (optional) list of str
            Names of channels that will be read and stored in the file.
            If not given, all channels in the file will be used.
        verbose : (optional) Boolean
            Whether or not to print statements

        Returns
        -------
        h5_path : str
            Filepath to the output HDF5 file.

        """
        return super(NanonisTranslator, self).translate(self.data_path,
                                                        data_channels=data_channels,
                                                        verbose=verbose)