File size: 3,755 Bytes
b77ba05
8d466ed
 
b77ba05
8d466ed
 
 
 
 
 
 
 
 
b77ba05
8d466ed
 
 
b9c9405
8d466ed
 
 
 
 
 
 
 
7b415ce
8d466ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b77ba05
8d466ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b77ba05
 
8d466ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9c9405
8d466ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import numpy as np
from scipy.interpolate import interp1d


def unified_preprocess_spectrum(
    wavenumbers,
    intensities,
    declared_yunits="",
    target_start=500.0,
    target_end=4000.0,
    num_points=3500,
):
    """Unified standard physical preprocessing pipeline for IR spectra.

    Strictly applies: Truncation of out-of-range ends, flat-padding with edge values 
    for missing ends, linear interpolation within the measured region, and normalization to [0, 1].
    """
    try:
        wn = np.array(wavenumbers, dtype=np.float64)
        spec = np.array(intensities, dtype=np.float64)

        # 1. Filter out invalid/non-finite data points
        valid_mask = np.isfinite(wn) & np.isfinite(spec)
        wn = wn[valid_mask]
        spec = spec[valid_mask]
        if len(wn) < 10:
            return None

        # 2. Sort in ascending order
        sort_idx = np.argsort(wn)
        wn = wn[sort_idx]
        spec = spec[sort_idx]

        # 3. Determine spectrum type (Transmittance vs. Absorbance)
        y_max = np.max(spec)
        y_median = np.median(spec)
        declared_upper = declared_yunits.upper()

        is_transmittance = False
        if any(t in declared_upper for t in ["TRANSMITTANCE", "%T", "PERCENT"]):
            is_transmittance = True
        elif any(a in declared_upper for a in ["ABSORBANCE", "ABS", "OD"]):
            is_transmittance = False
        else:
            if (y_max > 10.0 and y_median >= 40.0) or (
                y_median < 40.0 and y_max >= 95.0
            ):
                is_transmittance = True
            elif y_max <= 1.5 and y_median >= 0.6:
                is_transmittance = True

        # 4. Convert to absorbance (including removal of zero-transmittance dead zones)
        if is_transmittance:
            pos_mask = spec > 1e-5
            wn = wn[pos_mask]
            spec = spec[pos_mask]
            if len(wn) < 10:
                return None

            if np.max(spec) > 2.0:
                spec = spec / 100.0

            spec_clipped = np.clip(spec, 1e-4, 1.0)
            abs_spec = -np.log10(spec_clipped)
        else:
            abs_spec = spec - np.min(spec) if np.min(spec) < 0 else spec

        # Remove duplicates to ensure strictly monotonically increasing wavenumbers
        unique_wn, unique_idx = np.unique(wn, return_index=True)
        unique_abs = abs_spec[unique_idx]

        # 5. Enforce target range truncation and boundary padding (e.g., 500–4000 cm^-1)
        target_grid = np.linspace(target_start, target_end, num_points)
        orig_min = unique_wn[0]
        orig_max = unique_wn[-1]

        resampled_abs = np.zeros(num_points, dtype=np.float64)

        # 5.1 Below the actual start wavenumber: flat-pad using the first valid measured value
        left_mask = target_grid < orig_min
        resampled_abs[left_mask] = unique_abs[0]

        # 5.2 Above the actual end wavenumber: flat-pad using the last valid measured value
        right_mask = target_grid > orig_max
        resampled_abs[right_mask] = unique_abs[-1]

        # 5.3 Points within the measured range: linear interpolation (out-of-bounds regions are truncated)
        overlap_mask = (~left_mask) & (~right_mask)
        if np.any(overlap_mask):
            interp_func = interp1d(unique_wn, unique_abs, kind="linear")
            resampled_abs[overlap_mask] = interp_func(target_grid[overlap_mask])

        # 6. Min-Max normalization to [0, 1]
        min_v = np.min(resampled_abs)
        max_v = np.max(resampled_abs)
        if max_v > min_v:
            norm_abs = (resampled_abs - min_v) / (max_v - min_v)
        else:
            norm_abs = np.zeros_like(resampled_abs)

        return norm_abs

    except Exception:
        return None