Coverage for enpt/model/srf.py: 71%
77 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-03-07 11:39 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-03-07 11:39 +0000
1# -*- coding: utf-8 -*-
3# EnPT, EnMAP Processing Tool - A Python package for pre-processing of EnMAP Level-1B data
4#
5# Copyright (C) 2018-2024 Karl Segl (GFZ Potsdam, segl@gfz-potsdam.de), Daniel Scheffler
6# (GFZ Potsdam, danschef@gfz-potsdam.de), Niklas Bohn (GFZ Potsdam, nbohn@gfz-potsdam.de),
7# Stéphane Guillaso (GFZ Potsdam, stephane.guillaso@gfz-potsdam.de)
8#
9# This software was developed within the context of the EnMAP project supported
10# by the DLR Space Administration with funds of the German Federal Ministry of
11# Economic Affairs and Energy (on the basis of a decision by the German Bundestag:
12# 50 EE 1529) and contributions from DLR, GFZ and OHB System AG.
13#
14# This program is free software: you can redistribute it and/or modify it under
15# the terms of the GNU General Public License as published by the Free Software
16# Foundation, either version 3 of the License, or (at your option) any later
17# version. Please note the following exception: `EnPT` depends on tqdm, which
18# is distributed under the Mozilla Public Licence (MPL) v2.0 except for the files
19# "tqdm/_tqdm.py", "setup.py", "README.rst", "MANIFEST.in" and ".gitignore".
20# Details can be found here: https://github.com/tqdm/tqdm/blob/master/LICENCE.
21#
22# This program is distributed in the hope that it will be useful, but WITHOUT
23# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
24# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
25# details.
26#
27# You should have received a copy of the GNU Lesser General Public License along
28# with this program. If not, see <https://www.gnu.org/licenses/>.
30"""EnPT module for handling spectral response functions."""
32from typing import Union, List
34import numpy as np
35from matplotlib import pyplot as plt
36from scipy import stats
38__author__ = 'Daniel Scheffler'
41class SRF(object):
42 def __init__(self, wvl_unit: str = 'nanometers', wvl_min: float = 400, wvl_max: float = 2500, specres_nm: float = 1,
43 format_bandnames: bool = False, v: bool = False):
44 """SRF instance provides SRF functions, wavelength positions, etc..
46 :param wvl_unit: the wavelengths unit to be used within SRF instance ('nanometers' or 'micrometers)
47 :param wvl_min:
48 :param wvl_max:
49 :param specres_nm: output spectral resolution of SRFs in nanometers
50 :param format_bandnames: whether to format default strings from LayerBandsAssignment as 'B01', 'B02' etc..
51 :param v: verbose mode
52 """
53 if wvl_unit not in ['micrometers', 'nanometers']:
54 raise ValueError('Unknown wavelength unit %s.' % wvl_unit)
56 self.srfs_wvl = [] # wavelength positions with 1 nm precision
57 self.srfs = {} # srf values with 1 nm precision
58 self.srfs_norm01 = {} # srf values with 1 nm precision
59 self.bands = []
60 self.wvl = None
61 self.wvl_unit = wvl_unit
62 self.wvl_min = wvl_min
63 self.wvl_max = wvl_max
64 self.specres_nm = specres_nm
65 self.format_bandnames = format_bandnames
66 self.conv = {}
67 self.v = v
69 @staticmethod
70 def compute_gaussian_srf(cwl: float, fwhm: float, wvl_min: float, wvl_max: float, wvl_res: float,
71 normalize: bool = True) -> np.ndarray:
72 """Compute a spectral response function based on center wavelength and bandwidth using on a gaussian curve.
74 :param cwl: target center wavelength position
75 :param fwhm: target bandwidth (full width half maximum)
76 :param wvl_min: minimum wavelength to compute spectral response for
77 :param wvl_max: maximum wavelength to compute spectral response for
78 :param wvl_res: spectral resolution at which spectral response is to be computed
79 :param normalize: whether to normalize the output spectral response to values between 0 and 1
80 :return: 2D numpy.ndarray: rows: response per wavelength; columns: wavelength/response
81 """
82 x = np.arange(wvl_min, wvl_max, wvl_res)
83 dist = stats.norm(cwl, fwhm)
85 with np.errstate(under='ignore'):
86 # This suppresses Numpy warnings: "underflow encountered in exp/divide/multiply" due to very small values
87 y = dist.pdf(x)
89 if normalize:
90 y *= (1.0 / y.max())
92 rsp = np.empty((x.size, 2), dtype=float)
93 rsp[:, 0] = x
94 rsp[:, 1] = y
96 return rsp
98 @classmethod
99 def from_cwl_fwhm(cls, cwls: Union[list, np.ndarray], fwhms: Union[list, np.ndarray], **kwargs: dict) -> 'SRF':
100 """Create an instance of SRF based on center wavelength positions and bandwidths (using gaussian responses).
102 :param cwls: center wavelength positions
103 :param fwhms: bandwidths
104 :param kwargs: Keyword arguments to be passed to SRF.__init__().
105 :return: SRF instance
106 """
107 srf = cls(**kwargs)
109 if srf.wvl_unit != 'nanometers':
110 cwls, fwhms = cwls * 1000, fwhms * 1000
112 srf.bands = [str(i + 1) for i in range(len(cwls))]
114 # compute SRFs and set attributes
115 for bN, cwl, fwhm in zip(srf.bands, cwls, fwhms):
116 gaussian_srf = cls.compute_gaussian_srf(cwl, fwhm, srf.wvl_min, srf.wvl_max, srf.specres_nm)
117 srf.srfs_wvl = gaussian_srf[:, 0].flatten()
118 srf_norm01 = gaussian_srf[:, 1].flatten()
119 srf.srfs_norm01[bN] = srf_norm01
120 with np.errstate(under='ignore'): # suppress Warning: "underflow encountered in divide due to small values
121 srf.srfs[bN] = srf_norm01 / np.trapz(x=srf.srfs_wvl, y=srf_norm01)
123 srf.wvl = np.array(cwls)
125 srf.conv.update({key: value for key, value in zip(srf.bands, srf.wvl)})
126 srf.conv.update({value: key for key, value in zip(srf.bands, srf.wvl)})
128 return srf
130 def instrument(self, bands):
131 return {
132 'rspf': np.vstack([self[band] for band in bands]),
133 'wvl_rsp': np.copy(self.srfs_wvl),
134 'wvl_inst': np.copy(self.wvl),
135 'sol_irr': None
136 }
138 def convert_wvl_unit(self):
139 """Convert the wavelength unit to nanometers if they are in micrometers or vice versa."""
140 factor = 1/1000 if self.wvl_unit == 'nanometers' else 1000
141 self.srfs_wvl = self.srfs_wvl * factor
142 self.wvl = self.wvl * factor
143 self.wvl_unit = 'nanometers' if self.wvl_unit == 'micrometers' else 'micrometers'
145 def __call__(self, band):
146 return self.srfs[band]
148 def __getitem__(self, band):
149 return self.srfs[band]
151 def __iter__(self):
152 for band in self.bands:
153 yield self[band]
155 def plot_srfs(self, figsize: tuple = (15, 5), band: Union[str, List[str]] = None, normalize: bool = True):
156 """Show a plot of all spectral response functions.
158 :param figsize: figure size of the plot
159 :param band: band key to plot a single band instead of all bands
160 :param normalize: normalize SRFs to 0-1 (default: True)
161 """
162 if band and band not in self.bands:
163 raise ValueError("Parameter 'band' must be a string out of those: %s."
164 % ', '.join(self.bands))
166 plt.figure(figsize=figsize)
167 bands2plot = [band] if band else self.bands
168 for band in bands2plot:
169 srfs = list(self.srfs_norm01[band]) if normalize else list(self.srfs[band])
170 plt.plot(self.srfs_wvl, srfs, '-', label='Band %s' % band)
171 plt.title('EnMAP spectral response functions')
172 plt.xlabel('wavelength [%s]' % self.wvl_unit)
173 plt.ylabel('spectral response')
174 plt.legend(loc='upper right')