Skip to content

Commit c0f3ed2

Browse files
authored
Merge pull request Quasars#30 from ngergihun/fixes
Fixes: More robust NeaSpectralReader Fixed ProcessAllPoints for interferograms Additional test for multipoint interferograms Better code logic in Tools class
2 parents 8ae9d3f + b9c093d commit c0f3ed2

File tree

6 files changed

+143
-78
lines changed

6 files changed

+143
-78
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# www.neaspec.com
2+
# Scan:   Fourier Scan
3+
# Project:   nanoFTIR-QualityControl
4+
# Description:   NeaSpec-Slow-Arrowhead-Clean-REV2-45avg
5+
# Date:   05/19/2024 16:59:10
6+
# Scanner Center Position (X, Y): [µm] 46.77 49.79  
7+
# Rotation: [°] 0    
8+
# Scan Area (X, Y, Z): [µm] 0.000 0.000 0.000
9+
# Pixel Area (X, Y, Z): [px] 2 1 3
10+
# Interferometer Center/Distance: [µm] 470.000 490.000  
11+
# Averaging:   2    
12+
# Integration time: [ms] 20    
13+
# Wavenumber Scaling:   1.003656    
14+
# Laser Source:  
15+
# Detector:   R
16+
# Target Wavelength: [µm]    
17+
# Demodulation Mode:   Fourier
18+
# Tip Frequency: [Hz] 68,263.500    
19+
# Tip Amplitude: [mV] 339.702    
20+
# Tapping Amplitude: [nm] 76.233    
21+
# Modulation Frequency: [Hz] 0.000    
22+
# Modulation Amplitude: [mV] 0.000    
23+
# Modulation Offset: [mV] 0.000    
24+
# Setpoint: [%] 80.49    
25+
# Regulator (P, I, D):   3.767854 6.228756 1.000000
26+
# Tip Potential: [mV] 0.000    
27+
# M1A Scaling: [nm/V] 1.520    
28+
# M1A Cantilever Factor:   2.066    
29+
# Q-Factor:   221.1    
30+
# Version:   2.1.11508.0
31+
Row Column Run Depth Z M O0A O0P O1A O1P O2A O2P O3A O3P O4A O4P O5A O5P
32+
0 0 0 0 1.4195246E-06 0.0002253487 233.41681 0 14.466169 2.4147859 9.580825 -1.4510685 3.1200895 0.95955217 1.3408793 -2.8969262 0.6398502 -0.53890413
33+
0 0 0 1 1.4195394E-06 0.00022605936 225.92825 0 14.5146475 2.4173899 9.592879 -1.4507022 3.1344879 0.9692281 1.3316284 -2.8742592 0.56832266 -0.47886258
34+
0 0 0 2 1.4195472E-06 0.0002265469 222.50809 0 14.548334 2.4175787 9.569336 -1.4517778 3.0889504 0.96725243 1.3630728 -2.9196901 0.64972746 -0.54219437
35+
0 0 1 0 1.4195246E-06 0.0002253487 233.41681 0 14.466169 2.4147859 9.580825 -1.4510685 3.1200895 0.95955217 1.3408793 -2.8969262 0.6398502 -0.53890413
36+
0 0 1 1 1.4195394E-06 0.00022605936 225.92825 0 14.5146475 2.4173899 9.592879 -1.4507022 3.1344879 0.9692281 1.3316284 -2.8742592 0.56832266 -0.47886258
37+
0 0 1 2 1.4195472E-06 0.0002265469 222.50809 0 14.548334 2.4175787 9.569336 -1.4517778 3.0889504 0.96725243 1.3630728 -2.9196901 0.64972746 -0.54219437
38+
0 1 0 0 1.4195246E-06 0.0002253487 233.41681 0 14.466169 2.4147859 9.580825 -1.4510685 3.1200895 0.95955217 1.3408793 -2.8969262 0.6398502 -0.53890413
39+
0 1 0 1 1.4195394E-06 0.00022605936 225.92825 0 14.5146475 2.4173899 9.592879 -1.4507022 3.1344879 0.9692281 1.3316284 -2.8742592 0.56832266 -0.47886258
40+
0 1 0 2 1.4195472E-06 0.0002265469 222.50809 0 14.548334 2.4175787 9.569336 -1.4517778 3.0889504 0.96725243 1.3630728 -2.9196901 0.64972746 -0.54219437
41+
0 1 1 0 1.4195246E-06 0.0002253487 233.41681 0 14.466169 2.4147859 9.580825 -1.4510685 3.1200895 0.95955217 1.3408793 -2.8969262 0.6398502 -0.53890413
42+
0 1 1 1 1.4195394E-06 0.00022605936 225.92825 0 14.5146475 2.4173899 9.592879 -1.4507022 3.1344879 0.9692281 1.3316284 -2.8742592 0.56832266 -0.47886258
43+
0 1 1 2 1.4195472E-06 0.0002265469 222.50809 0 14.548334 2.4175787 9.569336 -1.4517778 3.0889504 0.96725243 1.3630728 -2.9196901 0.64972746 -0.54219437

pySNOM/images.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
class Measurement:
1717
def __init__(self, data, filename=None, info=None, mode="None"):
1818
self.filename = filename
19-
self.mode = (
20-
mode
21-
)
19+
self.mode = mode
2220
self._data = data
2321
self.info = info
2422

pySNOM/interferograms.py

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from scipy.fft import fft, fftshift
66
from scipy.interpolate import CubicSpline, interp1d
77
from pySNOM.spectra import NeaSpectrum, SingleChannelSpectrum
8+
import re
89

910
MeasurementModes = Enum("MeasurementModes", ["None", "nanoFTIR"])
1011
DataTypes = Enum("DataTypes", ["Amplitude", "Phase", "Topography"])
@@ -32,6 +33,20 @@ def data(self, value):
3233
"""Data setter to reshape properly"""
3334
self._data = Tools.reshape_ifg_data(value, self._parameters)
3435

36+
def add_channel(self, values, channelname):
37+
"""Adds a new channel to data dictionary"""
38+
if channelname not in list(self._data.keys()):
39+
self._data[channelname] = np.reshape(
40+
values,
41+
(
42+
int(self.parameters["PixelArea"][0]),
43+
int(self.parameters["PixelArea"][1]),
44+
int(self.parameters["PixelArea"][2] * self.parameters["Averaging"]),
45+
),
46+
)
47+
else:
48+
raise ValueError
49+
3550

3651
# TRANSFORMATIONS ------------------------------------------------------------------------------------------------------------------
3752
class Transformation:
@@ -64,7 +79,6 @@ def transform(self, ifg, maxis):
6479
stepsizes = np.mean(np.diff(maxis * 1e6))
6580
Fs = 1 / np.mean(stepsizes)
6681
faxis = (Fs / 2) * np.linspace(-1, 1, len(complex_spectrum)) * 10000 / 2
67-
6882
return (
6983
complex_spectrum[int(len(faxis) / 2) - 1 : -1],
7084
faxis[int(len(faxis) / 2) - 1 : -1],
@@ -319,37 +333,57 @@ def transform(self, neaifg):
319333
pointifg_data = dict()
320334
pointifg_params = dict()
321335
pointifg_params["PixelArea"] = [1, 1, neaifg.parameters["PixelArea"][2]]
336+
pointifg_params["Scan"] = "Fourier Scan"
337+
pointifg_params["Averaging"] = neaifg.parameters["Averaging"]
322338

323339
spectra = NeaSpectrum({}, {}, scantype=neaifg.scantype)
324340

325-
for order in range(6):
341+
allchannels = list(neaifg.data.keys())
342+
optical_channels = [
343+
name
344+
for name in allchannels
345+
if re.match("O(.?)A", name) or re.match("O(.?)P", name)
346+
]
347+
orders = [int(n) for c in optical_channels for n in re.findall(r"\d", c)]
348+
orders = np.unique(np.asarray(orders))
349+
350+
for order in orders:
326351
channelA = f"O{order}A"
327352
channelP = f"O{order}P"
328-
for i in range(pixel_area[0]):
329-
for k in range(pixel_area[1]):
330-
pointifg_data[channelA] = neaifg.data[channelA][i, k, :]
331-
pointifg_data[channelP] = neaifg.data[channelP][i, k, :]
332-
pointifg_data["M"] = neaifg.data["M"][i, k, :]
333-
334-
(
335-
ampFullData[i, k, :],
336-
phiFullData[i, k, :],
337-
fFullData[i, k, :],
338-
) = ProcessSingleChannel(
339-
order,
340-
method=self.method,
341-
apod=self.apod,
342-
windowtype=self.windowtype,
343-
nzeros=self.nzeros,
344-
interpmethod=self.interpmethod,
345-
simpleoutput=True,
346-
).transform(
347-
neaifg
348-
)
349-
350-
spectra.data[channelA] = ampFullData
351-
spectra.data[channelP] = phiFullData
352-
spectra.data["Wavenumber"] = fFullData
353+
354+
if channelA not in list(neaifg.data.keys()) or channelP not in list(
355+
neaifg.data.keys()
356+
):
357+
print(
358+
f"Skipped processing for order: {order}, since A or P is missing!"
359+
)
360+
continue
361+
else:
362+
for i in range(pixel_area[0]):
363+
for k in range(pixel_area[1]):
364+
pointifg_data[channelA] = neaifg.data[channelA][i, k, :]
365+
pointifg_data[channelP] = neaifg.data[channelP][i, k, :]
366+
pointifg_data["M"] = neaifg.data["M"][i, k, :]
367+
pointifg = NeaInterferogram(pointifg_data, pointifg_params)
368+
(
369+
ampFullData[i, k, :],
370+
phiFullData[i, k, :],
371+
fFullData[i, k, :],
372+
) = ProcessSingleChannel(
373+
order,
374+
method=self.method,
375+
apod=self.apod,
376+
windowtype=self.windowtype,
377+
nzeros=self.nzeros,
378+
interpmethod=self.interpmethod,
379+
simpleoutput=True,
380+
).transform(
381+
pointifg
382+
)
383+
384+
spectra.data[channelA] = ampFullData
385+
spectra.data[channelP] = phiFullData
386+
spectra.data["Wavenumber"] = fFullData
353387

354388
spectra._parameters["PixelArea"] = pixel_area
355389

@@ -363,7 +397,7 @@ def __init__(self):
363397

364398
@staticmethod
365399
def reshape_ifg_data(data, params):
366-
if params["PixelArea"][1] != 1 and params["PixelArea"][0] != 1:
400+
if params["PixelArea"][1] != 1 or params["PixelArea"][0] != 1:
367401
for channel in list(data.keys()):
368402
data[channel] = np.reshape(
369403
data[channel],

pySNOM/readers.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,20 +307,20 @@ def read_header(self):
307307
while f:
308308
line = f.readline()
309309
count = count + 1
310-
if line[0] != "#":
310+
if line[0] not in ("#", "\n"):
311311
break
312-
params = self.lineparser(line, params)
312+
if line[0] == "#":
313+
params = self.lineparser(line, params)
313314
channels = line.split("\t")
314-
channels = [
315-
channel.strip() for channel in channels
316-
]
315+
channels = [channel.strip() for channel in channels[:-1]]
317316

318317
return channels, params
319318

320319
def read(self):
321320
data = {}
322321

323322
channels, params = self.read_header()
323+
channels.append("")
324324

325325
count = len(list(params.keys())) + 2
326326

@@ -330,8 +330,12 @@ def read(self):
330330
skiprows=count,
331331
encoding="utf-8",
332332
names=channels,
333+
lineterminator="\n",
333334
).dropna(axis=1, how="all")
334335

336+
cols_to_keep = [c for c in data.columns if c != ""]
337+
data = data[cols_to_keep]
338+
335339
if self.output == "dict":
336340
data = data.to_dict("list")
337341
for key in list(data.keys()):

pySNOM/spectra.py

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -213,50 +213,25 @@ def transform(self, spectrum, refspectrum):
213213
# TOOLS ------------------------------------------------------------------------------------------------------------------
214214
class Tools:
215215
def reshape_spectrum_data(data, params):
216+
# To compensate for the zero-filling that NeaSpec does
217+
n = 1
218+
if params["Scan"] == "Fourier Scan":
219+
n = 2
220+
216221
for channel in list(data.keys()):
217222
# Point spectrum
218223
if params["PixelArea"][1] == 1 and params["PixelArea"][0] == 1:
219-
# NeaSpec tends to do zero-filling: 4 when processing spectra
220-
if params["Scan"] == "Fourier Scan":
221-
data[channel] = np.reshape(
222-
data[channel], (params["PixelArea"][2] * 2)
223-
)
224-
else:
225-
data[data[channel]] = np.reshape(
226-
data[channel], (params["PixelArea"][2])
227-
)
228-
# LineScan
229-
elif params["PixelArea"][1] == 1 and params["PixelArea"][0] != 1:
230-
# NeaSpec tends to do zero-filling: 4 when processing spectra
231-
if params["Scan"] == "Fourier Scan":
232-
data[data[channel]] = np.reshape(
233-
data[channel],
234-
(params["PixelArea"][0], params["PixelArea"][2] * 2),
235-
)
236-
else:
237-
data[data[channel]] = np.reshape(
238-
data[channel], (params["PixelArea"][0], params["PixelArea"][2])
239-
)
240-
# HyperScan
224+
data[channel] = np.reshape(data[channel], (params["PixelArea"][2] * n))
225+
226+
# Linescan and HyperScan
241227
else:
242-
# NeaSpec tends to do zero-filling: 4 when processing spectra
243-
if params["Scan"] == "Fourier Scan":
244-
data[data[channel]] = np.reshape(
245-
data[channel],
246-
(
247-
params["PixelArea"][0],
248-
params["PixelArea"][1],
249-
params["PixelArea"][2] * 2,
250-
),
251-
)
252-
else:
253-
data[data[channel]] = np.reshape(
254-
data[channel],
255-
(
256-
params["PixelArea"][0],
257-
params["PixelArea"][1],
258-
params["PixelArea"][2],
259-
),
260-
)
228+
data[data[channel]] = np.reshape(
229+
data[channel],
230+
(
231+
params["PixelArea"][0],
232+
params["PixelArea"][1],
233+
params["PixelArea"][2] * n,
234+
),
235+
)
261236

262237
return data

pySNOM/tests/test_interferograms.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,26 @@ def __init__(self, methodName: str = "runTest") -> None:
2121
os.path.join(pySNOM.__path__[0], fdata)
2222
)
2323
data, measparams = data_reader.read()
24-
2524
self.ifg = NeaInterferogram(data, measparams, filename=fdata)
2625

26+
f = "datasets/testifg_multipoints.txt"
27+
file_reader = readers.NeaInterferogramReader(
28+
os.path.join(pySNOM.__path__[0], f)
29+
)
30+
data, params = file_reader.read()
31+
self.multi_ifg = NeaInterferogram(data, params)
32+
2733
def test_pointinterferogram_object(self):
2834
np.testing.assert_almost_equal(self.ifg.data["O2A"][100], 9.564073)
2935
np.testing.assert_string_equal(self.ifg.parameters["Scan"], "Fourier Scan")
3036
np.testing.assert_string_equal(self.ifg.scantype, "Point")
3137
np.testing.assert_equal(np.shape(self.ifg.data["O2A"])[0], 2048)
3238

39+
def test_multipointinterferogram_object(self):
40+
np.testing.assert_array_equal(
41+
self.multi_ifg.data["Run"][1, 0], np.asarray([0, 0, 0, 1, 1, 1])
42+
)
43+
3344
def test_singlechannel_process(self):
3445
a2, p2, wn2 = ProcessSingleChannel(order=2, simpleoutput=True).transform(
3546
self.ifg

0 commit comments

Comments
 (0)