surfacetopography 0.91.2__py3.6-linux-x86_64.egg
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- EGG-INFO/PKG-INFO +11 -0
- EGG-INFO/SOURCES.txt +96 -0
- EGG-INFO/dependency_links.txt +1 -0
- EGG-INFO/native_libs.txt +1 -0
- EGG-INFO/not-zip-safe +1 -0
- EGG-INFO/requires.txt +6 -0
- EGG-INFO/top_level.txt +3 -0
- SurfaceTopography/ChangeLog.md +358 -0
- SurfaceTopography/Converters.py +197 -0
- SurfaceTopography/Generation.py +440 -0
- SurfaceTopography/HeightContainer.py +315 -0
- SurfaceTopography/IO/DI.py +261 -0
- SurfaceTopography/IO/FromFile.py +802 -0
- SurfaceTopography/IO/H5.py +79 -0
- SurfaceTopography/IO/IBW.py +176 -0
- SurfaceTopography/IO/MI.py +307 -0
- SurfaceTopography/IO/Matlab.py +106 -0
- SurfaceTopography/IO/NC.py +228 -0
- SurfaceTopography/IO/NPY.py +139 -0
- SurfaceTopography/IO/OPDx.py +885 -0
- SurfaceTopography/IO/Reader.py +389 -0
- SurfaceTopography/IO/__init__.py +241 -0
- SurfaceTopography/IO/__pycache__/DI.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/FromFile.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/H5.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/IBW.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/MI.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/Matlab.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/NC.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/NPY.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/OPDx.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/Reader.cpython-36.pyc +0 -0
- SurfaceTopography/IO/__pycache__/__init__.cpython-36.pyc +0 -0
- SurfaceTopography/Interpolation.py +30 -0
- SurfaceTopography/Nonuniform/Autocorrelation.py +161 -0
- SurfaceTopography/Nonuniform/Detrending.py +87 -0
- SurfaceTopography/Nonuniform/PowerSpectrum.py +178 -0
- SurfaceTopography/Nonuniform/ScalarParameters.py +140 -0
- SurfaceTopography/Nonuniform/VariableBandwidth.py +155 -0
- SurfaceTopography/Nonuniform/__init__.py +28 -0
- SurfaceTopography/Nonuniform/__pycache__/Autocorrelation.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/__pycache__/Detrending.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/__pycache__/PowerSpectrum.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/__pycache__/ScalarParameters.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/__pycache__/VariableBandwidth.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/__pycache__/__init__.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/__pycache__/common.cpython-36.pyc +0 -0
- SurfaceTopography/Nonuniform/common.py +95 -0
- SurfaceTopography/NonuniformLineScan.py +310 -0
- SurfaceTopography/Special.py +225 -0
- SurfaceTopography/Tools/__init__.py +28 -0
- SurfaceTopography/Tools/__pycache__/__init__.cpython-36.pyc +0 -0
- SurfaceTopography/Tools/__pycache__/common.cpython-36.pyc +0 -0
- SurfaceTopography/Tools/common.py +75 -0
- SurfaceTopography/Uniform/Autocorrelation.py +211 -0
- SurfaceTopography/Uniform/Detrending.py +155 -0
- SurfaceTopography/Uniform/Filtering.py +226 -0
- SurfaceTopography/Uniform/Interpolation.py +159 -0
- SurfaceTopography/Uniform/PowerSpectrum.py +197 -0
- SurfaceTopography/Uniform/ScalarParameters.py +173 -0
- SurfaceTopography/Uniform/VariableBandwidth.py +169 -0
- SurfaceTopography/Uniform/__init__.py +28 -0
- SurfaceTopography/Uniform/__pycache__/Autocorrelation.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/Detrending.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/Filtering.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/Interpolation.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/PowerSpectrum.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/ScalarParameters.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/VariableBandwidth.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/__init__.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/__pycache__/common.cpython-36.pyc +0 -0
- SurfaceTopography/Uniform/common.py +260 -0
- SurfaceTopography/UniformLineScanAndTopography.py +887 -0
- SurfaceTopography/__init__.py +62 -0
- SurfaceTopography/__pycache__/Converters.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/Generation.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/HeightContainer.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/Interpolation.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/NonuniformLineScan.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/Special.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/UniformLineScanAndTopography.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/__init__.cpython-36.pyc +0 -0
- SurfaceTopography/__pycache__/common.cpython-36.pyc +0 -0
- SurfaceTopography/common.py +111 -0
- _SurfaceTopography.cpython-36m-x86_64-linux-gnu.so +0 -0
- _SurfaceTopography.py +8 -0
- __pycache__/_SurfaceTopography.cpython-36.pyc +0 -0
- test/IO/__init__.py +28 -0
- test/IO/__pycache__/__init__.cpython-36.pyc +0 -0
- test/IO/__pycache__/test_ibw.cpython-36.pyc +0 -0
- test/IO/__pycache__/test_io.cpython-36.pyc +0 -0
- test/IO/__pycache__/test_mi.cpython-36.pyc +0 -0
- test/IO/__pycache__/test_netcdf.cpython-36.pyc +0 -0
- test/IO/__pycache__/test_npy.cpython-36.pyc +0 -0
- test/IO/__pycache__/test_opdx.cpython-36.pyc +0 -0
- test/IO/test_ibw.py +178 -0
- test/IO/test_io.py +401 -0
- test/IO/test_mi.py +103 -0
- test/IO/test_netcdf.py +134 -0
- test/IO/test_npy.py +135 -0
- test/IO/test_opdx.py +441 -0
- test/__init__.py +26 -0
- test/__pycache__/__init__.cpython-36.pyc +0 -0
- test/__pycache__/compare-orientation-with-gwyddion.cpython-36.pyc +0 -0
- test/__pycache__/conftest.cpython-36.pyc +0 -0
- test/__pycache__/test_autocorrelation.cpython-36.pyc +0 -0
- test/__pycache__/test_bicubic.cpython-36.pyc +0 -0
- test/__pycache__/test_filtering.cpython-36.pyc +0 -0
- test/__pycache__/test_generation.cpython-36.pyc +0 -0
- test/__pycache__/test_import.cpython-36.pyc +0 -0
- test/__pycache__/test_make_sphere.cpython-36.pyc +0 -0
- test/__pycache__/test_power_spectrum.cpython-36.pyc +0 -0
- test/__pycache__/test_scalar_parameters.cpython-36.pyc +0 -0
- test/__pycache__/test_topography1.cpython-36.pyc +0 -0
- test/__pycache__/test_topography2.cpython-36.pyc +0 -0
- test/__pycache__/test_variable_bandwidth.cpython-36.pyc +0 -0
- test/compare-orientation-with-gwyddion.py +102 -0
- test/conftest.py +80 -0
- test/test_autocorrelation.py +311 -0
- test/test_bicubic.py +190 -0
- test/test_filtering.py +69 -0
- test/test_generation.py +259 -0
- test/test_import.py +54 -0
- test/test_make_sphere.py +108 -0
- test/test_power_spectrum.py +259 -0
- test/test_scalar_parameters.py +176 -0
- test/test_topography1.py +1287 -0
- test/test_topography2.py +328 -0
- test/test_variable_bandwidth.py +107 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2019-2020 Lars Pastewka
|
|
3
|
+
# 2019 Antoine Sanner
|
|
4
|
+
#
|
|
5
|
+
# ### MIT license
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
# SOFTWARE.
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
from .. import Topography
|
|
27
|
+
from .Reader import ReaderBase, ChannelInfo
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class H5Reader(ReaderBase):
|
|
31
|
+
_format = 'h5'
|
|
32
|
+
_name = 'Hierarchical data format (HDF5)'
|
|
33
|
+
_description = '''
|
|
34
|
+
Import filter for [HDF5](https://support.hdfgroup.org/HDF5/) files provided
|
|
35
|
+
within the contact mechanics challenge. The reader looks for a two-dimensional
|
|
36
|
+
array named `surface`. HDF5 files do not store units or physical sizes. These
|
|
37
|
+
need to be manually provided by the user.
|
|
38
|
+
|
|
39
|
+
The original contact mechanics challenge data can be downloaded
|
|
40
|
+
[here](https://www.lmp.uni-saarland.de/index.php/research-topics/contact-mechanics-challenge-announcement/).
|
|
41
|
+
''' # noqa: E501
|
|
42
|
+
|
|
43
|
+
def __init__(self, fobj):
|
|
44
|
+
self._h5 = None
|
|
45
|
+
import h5py
|
|
46
|
+
self._h5 = h5py.File(fobj, 'r')
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
if self._h5 is not None:
|
|
50
|
+
self._h5.close()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def channels(self):
|
|
54
|
+
return [ChannelInfo(self,
|
|
55
|
+
0, # channel index
|
|
56
|
+
name='Default',
|
|
57
|
+
dim=len(self._h5['surface'].shape),
|
|
58
|
+
nb_grid_pts=self._h5['surface'].shape)]
|
|
59
|
+
|
|
60
|
+
def topography(self, channel_index=None, physical_sizes=None,
|
|
61
|
+
height_scale_factor=None, info={}, periodic=False,
|
|
62
|
+
subdomain_locations=None, nb_subdomain_grid_pts=None):
|
|
63
|
+
if subdomain_locations is not None or \
|
|
64
|
+
nb_subdomain_grid_pts is not None:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
'This reader does not support MPI parallelization.')
|
|
67
|
+
if channel_index is None:
|
|
68
|
+
channel_index = self._default_channel_index
|
|
69
|
+
if channel_index != 0:
|
|
70
|
+
raise RuntimeError('HDF5 reader only supports a single channel')
|
|
71
|
+
size = self._check_physical_sizes(physical_sizes)
|
|
72
|
+
t = Topography(self._h5['surface'][...], size, info=info,
|
|
73
|
+
periodic=periodic)
|
|
74
|
+
if height_scale_factor is not None:
|
|
75
|
+
t = t.scale(height_scale_factor)
|
|
76
|
+
return t
|
|
77
|
+
|
|
78
|
+
channels.__doc__ = ReaderBase.channels.__doc__
|
|
79
|
+
topography.__doc__ = ReaderBase.topography.__doc__
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2019-2020 Lars Pastewka
|
|
3
|
+
# 2019-2020 Michael Röttger
|
|
4
|
+
# 2019 Kai Haase
|
|
5
|
+
#
|
|
6
|
+
# ### MIT license
|
|
7
|
+
#
|
|
8
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
# in the Software without restriction, including without limitation the rights
|
|
11
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
# furnished to do so, subject to the following conditions:
|
|
14
|
+
#
|
|
15
|
+
# The above copyright notice and this permission notice shall be included in
|
|
16
|
+
# all copies or substantial portions of the Software.
|
|
17
|
+
#
|
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
# SOFTWARE.
|
|
25
|
+
#
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
from io import TextIOBase
|
|
29
|
+
|
|
30
|
+
from igor.binarywave import load as loadibw
|
|
31
|
+
|
|
32
|
+
from .. import Topography
|
|
33
|
+
from .Reader import ReaderBase, ChannelInfo
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class IBWReader(ReaderBase):
|
|
37
|
+
_format = 'ibw'
|
|
38
|
+
_name = 'Igor binary wave'
|
|
39
|
+
_description = '''
|
|
40
|
+
Igor binary wave is a container format of the
|
|
41
|
+
[Igor Pro](https://www.wavemetrics.com/products/igorpro/programming)
|
|
42
|
+
language. This format is used by AFMs from Asylum Research (now Oxford
|
|
43
|
+
Instruments) to store topography information. This format contains information
|
|
44
|
+
on the physical size of the topography map as well as its units.
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
# Reads in the positions of all the data and metadata
|
|
48
|
+
def __init__(self, file_path):
|
|
49
|
+
|
|
50
|
+
# depending from where this function is called, file_path might already
|
|
51
|
+
# be a filestream
|
|
52
|
+
already_open = False
|
|
53
|
+
if not hasattr(file_path, 'read'):
|
|
54
|
+
f = open(file_path, "rb")
|
|
55
|
+
else:
|
|
56
|
+
already_open = True
|
|
57
|
+
if isinstance(file_path, TextIOBase):
|
|
58
|
+
# file was opened without the 'b' option, so read its buffer to
|
|
59
|
+
# get the binary data
|
|
60
|
+
f = file_path.buffer
|
|
61
|
+
else:
|
|
62
|
+
f = file_path
|
|
63
|
+
|
|
64
|
+
# This catches and closes on its own
|
|
65
|
+
try:
|
|
66
|
+
file = loadibw(f)
|
|
67
|
+
except Exception:
|
|
68
|
+
if not f.closed:
|
|
69
|
+
if not already_open:
|
|
70
|
+
f.close()
|
|
71
|
+
raise RuntimeError('Invalid file format.')
|
|
72
|
+
|
|
73
|
+
if file['version'] != 5:
|
|
74
|
+
raise RuntimeError('Only IBW version 5 supported!')
|
|
75
|
+
|
|
76
|
+
self.data = file['wave']
|
|
77
|
+
|
|
78
|
+
# the first two labels are of x and y axis. we cannot read those
|
|
79
|
+
self._channel_names = [channel.decode() for channel in
|
|
80
|
+
self.data['labels'][2][1:]]
|
|
81
|
+
self._default_channel = 0
|
|
82
|
+
|
|
83
|
+
#
|
|
84
|
+
# Read physical sizes from file, since all data
|
|
85
|
+
# has already been loaded, we can calculate it here
|
|
86
|
+
#
|
|
87
|
+
height_data = self.data['wData']
|
|
88
|
+
|
|
89
|
+
# TODO is it always like this?
|
|
90
|
+
assert len(height_data.shape) == 3, \
|
|
91
|
+
"We expect all channels being coded in to one wave and all are " \
|
|
92
|
+
"2D. This is not true somehow, cannot proceed."
|
|
93
|
+
nx, ny, num_channels = height_data.shape
|
|
94
|
+
|
|
95
|
+
# ensure that there are not too many channel names
|
|
96
|
+
self._channel_names = self._channel_names[:num_channels]
|
|
97
|
+
|
|
98
|
+
# add channel names for all channels without name
|
|
99
|
+
no_name_idx = 1
|
|
100
|
+
while len(self._channel_names) < num_channels:
|
|
101
|
+
self._channel_names.append("no name ({})".format(no_name_idx))
|
|
102
|
+
no_name_idx += 1
|
|
103
|
+
|
|
104
|
+
#
|
|
105
|
+
# Decode units
|
|
106
|
+
#
|
|
107
|
+
def decode_unit_entry(u):
|
|
108
|
+
return u.tobytes().partition(b'\0')[0].decode('latin-1')
|
|
109
|
+
|
|
110
|
+
data_unit = decode_unit_entry(self.data['wave_header']['dataUnits'])
|
|
111
|
+
x_unit = decode_unit_entry(self.data['wave_header']['dimUnits'][0])
|
|
112
|
+
y_unit = decode_unit_entry(self.data['wave_header']['dimUnits'][1])
|
|
113
|
+
|
|
114
|
+
# the following is not necessary, we could handle different units by
|
|
115
|
+
# rescaling, however I'll leave it like this for now, print some
|
|
116
|
+
# message so we know what to do if a file occurs which does not fulfill
|
|
117
|
+
# this assumption
|
|
118
|
+
assert data_unit == x_unit == y_unit, \
|
|
119
|
+
"So far, data units and dimension units must be all the same. " +\
|
|
120
|
+
"data unit: '{}', x unit: '{}', y unit: '{}'".format(data_unit,
|
|
121
|
+
x_unit,
|
|
122
|
+
y_unit)
|
|
123
|
+
|
|
124
|
+
self._data_unit = data_unit
|
|
125
|
+
#
|
|
126
|
+
# Decode sizes
|
|
127
|
+
#
|
|
128
|
+
sfA = self.data['wave_header']['sfA']
|
|
129
|
+
self._physical_sizes = (nx * sfA[0], ny * sfA[1])
|
|
130
|
+
# Comment in C header file on these fields: Index value for element e
|
|
131
|
+
# of dimension d = sfA[d]*e + sfB[d]. sfB is left out here, because we
|
|
132
|
+
# are interested in the width and height, not the absolute offsets.
|
|
133
|
+
|
|
134
|
+
#
|
|
135
|
+
# Build channel information
|
|
136
|
+
#
|
|
137
|
+
self._channels = [
|
|
138
|
+
ChannelInfo(self, i, name=cn, dim=2, nb_grid_pts=(nx, ny),
|
|
139
|
+
physical_sizes=self._physical_sizes)
|
|
140
|
+
for i, cn in enumerate(self._channel_names)]
|
|
141
|
+
|
|
142
|
+
# Shall we use the channel names in order to assign a unit as Gwyddion
|
|
143
|
+
# does?
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def channels(self):
|
|
147
|
+
return self._channels
|
|
148
|
+
|
|
149
|
+
def topography(self, channel_index=None, physical_sizes=None,
|
|
150
|
+
height_scale_factor=None, info={},
|
|
151
|
+
periodic=False, subdomain_locations=None,
|
|
152
|
+
nb_subdomain_grid_pts=None):
|
|
153
|
+
if channel_index is None:
|
|
154
|
+
channel_index = self._default_channel_index
|
|
155
|
+
|
|
156
|
+
if subdomain_locations is not None or \
|
|
157
|
+
nb_subdomain_grid_pts is not None:
|
|
158
|
+
raise RuntimeError(
|
|
159
|
+
'This reader does not support MPI parallelization.')
|
|
160
|
+
|
|
161
|
+
height_data = self.data['wData']
|
|
162
|
+
height_data = np.fliplr(height_data[:, :, channel_index].copy())
|
|
163
|
+
|
|
164
|
+
if physical_sizes is None:
|
|
165
|
+
physical_sizes = self._physical_sizes
|
|
166
|
+
|
|
167
|
+
topo = Topography(height_data, physical_sizes,
|
|
168
|
+
info=info,
|
|
169
|
+
periodic=periodic)
|
|
170
|
+
# we could pass the data units here, but they dont seem to be always
|
|
171
|
+
# correct for all channels?!
|
|
172
|
+
|
|
173
|
+
if height_scale_factor is not None:
|
|
174
|
+
topo = topo.scale(height_scale_factor)
|
|
175
|
+
|
|
176
|
+
return topo
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2019-2020 Lars Pastewka
|
|
3
|
+
# 2020 Michael Röttger
|
|
4
|
+
# 2019-2020 Kai Haase
|
|
5
|
+
# 2019 Antoine Sanner
|
|
6
|
+
#
|
|
7
|
+
# ### MIT license
|
|
8
|
+
#
|
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
# in the Software without restriction, including without limitation the rights
|
|
12
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
# furnished to do so, subject to the following conditions:
|
|
15
|
+
#
|
|
16
|
+
# The above copyright notice and this permission notice shall be included in
|
|
17
|
+
# all copies or substantial portions of the Software.
|
|
18
|
+
#
|
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
# SOFTWARE.
|
|
26
|
+
#
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from .. import Topography
|
|
31
|
+
from .Reader import ReaderBase, CorruptFile, ChannelInfo
|
|
32
|
+
from .FromFile import mangle_height_unit
|
|
33
|
+
from io import TextIOBase
|
|
34
|
+
|
|
35
|
+
image_head = b'fileType Image\n'
|
|
36
|
+
spec_head = b'fileType Spectroscopy\n'
|
|
37
|
+
|
|
38
|
+
magic_data = b'data \n'
|
|
39
|
+
magic_data_binary = b'data BINARY\n'
|
|
40
|
+
magic_data_binary32 = b'data BINARY_32\n'
|
|
41
|
+
magic_data_ascii = b'data ASCII'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MIReader(ReaderBase):
|
|
45
|
+
_format = 'mi'
|
|
46
|
+
_name = 'Molecular imaging data file'
|
|
47
|
+
_description = '''
|
|
48
|
+
This reader opens Agilent Technologies (Molecular Imaging) AFM files saved
|
|
49
|
+
in the MI format. This format contains information on the physical size of the
|
|
50
|
+
topography map as well as its units.
|
|
51
|
+
'''
|
|
52
|
+
|
|
53
|
+
# Reads in the positions of all the data and metadata
|
|
54
|
+
def __init__(self, file_path):
|
|
55
|
+
self.file_path = file_path
|
|
56
|
+
|
|
57
|
+
# Start of the data in the file
|
|
58
|
+
self.data_start = None
|
|
59
|
+
# If image oder spectroscopy
|
|
60
|
+
self.is_image = None
|
|
61
|
+
# Format of the saved data
|
|
62
|
+
self.data_type = None
|
|
63
|
+
|
|
64
|
+
self.mifile = None
|
|
65
|
+
|
|
66
|
+
# Process the header and read in metadata
|
|
67
|
+
self.process_header()
|
|
68
|
+
|
|
69
|
+
def process_header(self):
|
|
70
|
+
|
|
71
|
+
already_open = False
|
|
72
|
+
if not hasattr(self.file_path, 'read'):
|
|
73
|
+
f = open(self.file_path, "rb")
|
|
74
|
+
else:
|
|
75
|
+
already_open = True
|
|
76
|
+
if isinstance(self.file_path, TextIOBase):
|
|
77
|
+
# file was opened without the 'b' option, so read its buffer
|
|
78
|
+
# to get the binary data
|
|
79
|
+
f = self.file_path.buffer
|
|
80
|
+
else:
|
|
81
|
+
f = self.file_path
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
self.lines = f.readlines()
|
|
85
|
+
|
|
86
|
+
# Find out if image or spectroscopy
|
|
87
|
+
if self.lines[0] == image_head:
|
|
88
|
+
self.is_image = True
|
|
89
|
+
elif self.lines[0] == spec_head:
|
|
90
|
+
self.is_image = False
|
|
91
|
+
else:
|
|
92
|
+
raise CorruptFile
|
|
93
|
+
|
|
94
|
+
# Find the start of the height data and denote data type
|
|
95
|
+
if magic_data in self.lines:
|
|
96
|
+
header_size = self.lines.index(magic_data)
|
|
97
|
+
self.data_type = 'text'
|
|
98
|
+
elif magic_data_binary in self.lines:
|
|
99
|
+
header_size = self.lines.index(magic_data_binary)
|
|
100
|
+
self.data_type = 'binary'
|
|
101
|
+
elif magic_data_binary32 in self.lines:
|
|
102
|
+
header_size = self.lines.index(magic_data_binary32)
|
|
103
|
+
self.data_type = 'binary32'
|
|
104
|
+
elif magic_data_ascii in self.lines:
|
|
105
|
+
header_size = self.lines.index(magic_data_ascii)
|
|
106
|
+
self.data_type = 'ascii'
|
|
107
|
+
else:
|
|
108
|
+
raise CorruptFile
|
|
109
|
+
|
|
110
|
+
# Save start of data for later reading of the matrix
|
|
111
|
+
self.data_start = header_size + 1
|
|
112
|
+
|
|
113
|
+
# Create mifile from header, reading out meta data and channel info
|
|
114
|
+
if self.is_image:
|
|
115
|
+
self.mifile = read_header_image(self.lines[1:header_size])
|
|
116
|
+
else: # TODO
|
|
117
|
+
self.mifile = read_header_spect(self.lines[1:header_size])
|
|
118
|
+
|
|
119
|
+
# Reformat the metadata
|
|
120
|
+
for buf in self.mifile.channels:
|
|
121
|
+
buf.meta['name'] = buf.name
|
|
122
|
+
buf.meta['unit'] = mangle_height_unit(
|
|
123
|
+
buf.meta.pop('bufferUnit'))
|
|
124
|
+
buf.meta['range'] = buf.meta.pop('bufferRange')
|
|
125
|
+
buf.meta['label'] = buf.meta.pop('bufferLabel')
|
|
126
|
+
|
|
127
|
+
self._physical_sizes = float(self.mifile.meta['xLength']), \
|
|
128
|
+
float(self.mifile.meta['yLength'])
|
|
129
|
+
self._nb_grid_pts = int(self.mifile.meta['xPixels']), \
|
|
130
|
+
int(self.mifile.meta['yPixels'])
|
|
131
|
+
|
|
132
|
+
finally:
|
|
133
|
+
if not already_open:
|
|
134
|
+
f.close()
|
|
135
|
+
|
|
136
|
+
def topography(self, channel_index=None, physical_sizes=None,
|
|
137
|
+
height_scale_factor=None, info={}, periodic=False,
|
|
138
|
+
subdomain_locations=None, nb_subdomain_grid_pts=None):
|
|
139
|
+
if channel_index is None:
|
|
140
|
+
channel_index = self._default_channel_index
|
|
141
|
+
|
|
142
|
+
if subdomain_locations is not None or \
|
|
143
|
+
nb_subdomain_grid_pts is not None:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
'This reader does not support MPI parallelization.')
|
|
146
|
+
|
|
147
|
+
output_channel = self.mifile.channels[channel_index]
|
|
148
|
+
|
|
149
|
+
buffer = b''.join(self.lines[self.data_start:])
|
|
150
|
+
buffer = [chr(_) for _ in buffer]
|
|
151
|
+
|
|
152
|
+
# Read height data
|
|
153
|
+
if self.is_image:
|
|
154
|
+
if self.data_type == 'binary':
|
|
155
|
+
dt = "i2"
|
|
156
|
+
encode_length = 2
|
|
157
|
+
type_range = 32768
|
|
158
|
+
elif self.data_type == 'binary32':
|
|
159
|
+
dt = "i4"
|
|
160
|
+
encode_length = 4
|
|
161
|
+
type_range = 2147483648
|
|
162
|
+
else: # text or ascii
|
|
163
|
+
type_range = 32768
|
|
164
|
+
|
|
165
|
+
start = int(self.mifile.xres) * int(
|
|
166
|
+
self.mifile.yres) * encode_length * channel_index
|
|
167
|
+
end = int(self.mifile.xres) * int(
|
|
168
|
+
self.mifile.yres) * encode_length * (channel_index + 1)
|
|
169
|
+
|
|
170
|
+
data = ''.join(buffer[start:end])
|
|
171
|
+
out = np.frombuffer(str.encode(data, "raw_unicode_escape"), dt)
|
|
172
|
+
out = out.reshape((int(self.mifile.xres), int(self.mifile.yres)))
|
|
173
|
+
|
|
174
|
+
# Undo normalizing with range of data type
|
|
175
|
+
out = out / type_range
|
|
176
|
+
|
|
177
|
+
# If scan direction is upwards, flip the height map vertically
|
|
178
|
+
if self.mifile.meta['scanUp']:
|
|
179
|
+
out = np.flip(out, 0)
|
|
180
|
+
|
|
181
|
+
# Multiply the heights with the bufferRange
|
|
182
|
+
out *= float(output_channel.meta['range'])
|
|
183
|
+
else:
|
|
184
|
+
pass # TODO
|
|
185
|
+
|
|
186
|
+
joined_meta = {**self.mifile.meta, **output_channel.meta}
|
|
187
|
+
|
|
188
|
+
# Initialize heights with transposed array in order to match Gwdyydion
|
|
189
|
+
# when plotted with pcolormesh(t.heights().T), except that the y axis
|
|
190
|
+
# is flipped because the origin is in lower left with pcolormesh;
|
|
191
|
+
# imshow(t.heights().T) shows the image like gwyddion
|
|
192
|
+
t = Topography(heights=out.T,
|
|
193
|
+
physical_sizes=self._check_physical_sizes(
|
|
194
|
+
physical_sizes, self._physical_sizes),
|
|
195
|
+
info=joined_meta, periodic=periodic)
|
|
196
|
+
if height_scale_factor is not None:
|
|
197
|
+
t.scale(height_scale_factor)
|
|
198
|
+
return t
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def channels(self):
|
|
202
|
+
return [ChannelInfo(self, i, name=channel.meta['name'],
|
|
203
|
+
dim=len(self._nb_grid_pts),
|
|
204
|
+
nb_grid_pts=self._nb_grid_pts,
|
|
205
|
+
physical_sizes=self._physical_sizes,
|
|
206
|
+
info=channel.meta)
|
|
207
|
+
for i, channel in enumerate(self.mifile.channels)]
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def info(self):
|
|
211
|
+
""" Return all the available metadata as a dict. """
|
|
212
|
+
return self.mifile.meta
|
|
213
|
+
|
|
214
|
+
channels.__doc__ = ReaderBase.channels.__doc__
|
|
215
|
+
topography.__doc__ = ReaderBase.topography.__doc__
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def read_header_image(header):
|
|
219
|
+
"""
|
|
220
|
+
Reads in global metadata and information about about included channels as
|
|
221
|
+
well as their metadata.
|
|
222
|
+
:param header: The header as a line of text.
|
|
223
|
+
:return:
|
|
224
|
+
MIFile item containing the metadata, with the channels as a list of
|
|
225
|
+
'buffers'.
|
|
226
|
+
"""
|
|
227
|
+
# This object will store the file-wide metadata
|
|
228
|
+
mifile = MIFile()
|
|
229
|
+
|
|
230
|
+
# False while reading global metadata, gets true if we start to read in the
|
|
231
|
+
# channel info
|
|
232
|
+
reading_buffers = False
|
|
233
|
+
|
|
234
|
+
for line in header:
|
|
235
|
+
line = line.decode("utf-8")
|
|
236
|
+
|
|
237
|
+
# As soon as we see a line starting with 'bufferLabel', we know we are
|
|
238
|
+
# now reading in channels
|
|
239
|
+
if line.startswith('bufferLabel'):
|
|
240
|
+
# Create a new channel with the id as name
|
|
241
|
+
channel = Channel(name=str.strip(line[14:]))
|
|
242
|
+
mifile.channels.append(channel)
|
|
243
|
+
reading_buffers = True
|
|
244
|
+
|
|
245
|
+
if line[0] == ' ':
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# For all key value pairs in the file:
|
|
249
|
+
# Append to global metadata oder channel metadata, depending on our
|
|
250
|
+
# state
|
|
251
|
+
key = str.strip(line[:14])
|
|
252
|
+
value = str.strip(line[14:])
|
|
253
|
+
|
|
254
|
+
if reading_buffers:
|
|
255
|
+
mifile.channels[-1].meta[key] = value
|
|
256
|
+
else:
|
|
257
|
+
mifile.meta[key] = value
|
|
258
|
+
|
|
259
|
+
# Catch 'special' metadata
|
|
260
|
+
if key == "xPixels":
|
|
261
|
+
mifile.xres = value
|
|
262
|
+
elif key == "yPixels":
|
|
263
|
+
mifile.yres = value
|
|
264
|
+
return mifile
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def read_header_spect(header):
|
|
268
|
+
"""
|
|
269
|
+
Reads in metadata out of the header.
|
|
270
|
+
:param header: The header.
|
|
271
|
+
:return:
|
|
272
|
+
MIFile item containing the metadata.
|
|
273
|
+
"""
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class Channel:
|
|
278
|
+
"""
|
|
279
|
+
Class structure for a channel contained in the file.
|
|
280
|
+
Has a name and metadata (height data is not needed since it is returned
|
|
281
|
+
directly).
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def __init__(self, name=None, meta=None):
|
|
285
|
+
if meta is None:
|
|
286
|
+
meta = dict()
|
|
287
|
+
|
|
288
|
+
self.name = name
|
|
289
|
+
self.meta = meta
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class MIFile:
|
|
293
|
+
"""
|
|
294
|
+
Class structure for the while file. Has a list of channels, global metadata
|
|
295
|
+
and a nb_grid_pts.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(self, res=(0, 0), channels=None, meta=None):
|
|
299
|
+
if channels is None:
|
|
300
|
+
channels = list()
|
|
301
|
+
if meta is None:
|
|
302
|
+
meta = dict()
|
|
303
|
+
|
|
304
|
+
self.xres = res[0]
|
|
305
|
+
self.yres = res[1]
|
|
306
|
+
self.channels = channels
|
|
307
|
+
self.meta = meta
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2019-2020 Lars Pastewka
|
|
3
|
+
# 2019 Antoine Sanner
|
|
4
|
+
#
|
|
5
|
+
# ### MIT license
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
# SOFTWARE.
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
from .. import Topography
|
|
27
|
+
from .Reader import ReaderBase, ChannelInfo
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MatReader(ReaderBase):
|
|
31
|
+
_format = 'mat'
|
|
32
|
+
_name = 'MATLAB'
|
|
33
|
+
_description = '''
|
|
34
|
+
Imports topography data stored in MATLAB workspace files. The reader
|
|
35
|
+
automatically extracts all 2D arrays stored in the file and interprets those
|
|
36
|
+
as height information. Matlab files do not store units or physical sizes.
|
|
37
|
+
These need to be manually provided by the user.
|
|
38
|
+
'''
|
|
39
|
+
|
|
40
|
+
def __init__(self, fobj):
|
|
41
|
+
"""
|
|
42
|
+
Reads a surface profile from a Matlab file and presents in in a
|
|
43
|
+
SurfaceTopography-conformant manner.
|
|
44
|
+
|
|
45
|
+
All two-dimensional arrays present in the matlab data file are
|
|
46
|
+
returned.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
fobj: filename or file object
|
|
51
|
+
File to read.
|
|
52
|
+
"""
|
|
53
|
+
from scipy.io import loadmat
|
|
54
|
+
|
|
55
|
+
close_file = False
|
|
56
|
+
if not hasattr(fobj, 'read'):
|
|
57
|
+
fobj = open(fobj, 'rb')
|
|
58
|
+
close_file = True
|
|
59
|
+
try:
|
|
60
|
+
data = loadmat(fobj)
|
|
61
|
+
self._channels = []
|
|
62
|
+
self._height_data = []
|
|
63
|
+
for key, value in data.items():
|
|
64
|
+
is_2darray = False
|
|
65
|
+
try:
|
|
66
|
+
nx, ny = value.shape
|
|
67
|
+
is_2darray = True
|
|
68
|
+
except (AttributeError, ValueError):
|
|
69
|
+
pass
|
|
70
|
+
if is_2darray:
|
|
71
|
+
channelinfo = ChannelInfo(self,
|
|
72
|
+
len(self._channels),
|
|
73
|
+
name=key,
|
|
74
|
+
dim=len(value.shape),
|
|
75
|
+
nb_grid_pts=value.shape)
|
|
76
|
+
|
|
77
|
+
self._channels.append(channelinfo)
|
|
78
|
+
self._height_data.append(value)
|
|
79
|
+
finally:
|
|
80
|
+
if close_file:
|
|
81
|
+
fobj.close()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def channels(self):
|
|
85
|
+
return self._channels
|
|
86
|
+
|
|
87
|
+
def topography(self, channel_index=None, physical_sizes=None,
|
|
88
|
+
height_scale_factor=None, info={}, periodic=False,
|
|
89
|
+
subdomain_locations=None, nb_subdomain_grid_pts=None):
|
|
90
|
+
if channel_index is None:
|
|
91
|
+
channel_index = self._default_channel_index
|
|
92
|
+
|
|
93
|
+
if subdomain_locations is not None or \
|
|
94
|
+
nb_subdomain_grid_pts is not None:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
'This reader does not support MPI parallelization.')
|
|
97
|
+
|
|
98
|
+
return Topography(self._height_data[channel_index],
|
|
99
|
+
physical_sizes=self._check_physical_sizes(
|
|
100
|
+
physical_sizes),
|
|
101
|
+
info=dict(
|
|
102
|
+
data_source=self.channels[channel_index].name),
|
|
103
|
+
periodic=periodic)
|
|
104
|
+
|
|
105
|
+
channels.__doc__ = ReaderBase.channels.__doc__
|
|
106
|
+
topography.__doc__ = ReaderBase.topography.__doc__
|