hylite.hyfeature

Fit and visualise individual hyperspectral features.

  1"""
  2Fit and visualise individual hyperspectral features.
  3"""
  4
  5import numpy as np
  6import matplotlib.pyplot as plt
  7from scipy.optimize import least_squares
  8from scipy.signal import argrelmin
  9from tqdm import tqdm
 10
 11from gfit import gfit, initialise, evaluate
 12
 13class HyFeature(object):
 14    """
 15    Utility class for representing and fitting individual or multiple absorption features.
 16    """
 17
 18    def __init__(self, name, pos, width, depth=1, data=None, color='g'):
 19        """
 20        Args:
 21            name (str) a name for this feature.
 22            pos (float): the position of this feature (in nm).
 23            width (float): the width of this feature (in nm).
 24            data (ndarray): a real spectra associated with this feature (e.g. for feature fitting or from reference libraries).
 25                  Should be a numpy array such that data[0,:] gives wavelength and data[1,:] gives reflectance.
 26        """
 27
 28        self.name = name
 29        self.pos = pos
 30        self.width = width
 31        self.depth = depth
 32        self.color = color
 33        self.data = data
 34        self.mae = -1
 35        self.strength = -1
 36        self.components = None
 37        self.endmembers = None
 38
 39    def get_start(self):
 40        """
 41        Get start of feature.
 42
 43        Returns:
 44            the feature position - 0.5 * feature width.
 45        """
 46
 47        return self.pos - self.width * 0.5
 48
 49    def get_end(self):
 50        """
 51        Get approximate end of feature
 52
 53        Returns:
 54        returns feature position - 0.5 * feature width.
 55        """
 56
 57        return self.pos + self.width * 0.5
 58
 59
 60    ######################
 61    ## Feature models
 62    ######################
 63    @classmethod
 64    def gaussian(cls, _x, pos, width, depth):
 65        """
 66        Static function for evaluating a gaussian feature model
 67
 68        Args:
 69            x (ndarray): wavelengths (nanometres) to evaluate the feature over
 70            pos (float): position for the gaussian function (nanometres)
 71            width (float): width for the gaussian function.
 72            depth (float): depth for the gaussian function (max to min)
 73            offset (float): the vertical offset of the functions. Default is 1.0.
 74        """
 75        return 1 - depth * np.exp( -(_x - pos)**2 / width )
 76
 77    @classmethod
 78    def multi_gauss(cls, x, pos, width, depth, asym=None):
 79        """
 80        Static function for evaluating a multi-gaussian feature model
 81
 82        Args:
 83            x (ndarray): wavelengths (nanometres) to evaluate the feature over
 84            pos (list): a list of positions for each individual gaussian function (nanometres)
 85            width (list): a list of widths for each individual gaussian function.
 86            depth (list): a list of depths for each individual gaussian function (max to min)
 87            asym (list): a list of feature asymmetries. The right-hand width will be calculated as:
 88                         w2 = asym * width. Default is 1.0.
 89        """
 90        if asym is None:
 91            asym = np.ones( len(width) )
 92        M = np.hstack( [[depth[i], pos[i], width[i], width[i]*asym[i]] for i in range(len(depth))] )
 93        y = evaluate( x, M, sym=False )
 94        return 1 - y
 95
 96    # noinspection PyDefaultArgument
 97    def quick_plot(self, method='gauss', ax=None, label='top', lab_kwds={}, **kwds):
 98        """
 99        Quickly plot this feature.
100
101        Args:
102            method (str): the method used to represent this feature. Options are:
103
104                        - 'gauss' = represent using a gaussian function
105                        - 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
106                        - 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
107                        - 'line' = plot a (vertical) line at the position of this feature.
108                        - 'all' = plot with all of the above methods.
109
110            ax: an axis to add the plot to. If None (default) a new axis is created.
111            label (float): Label this feature (using it's name?). Options are None (no label), 'top', 'middle' or 'lower'. Or,
112                   if an integer is passed, odd integers will be plotted as 'top' and even integers as 'lower'.
113            lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
114            **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
115
116        Returns:
117            Tuple containing
118
119            - fig: the figure that was plotted to.
120            - ax: the axis that was plotted to.
121        """
122
123        if ax is None:
124            fig, ax = plt.subplots()
125
126        # plot reference spectra and get _x for plotting
127        if self.data is not None:
128            _x = self.data[0, : ]
129            ax.plot(_x, self.data[1, :], color='k', **kwds)
130        else:
131            _x = np.linspace(self.pos - self.width, self.pos + self.width)
132
133        # set color
134        if 'c' in kwds:
135            kwds['color'] = kwds['c']
136            del kwds['c']
137        kwds['color'] = kwds.get('color', self.color)
138
139        # get _x for plotting
140        if 'range' in method.lower() or 'all' in method.lower():
141            ax.axvline(self.pos - self.width / 2, **kwds)
142            ax.axvline(self.pos + self.width / 2, **kwds)
143        if 'line' in method.lower() or 'all' in method.lower():
144            ax.axvline(self.pos, color='k', alpha=0.4)
145        if 'gauss' in method.lower() or 'all' in method.lower():
146            if self.components is None: # plot single feature
147                _y = HyFeature.gaussian(_x, self.pos, self.width, self.depth)
148            else:
149                _y = HyFeature.multi_gauss(_x, [c.pos for c in self.components],
150                                               [c.width for c in self.components],
151                                               [c.depth for c in self.components] )
152            ax.plot(_x, _y, **kwds)
153        if 'fill' in method.lower() or 'all' in method.lower():
154            kwds['alpha'] = kwds.get('alpha', 0.25)
155            ax.axvspan(self.pos - self.width / 2, self.pos + self.width / 2, **kwds)
156
157        # label
158        if not label is None:
159
160            # calculate label position
161            rnge = ax.get_ylim()[1] - ax.get_ylim()[0]
162            if isinstance(label, int):
163                if label % 2 == 0:
164                    label = 'top'  # even
165                else:
166                    label = 'low'  # odd
167            if 'top' in label.lower():
168                _y = ax.get_ylim()[1] - 0.05 * rnge
169                va = lab_kwds.get('va', 'top')
170            elif 'mid' in label.lower():
171                _y = ax.get_ylim()[0] + 0.5 * rnge
172                va = lab_kwds.get('va', 'center')
173            elif 'low' in label.lower():
174                _y = ax.get_ylim()[0] + 0.05 * rnge
175                va = lab_kwds.get('va', 'bottom')
176            else:
177                assert False, "Error - invalid label position '%s'" % label.lower()
178
179            # plot label
180            lab_kwds['rotation'] = lab_kwds.get('rotation', 90)
181            lab_kwds['alpha'] = lab_kwds.get('alpha', 0.5)
182            ha = lab_kwds.get('ha', 'center')
183            if 'ha' in lab_kwds: del lab_kwds['ha']
184            if 'va' in lab_kwds: del lab_kwds['va']
185            lab_kwds['bbox'] = lab_kwds.get('bbox', dict(boxstyle="round",
186                                                         ec=(0.2, 0.2, 0.2),
187                                                         fc=(1., 1., 1.),
188                                                         ))
189            ax.text(self.pos, _y, self.name, va=va, ha=ha, **lab_kwds)
190
191        return ax.get_figure(), ax
192
193class MultiFeature(HyFeature):
194    """
195    A spectral feature with variable position due to a solid solution between known end-members.
196    """
197
198    def __init__(self, name, endmembers):
199        """
200        Args:
201            endmembers (list): a list of HyFeature objects representing each end-member.
202        """
203
204        # init this feature so that it ~ covers all of its 'sub-features'
205        minw = min([e.pos - e.width / 2 for e in endmembers])
206        maxw = max([e.pos + e.width / 2 for e in endmembers])
207        depth = np.mean([e.depth for e in endmembers])
208        super().__init__(name, pos=(minw + maxw) / 2, width=maxw - minw, depth=depth, color=endmembers[0].color)
209
210        # store endmemebers
211        self.endmembers = endmembers
212
213    def count(self):
214        return len(self.endmembers)
215
216    def quick_plot(self, method='fill+line', ax=None, suplabel=None, sublabel=('alternate', {}), **kwds):
217        """
218         Quickly plot this feature.
219
220         Args:
221            method (str): the method used to represent this feature. Default is 'fill+line'. Options are:
222
223                         - 'gauss' = represent using a gaussian function at each endmember.
224                         - 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
225                         - 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
226                         - 'line' = plot a (vertical) line at the position of each feature.
227                         - 'all' = plot with all of the above methods.
228
229            ax: an axis to add the plot to. If None (default) a new axis is created.
230            suplabel (str): Label positions for this feature. Default is None (no labels). Options are 'top', 'middle' or 'lower'.
231            sublabel (str): Label positions for endmembers. Options are None (no labels), 'top', 'middle', 'lower' or 'alternate'. Or, if an integer
232                    is passed then it will be used to initialise an alternating pattern (even = top, odd = lower).
233            lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
234            **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
235
236         Returns:
237            Tuple containing
238
239            - fig: the figure that was plotted to
240            - ax: the axis that was plotted to
241         """
242
243        if ax is None:
244            fig, ax = plt.subplots()
245
246        # plot
247        if 'range' in method.lower() or 'all' in method.lower():
248            super().quick_plot(method='range', ax=ax, label=None, **kwds)
249        if 'line' in method.lower() or 'all' in method.lower():
250            for e in self.endmembers:  # plot line for each end-member
251                e.quick_plot(method='line', ax=ax, label=None, **kwds)
252        if 'gauss' in method.lower() or 'all' in method.lower():
253            for e in self.endmembers:  # plot gaussian for each end-member
254                e.quick_plot(method='gauss', ax=ax, label=None, **kwds)
255                if isinstance(sublabel, int): sublabel += 1
256        if 'fill' in method.lower() or 'all' in method.lower():
257            super().quick_plot(method='fill', ax=ax, label=None, **kwds)
258
259        # and do labels
260        if not suplabel is None:
261            if not isinstance(suplabel, tuple): suplabel = (suplabel, {})
262            super().quick_plot(method='label', ax=ax, label=suplabel[0], lab_kwds=suplabel[1])
263        if not sublabel is None:
264            if not isinstance(sublabel, tuple): sublabel = (sublabel, {})
265            if isinstance(sublabel[0], str) and 'alt' in sublabel[0].lower():
266                sublabel = (1, sublabel[1])  # alternate labelling
267            for e in self.endmembers:
268                e.quick_plot(method='label', ax=ax, label=sublabel[0], lab_kwds=sublabel[1])
269                sublabel = (sublabel[0] + 1, sublabel[1])
270        return ax.get_figure(), ax
271
272class MixedFeature(HyFeature):
273    """
274    A spectral feature resulting from a mixture of known sub-features.
275    """
276
277    def __init__(self, name, components, **kwds):
278        """
279        Args:
280            components: a list of HyFeature objects representing each end-member.
281            **kwds: keywords are passed to HyFeature.init()
282        """
283
284        # init this feature so that it ~ covers all of its 'sub-features'
285        minw = min([e.pos - e.width / 2 for e in components])
286        maxw = max([e.pos + e.width / 2 for e in components])
287        depth = np.mean([e.depth for e in components])
288
289        if not 'color' in kwds:
290            kwds['color'] = components[0].color
291        super().__init__(name, pos=(minw + maxw) / 2, width=maxw - minw, depth=depth, **kwds)
292
293        # store components
294        self.components = components
295
296    def count(self):
297        return len(self.components)
class HyFeature:
 14class HyFeature(object):
 15    """
 16    Utility class for representing and fitting individual or multiple absorption features.
 17    """
 18
 19    def __init__(self, name, pos, width, depth=1, data=None, color='g'):
 20        """
 21        Args:
 22            name (str) a name for this feature.
 23            pos (float): the position of this feature (in nm).
 24            width (float): the width of this feature (in nm).
 25            data (ndarray): a real spectra associated with this feature (e.g. for feature fitting or from reference libraries).
 26                  Should be a numpy array such that data[0,:] gives wavelength and data[1,:] gives reflectance.
 27        """
 28
 29        self.name = name
 30        self.pos = pos
 31        self.width = width
 32        self.depth = depth
 33        self.color = color
 34        self.data = data
 35        self.mae = -1
 36        self.strength = -1
 37        self.components = None
 38        self.endmembers = None
 39
 40    def get_start(self):
 41        """
 42        Get start of feature.
 43
 44        Returns:
 45            the feature position - 0.5 * feature width.
 46        """
 47
 48        return self.pos - self.width * 0.5
 49
 50    def get_end(self):
 51        """
 52        Get approximate end of feature
 53
 54        Returns:
 55        returns feature position - 0.5 * feature width.
 56        """
 57
 58        return self.pos + self.width * 0.5
 59
 60
 61    ######################
 62    ## Feature models
 63    ######################
 64    @classmethod
 65    def gaussian(cls, _x, pos, width, depth):
 66        """
 67        Static function for evaluating a gaussian feature model
 68
 69        Args:
 70            x (ndarray): wavelengths (nanometres) to evaluate the feature over
 71            pos (float): position for the gaussian function (nanometres)
 72            width (float): width for the gaussian function.
 73            depth (float): depth for the gaussian function (max to min)
 74            offset (float): the vertical offset of the functions. Default is 1.0.
 75        """
 76        return 1 - depth * np.exp( -(_x - pos)**2 / width )
 77
 78    @classmethod
 79    def multi_gauss(cls, x, pos, width, depth, asym=None):
 80        """
 81        Static function for evaluating a multi-gaussian feature model
 82
 83        Args:
 84            x (ndarray): wavelengths (nanometres) to evaluate the feature over
 85            pos (list): a list of positions for each individual gaussian function (nanometres)
 86            width (list): a list of widths for each individual gaussian function.
 87            depth (list): a list of depths for each individual gaussian function (max to min)
 88            asym (list): a list of feature asymmetries. The right-hand width will be calculated as:
 89                         w2 = asym * width. Default is 1.0.
 90        """
 91        if asym is None:
 92            asym = np.ones( len(width) )
 93        M = np.hstack( [[depth[i], pos[i], width[i], width[i]*asym[i]] for i in range(len(depth))] )
 94        y = evaluate( x, M, sym=False )
 95        return 1 - y
 96
 97    # noinspection PyDefaultArgument
 98    def quick_plot(self, method='gauss', ax=None, label='top', lab_kwds={}, **kwds):
 99        """
100        Quickly plot this feature.
101
102        Args:
103            method (str): the method used to represent this feature. Options are:
104
105                        - 'gauss' = represent using a gaussian function
106                        - 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
107                        - 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
108                        - 'line' = plot a (vertical) line at the position of this feature.
109                        - 'all' = plot with all of the above methods.
110
111            ax: an axis to add the plot to. If None (default) a new axis is created.
112            label (float): Label this feature (using it's name?). Options are None (no label), 'top', 'middle' or 'lower'. Or,
113                   if an integer is passed, odd integers will be plotted as 'top' and even integers as 'lower'.
114            lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
115            **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
116
117        Returns:
118            Tuple containing
119
120            - fig: the figure that was plotted to.
121            - ax: the axis that was plotted to.
122        """
123
124        if ax is None:
125            fig, ax = plt.subplots()
126
127        # plot reference spectra and get _x for plotting
128        if self.data is not None:
129            _x = self.data[0, : ]
130            ax.plot(_x, self.data[1, :], color='k', **kwds)
131        else:
132            _x = np.linspace(self.pos - self.width, self.pos + self.width)
133
134        # set color
135        if 'c' in kwds:
136            kwds['color'] = kwds['c']
137            del kwds['c']
138        kwds['color'] = kwds.get('color', self.color)
139
140        # get _x for plotting
141        if 'range' in method.lower() or 'all' in method.lower():
142            ax.axvline(self.pos - self.width / 2, **kwds)
143            ax.axvline(self.pos + self.width / 2, **kwds)
144        if 'line' in method.lower() or 'all' in method.lower():
145            ax.axvline(self.pos, color='k', alpha=0.4)
146        if 'gauss' in method.lower() or 'all' in method.lower():
147            if self.components is None: # plot single feature
148                _y = HyFeature.gaussian(_x, self.pos, self.width, self.depth)
149            else:
150                _y = HyFeature.multi_gauss(_x, [c.pos for c in self.components],
151                                               [c.width for c in self.components],
152                                               [c.depth for c in self.components] )
153            ax.plot(_x, _y, **kwds)
154        if 'fill' in method.lower() or 'all' in method.lower():
155            kwds['alpha'] = kwds.get('alpha', 0.25)
156            ax.axvspan(self.pos - self.width / 2, self.pos + self.width / 2, **kwds)
157
158        # label
159        if not label is None:
160
161            # calculate label position
162            rnge = ax.get_ylim()[1] - ax.get_ylim()[0]
163            if isinstance(label, int):
164                if label % 2 == 0:
165                    label = 'top'  # even
166                else:
167                    label = 'low'  # odd
168            if 'top' in label.lower():
169                _y = ax.get_ylim()[1] - 0.05 * rnge
170                va = lab_kwds.get('va', 'top')
171            elif 'mid' in label.lower():
172                _y = ax.get_ylim()[0] + 0.5 * rnge
173                va = lab_kwds.get('va', 'center')
174            elif 'low' in label.lower():
175                _y = ax.get_ylim()[0] + 0.05 * rnge
176                va = lab_kwds.get('va', 'bottom')
177            else:
178                assert False, "Error - invalid label position '%s'" % label.lower()
179
180            # plot label
181            lab_kwds['rotation'] = lab_kwds.get('rotation', 90)
182            lab_kwds['alpha'] = lab_kwds.get('alpha', 0.5)
183            ha = lab_kwds.get('ha', 'center')
184            if 'ha' in lab_kwds: del lab_kwds['ha']
185            if 'va' in lab_kwds: del lab_kwds['va']
186            lab_kwds['bbox'] = lab_kwds.get('bbox', dict(boxstyle="round",
187                                                         ec=(0.2, 0.2, 0.2),
188                                                         fc=(1., 1., 1.),
189                                                         ))
190            ax.text(self.pos, _y, self.name, va=va, ha=ha, **lab_kwds)
191
192        return ax.get_figure(), ax

Utility class for representing and fitting individual or multiple absorption features.

HyFeature(name, pos, width, depth=1, data=None, color='g')
19    def __init__(self, name, pos, width, depth=1, data=None, color='g'):
20        """
21        Args:
22            name (str) a name for this feature.
23            pos (float): the position of this feature (in nm).
24            width (float): the width of this feature (in nm).
25            data (ndarray): a real spectra associated with this feature (e.g. for feature fitting or from reference libraries).
26                  Should be a numpy array such that data[0,:] gives wavelength and data[1,:] gives reflectance.
27        """
28
29        self.name = name
30        self.pos = pos
31        self.width = width
32        self.depth = depth
33        self.color = color
34        self.data = data
35        self.mae = -1
36        self.strength = -1
37        self.components = None
38        self.endmembers = None
Arguments:
  • name (str) a name for this feature.
  • pos (float): the position of this feature (in nm).
  • width (float): the width of this feature (in nm).
  • data (ndarray): a real spectra associated with this feature (e.g. for feature fitting or from reference libraries). Should be a numpy array such that data[0,:] gives wavelength and data[1,:] gives reflectance.
def get_start(self):
40    def get_start(self):
41        """
42        Get start of feature.
43
44        Returns:
45            the feature position - 0.5 * feature width.
46        """
47
48        return self.pos - self.width * 0.5

Get start of feature.

Returns:

the feature position - 0.5 * feature width.

def get_end(self):
50    def get_end(self):
51        """
52        Get approximate end of feature
53
54        Returns:
55        returns feature position - 0.5 * feature width.
56        """
57
58        return self.pos + self.width * 0.5

Get approximate end of feature

Returns: returns feature position - 0.5 * feature width.

@classmethod
def gaussian(cls, _x, pos, width, depth):
64    @classmethod
65    def gaussian(cls, _x, pos, width, depth):
66        """
67        Static function for evaluating a gaussian feature model
68
69        Args:
70            x (ndarray): wavelengths (nanometres) to evaluate the feature over
71            pos (float): position for the gaussian function (nanometres)
72            width (float): width for the gaussian function.
73            depth (float): depth for the gaussian function (max to min)
74            offset (float): the vertical offset of the functions. Default is 1.0.
75        """
76        return 1 - depth * np.exp( -(_x - pos)**2 / width )

Static function for evaluating a gaussian feature model

Arguments:
  • x (ndarray): wavelengths (nanometres) to evaluate the feature over
  • pos (float): position for the gaussian function (nanometres)
  • width (float): width for the gaussian function.
  • depth (float): depth for the gaussian function (max to min)
  • offset (float): the vertical offset of the functions. Default is 1.0.
@classmethod
def multi_gauss(cls, x, pos, width, depth, asym=None):
78    @classmethod
79    def multi_gauss(cls, x, pos, width, depth, asym=None):
80        """
81        Static function for evaluating a multi-gaussian feature model
82
83        Args:
84            x (ndarray): wavelengths (nanometres) to evaluate the feature over
85            pos (list): a list of positions for each individual gaussian function (nanometres)
86            width (list): a list of widths for each individual gaussian function.
87            depth (list): a list of depths for each individual gaussian function (max to min)
88            asym (list): a list of feature asymmetries. The right-hand width will be calculated as:
89                         w2 = asym * width. Default is 1.0.
90        """
91        if asym is None:
92            asym = np.ones( len(width) )
93        M = np.hstack( [[depth[i], pos[i], width[i], width[i]*asym[i]] for i in range(len(depth))] )
94        y = evaluate( x, M, sym=False )
95        return 1 - y

Static function for evaluating a multi-gaussian feature model

Arguments:
  • x (ndarray): wavelengths (nanometres) to evaluate the feature over
  • pos (list): a list of positions for each individual gaussian function (nanometres)
  • width (list): a list of widths for each individual gaussian function.
  • depth (list): a list of depths for each individual gaussian function (max to min)
  • asym (list): a list of feature asymmetries. The right-hand width will be calculated as: w2 = asym * width. Default is 1.0.
def quick_plot(self, method='gauss', ax=None, label='top', lab_kwds={}, **kwds):
 98    def quick_plot(self, method='gauss', ax=None, label='top', lab_kwds={}, **kwds):
 99        """
100        Quickly plot this feature.
101
102        Args:
103            method (str): the method used to represent this feature. Options are:
104
105                        - 'gauss' = represent using a gaussian function
106                        - 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
107                        - 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
108                        - 'line' = plot a (vertical) line at the position of this feature.
109                        - 'all' = plot with all of the above methods.
110
111            ax: an axis to add the plot to. If None (default) a new axis is created.
112            label (float): Label this feature (using it's name?). Options are None (no label), 'top', 'middle' or 'lower'. Or,
113                   if an integer is passed, odd integers will be plotted as 'top' and even integers as 'lower'.
114            lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
115            **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
116
117        Returns:
118            Tuple containing
119
120            - fig: the figure that was plotted to.
121            - ax: the axis that was plotted to.
122        """
123
124        if ax is None:
125            fig, ax = plt.subplots()
126
127        # plot reference spectra and get _x for plotting
128        if self.data is not None:
129            _x = self.data[0, : ]
130            ax.plot(_x, self.data[1, :], color='k', **kwds)
131        else:
132            _x = np.linspace(self.pos - self.width, self.pos + self.width)
133
134        # set color
135        if 'c' in kwds:
136            kwds['color'] = kwds['c']
137            del kwds['c']
138        kwds['color'] = kwds.get('color', self.color)
139
140        # get _x for plotting
141        if 'range' in method.lower() or 'all' in method.lower():
142            ax.axvline(self.pos - self.width / 2, **kwds)
143            ax.axvline(self.pos + self.width / 2, **kwds)
144        if 'line' in method.lower() or 'all' in method.lower():
145            ax.axvline(self.pos, color='k', alpha=0.4)
146        if 'gauss' in method.lower() or 'all' in method.lower():
147            if self.components is None: # plot single feature
148                _y = HyFeature.gaussian(_x, self.pos, self.width, self.depth)
149            else:
150                _y = HyFeature.multi_gauss(_x, [c.pos for c in self.components],
151                                               [c.width for c in self.components],
152                                               [c.depth for c in self.components] )
153            ax.plot(_x, _y, **kwds)
154        if 'fill' in method.lower() or 'all' in method.lower():
155            kwds['alpha'] = kwds.get('alpha', 0.25)
156            ax.axvspan(self.pos - self.width / 2, self.pos + self.width / 2, **kwds)
157
158        # label
159        if not label is None:
160
161            # calculate label position
162            rnge = ax.get_ylim()[1] - ax.get_ylim()[0]
163            if isinstance(label, int):
164                if label % 2 == 0:
165                    label = 'top'  # even
166                else:
167                    label = 'low'  # odd
168            if 'top' in label.lower():
169                _y = ax.get_ylim()[1] - 0.05 * rnge
170                va = lab_kwds.get('va', 'top')
171            elif 'mid' in label.lower():
172                _y = ax.get_ylim()[0] + 0.5 * rnge
173                va = lab_kwds.get('va', 'center')
174            elif 'low' in label.lower():
175                _y = ax.get_ylim()[0] + 0.05 * rnge
176                va = lab_kwds.get('va', 'bottom')
177            else:
178                assert False, "Error - invalid label position '%s'" % label.lower()
179
180            # plot label
181            lab_kwds['rotation'] = lab_kwds.get('rotation', 90)
182            lab_kwds['alpha'] = lab_kwds.get('alpha', 0.5)
183            ha = lab_kwds.get('ha', 'center')
184            if 'ha' in lab_kwds: del lab_kwds['ha']
185            if 'va' in lab_kwds: del lab_kwds['va']
186            lab_kwds['bbox'] = lab_kwds.get('bbox', dict(boxstyle="round",
187                                                         ec=(0.2, 0.2, 0.2),
188                                                         fc=(1., 1., 1.),
189                                                         ))
190            ax.text(self.pos, _y, self.name, va=va, ha=ha, **lab_kwds)
191
192        return ax.get_figure(), ax

Quickly plot this feature.

Arguments:
  • method (str): the method used to represent this feature. Options are:

    • 'gauss' = represent using a gaussian function
    • 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
    • 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
    • 'line' = plot a (vertical) line at the position of this feature.
    • 'all' = plot with all of the above methods.
  • ax: an axis to add the plot to. If None (default) a new axis is created.
  • label (float): Label this feature (using it's name?). Options are None (no label), 'top', 'middle' or 'lower'. Or, if an integer is passed, odd integers will be plotted as 'top' and even integers as 'lower'.
  • lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
  • **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
Returns:

Tuple containing

  • fig: the figure that was plotted to.
  • ax: the axis that was plotted to.
class HyFeature.Features:
12class Features:
13    """
14    Specific absorption types. Useful for plotting etc. Not really used for anything and will probably be depreciated soon.
15    """
16
17    H2O = [ HyFeature("H2O", p, w, color='skyblue') for p,w in [(825,50), (940,75), (1130,100), (1400,150), (1900,200), (2700,150)] ]
18    OH = [HyFeature("OH", 1400, 50, color='aquamarine'), HyFeature("OH", 1550, 50, color='aquamarine'), HyFeature("OH", 1800, 100, color='aquamarine')]
19    AlOH = [HyFeature("AlOH", 2190, 60, color='salmon')]
20    FeOH = [HyFeature("FeOH", 2265, 70, color='orange')]
21    MgOH = [HyFeature("MgOH", 2330, 60, color='blue'), HyFeature("MgOH", 2385, 30,color='blue')]
22    MgCO3 = [HyFeature("MgCO3", 2320, 20, color='green')]
23    CaCO3 = [HyFeature("CaCO3", 2340, 20, color='blue')]
24    FeCO3 = [HyFeature("FeCO3", 2350, 20, color='steelblue')]
25    Ferrous = [HyFeature("Fe2+", 1000, 400, color='green')]
26    Ferric = [HyFeature("Fe3+", 650, 170, color='green')]
27
28    # REE features
29    Pr = [HyFeature("Pr", w, 5, color=(74 / 256., 155 / 256., 122 / 256., 1)) for w in [457, 485, 473, 595, 1017] ]
30    Nd = [HyFeature("Nd", w, 5, color=(116 / 256., 114 / 256., 174 / 256., 1)) for w in [430, 463, 475, 514, 525, 580, 627, 680, 750, 800, 880, 1430, 1720, 2335, 2470]]
31    Sm = [HyFeature("Sm", w, 5, color=(116 / 256., 114 / 256., 174 / 256., 1)) for w in [945, 959, 1085, 1235, 1257, 1400, 1550]]
32    Eu = [HyFeature("Eu", w, 5, color=(213 / 256., 64 / 256., 136 / 256., 1)) for w in [385, 405, 470, 530, 1900, 2025, 2170, 2400, 2610]]
33    Dy = [HyFeature("Dy", w, 5, color=(117 / 256., 163 / 256., 58 / 256., 1)) for w in [368, 390, 403, 430, 452, 461, 475, 760, 810, 830, 915, 1117, 1276, 1725]]
34    Ho = [HyFeature("Ho", w, 5, color=(222 / 256., 172 / 256., 59 / 256., 1)) for w in [363, 420, 458, 545, 650, 900, 1130, 1180, 1870, 1930, 2005]]
35    Er = [HyFeature("Er", w, 5, color=(159 / 256., 119 / 256., 49 / 256., 1)) for w in [390, 405, 455, 490, 522, 540, 652, 805, 985, 1485, 1545]]
36    Tm = [HyFeature("Tm", w, 5, color=(102 / 256., 102 / 256., 102 / 256., 1)) for w in [390, 470, 660, 685, 780, 1190, 1640, 1750]]
37    Yb = [HyFeature("Yb", w, 5, color=(209 / 256., 53 / 256., 43 / 256., 1)) for w in [955, 975, 1004 ]]

Specific absorption types. Useful for plotting etc. Not really used for anything and will probably be depreciated soon.

class HyFeature.Minerals:
40class Minerals:
41    """
42    Common mineral absorption features. Useful for plotting etc. Not really used for anything and will probably be depreciated soon.
43    """
44
45    # Kaolin clays (dominant SWIR feature)
46    Kaolinite = [HyFeature("Kaolinite/Halloysite", 2200, 100, color='aquamarine')]
47    Halloysite = [Kaolinite]
48    Dickite = [HyFeature("Dickite/Nacrite", 2180, 100, color='aquamarine')]
49    Nacrite = [Dickite]
50    KAOLIN = MultiFeature("Kaolin", Kaolinite + Dickite)
51
52    Pyrophyllite = HyFeature("Pyrophyllite", 2160.0, 150, color='aquamarine')
53
54    #Smectite clays (dominant SWIR feature)
55    Montmorillonite = [HyFeature("Montmorillonite", 2210.0, 125, color='orange')]
56    Nontronite = [HyFeature("Nontronite", 2280, 125, color='orange')]
57    Saponite = [HyFeature("Saponite", 2309, 100, color='orange')]
58    SMECTITE = MultiFeature("Smectite", Montmorillonite + Nontronite + Saponite)
59
60    # white micas (dominant SWIR feature)
61    Mica_Na = [HyFeature("Mica (Na)", 2150, 150, color='coral' )]
62    Mica_K = [HyFeature("Mica (K)", 2190, 150, color='lightcoral' )]
63    Mica_MgFe = [HyFeature("Mica (Mg, Fe)", 2225, 150 , color='sandybrown')]
64    MICA = MultiFeature("White mica", Mica_Na + Mica_K +Mica_MgFe)
65
66    # chlorite
67    Chlorite_Mg = [ HyFeature("Chlorite (Mg)", 2245.0, 50, color='seagreen'), HyFeature("Chlorite (Mg)", 2325.0, 50, color='seagreen') ]
68    Chlorite_Fe = [HyFeature("Chlorite (Fe)", 2261.0, 50, color='seagreen'), HyFeature("Chlorite (Fe)", 2355.0, 50, color='seagreen') ]
69    CHLORITE = [MultiFeature("Chlorite (FeOH)", [Chlorite_Mg[0], Chlorite_Fe[0]]),
70                MultiFeature("Chlorite (MgOH)", [Chlorite_Mg[1], Chlorite_Fe[1]])]
71
72    # biotite
73    Biotite_Mg = [ HyFeature("Biotite (Mg)", 2326, 50, color='firebrick'), HyFeature("Biotite (Mg)", 2377, 50, color='firebrick') ]
74    Biotite_Fe = [ HyFeature("Biotite (Fe)", 2250, 50, color='firebrick'), HyFeature("Biotite (Fe)", 2350, 50, color='firebrick') ]
75    BIOTITE = [MultiFeature("Biotite (FeOH)", [Biotite_Mg[0], Biotite_Fe[0]]),
76               MultiFeature("Biotite (MgOH)", [Biotite_Mg[1], Biotite_Fe[1]]) ]
77
78    # amphiboles Tremolite, hornblende, actinolite
79    Amphibole_Mg =  [HyFeature("Amphibole (Mg)", 2320.0, 50, color='royalblue')]
80    Amphibole_Fe =  [HyFeature("Amphibole (Fe)", 2345.0, 50, color='royalblue')]
81    AMPHIBOLE = MultiFeature("Amphibole", Amphibole_Mg + Amphibole_Fe)
82
83    # carbonate minerals
84    Dolomite = [HyFeature("Dolomite", 2320, 20, color='green')]
85    Calcite = [HyFeature("Calcite", 2345, 20, color='blue')]
86    Ankerite = [HyFeature("Ankerite", 2330, 20, color='steelblue')]
87    CARBONATE = MultiFeature("Carbonate", Dolomite+ Ankerite+ Calcite)
88
89    #Sulphates Jarosite
90    Gypsum = [HyFeature("Gypsum", 1449.0, 50, color='gold'), HyFeature("Gypsum", 1750, 50, color='gold'), HyFeature("Gypsum", 1948.0, 50, color='gold')]
91    Jarosite = [HyFeature("Jarosite", 1470.0, 50, color='orange'), HyFeature("Jarosite", 1850, 50, color='orange'), HyFeature("Jarosite", 2270.0, 50, color='orange')]
92    SULPHATE = MultiFeature( "Sulphate", Gypsum + Jarosite )
93
94    # misc
95    Epidote = [ HyFeature("Epidote", 2256.0, 40, color='green'), HyFeature("Epidote", 2340.0, 40, color='green')]

Common mineral absorption features. Useful for plotting etc. Not really used for anything and will probably be depreciated soon.

class HyFeature.Themes:
 98class Themes:
 99    """
100    Some useful 'themes' (for plotting etc)
101    """
102    ATMOSPHERE = Features.H2O  #[HyFeature("H2O", 975, 30), HyFeature("H2O", 1395, 120), HyFeature("H2O", 1885, 180), HyFeature("H2O", 2450, 100)]
103    CARBONATE = [Minerals.CARBONATE]
104    OH = Features.AlOH + Features.FeOH + Features.MgOH
105    CLAY = [ Minerals.KAOLIN, Minerals.SMECTITE ]
106    DIAGNOSTIC = Features.Ferrous + Features.AlOH+Features.FeOH+Features.MgOH

Some useful 'themes' (for plotting etc)

class MultiFeature(HyFeature):
194class MultiFeature(HyFeature):
195    """
196    A spectral feature with variable position due to a solid solution between known end-members.
197    """
198
199    def __init__(self, name, endmembers):
200        """
201        Args:
202            endmembers (list): a list of HyFeature objects representing each end-member.
203        """
204
205        # init this feature so that it ~ covers all of its 'sub-features'
206        minw = min([e.pos - e.width / 2 for e in endmembers])
207        maxw = max([e.pos + e.width / 2 for e in endmembers])
208        depth = np.mean([e.depth for e in endmembers])
209        super().__init__(name, pos=(minw + maxw) / 2, width=maxw - minw, depth=depth, color=endmembers[0].color)
210
211        # store endmemebers
212        self.endmembers = endmembers
213
214    def count(self):
215        return len(self.endmembers)
216
217    def quick_plot(self, method='fill+line', ax=None, suplabel=None, sublabel=('alternate', {}), **kwds):
218        """
219         Quickly plot this feature.
220
221         Args:
222            method (str): the method used to represent this feature. Default is 'fill+line'. Options are:
223
224                         - 'gauss' = represent using a gaussian function at each endmember.
225                         - 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
226                         - 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
227                         - 'line' = plot a (vertical) line at the position of each feature.
228                         - 'all' = plot with all of the above methods.
229
230            ax: an axis to add the plot to. If None (default) a new axis is created.
231            suplabel (str): Label positions for this feature. Default is None (no labels). Options are 'top', 'middle' or 'lower'.
232            sublabel (str): Label positions for endmembers. Options are None (no labels), 'top', 'middle', 'lower' or 'alternate'. Or, if an integer
233                    is passed then it will be used to initialise an alternating pattern (even = top, odd = lower).
234            lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
235            **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
236
237         Returns:
238            Tuple containing
239
240            - fig: the figure that was plotted to
241            - ax: the axis that was plotted to
242         """
243
244        if ax is None:
245            fig, ax = plt.subplots()
246
247        # plot
248        if 'range' in method.lower() or 'all' in method.lower():
249            super().quick_plot(method='range', ax=ax, label=None, **kwds)
250        if 'line' in method.lower() or 'all' in method.lower():
251            for e in self.endmembers:  # plot line for each end-member
252                e.quick_plot(method='line', ax=ax, label=None, **kwds)
253        if 'gauss' in method.lower() or 'all' in method.lower():
254            for e in self.endmembers:  # plot gaussian for each end-member
255                e.quick_plot(method='gauss', ax=ax, label=None, **kwds)
256                if isinstance(sublabel, int): sublabel += 1
257        if 'fill' in method.lower() or 'all' in method.lower():
258            super().quick_plot(method='fill', ax=ax, label=None, **kwds)
259
260        # and do labels
261        if not suplabel is None:
262            if not isinstance(suplabel, tuple): suplabel = (suplabel, {})
263            super().quick_plot(method='label', ax=ax, label=suplabel[0], lab_kwds=suplabel[1])
264        if not sublabel is None:
265            if not isinstance(sublabel, tuple): sublabel = (sublabel, {})
266            if isinstance(sublabel[0], str) and 'alt' in sublabel[0].lower():
267                sublabel = (1, sublabel[1])  # alternate labelling
268            for e in self.endmembers:
269                e.quick_plot(method='label', ax=ax, label=sublabel[0], lab_kwds=sublabel[1])
270                sublabel = (sublabel[0] + 1, sublabel[1])
271        return ax.get_figure(), ax

A spectral feature with variable position due to a solid solution between known end-members.

MultiFeature(name, endmembers)
199    def __init__(self, name, endmembers):
200        """
201        Args:
202            endmembers (list): a list of HyFeature objects representing each end-member.
203        """
204
205        # init this feature so that it ~ covers all of its 'sub-features'
206        minw = min([e.pos - e.width / 2 for e in endmembers])
207        maxw = max([e.pos + e.width / 2 for e in endmembers])
208        depth = np.mean([e.depth for e in endmembers])
209        super().__init__(name, pos=(minw + maxw) / 2, width=maxw - minw, depth=depth, color=endmembers[0].color)
210
211        # store endmemebers
212        self.endmembers = endmembers
Arguments:
  • endmembers (list): a list of HyFeature objects representing each end-member.
def count(self):
214    def count(self):
215        return len(self.endmembers)
def quick_plot( self, method='fill+line', ax=None, suplabel=None, sublabel=('alternate', {}), **kwds):
217    def quick_plot(self, method='fill+line', ax=None, suplabel=None, sublabel=('alternate', {}), **kwds):
218        """
219         Quickly plot this feature.
220
221         Args:
222            method (str): the method used to represent this feature. Default is 'fill+line'. Options are:
223
224                         - 'gauss' = represent using a gaussian function at each endmember.
225                         - 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
226                         - 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
227                         - 'line' = plot a (vertical) line at the position of each feature.
228                         - 'all' = plot with all of the above methods.
229
230            ax: an axis to add the plot to. If None (default) a new axis is created.
231            suplabel (str): Label positions for this feature. Default is None (no labels). Options are 'top', 'middle' or 'lower'.
232            sublabel (str): Label positions for endmembers. Options are None (no labels), 'top', 'middle', 'lower' or 'alternate'. Or, if an integer
233                    is passed then it will be used to initialise an alternating pattern (even = top, odd = lower).
234            lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
235            **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
236
237         Returns:
238            Tuple containing
239
240            - fig: the figure that was plotted to
241            - ax: the axis that was plotted to
242         """
243
244        if ax is None:
245            fig, ax = plt.subplots()
246
247        # plot
248        if 'range' in method.lower() or 'all' in method.lower():
249            super().quick_plot(method='range', ax=ax, label=None, **kwds)
250        if 'line' in method.lower() or 'all' in method.lower():
251            for e in self.endmembers:  # plot line for each end-member
252                e.quick_plot(method='line', ax=ax, label=None, **kwds)
253        if 'gauss' in method.lower() or 'all' in method.lower():
254            for e in self.endmembers:  # plot gaussian for each end-member
255                e.quick_plot(method='gauss', ax=ax, label=None, **kwds)
256                if isinstance(sublabel, int): sublabel += 1
257        if 'fill' in method.lower() or 'all' in method.lower():
258            super().quick_plot(method='fill', ax=ax, label=None, **kwds)
259
260        # and do labels
261        if not suplabel is None:
262            if not isinstance(suplabel, tuple): suplabel = (suplabel, {})
263            super().quick_plot(method='label', ax=ax, label=suplabel[0], lab_kwds=suplabel[1])
264        if not sublabel is None:
265            if not isinstance(sublabel, tuple): sublabel = (sublabel, {})
266            if isinstance(sublabel[0], str) and 'alt' in sublabel[0].lower():
267                sublabel = (1, sublabel[1])  # alternate labelling
268            for e in self.endmembers:
269                e.quick_plot(method='label', ax=ax, label=sublabel[0], lab_kwds=sublabel[1])
270                sublabel = (sublabel[0] + 1, sublabel[1])
271        return ax.get_figure(), ax

Quickly plot this feature.

Arguments:
  • method (str): the method used to represent this feature. Default is 'fill+line'. Options are:

    • 'gauss' = represent using a gaussian function at each endmember.
    • 'range' = draw vertical lines at pos - width / 2 and pos + width / 2.
    • 'fill' = fill a rectangle in the region dominated by the feature with 'color' specifed in kwds.
    • 'line' = plot a (vertical) line at the position of each feature.
    • 'all' = plot with all of the above methods.
  • ax: an axis to add the plot to. If None (default) a new axis is created.
  • suplabel (str): Label positions for this feature. Default is None (no labels). Options are 'top', 'middle' or 'lower'.
  • sublabel (str): Label positions for endmembers. Options are None (no labels), 'top', 'middle', 'lower' or 'alternate'. Or, if an integer is passed then it will be used to initialise an alternating pattern (even = top, odd = lower).
  • lab_kwds (dict): Dictionary of keywords to pass to plt.text( ... ) for controlling labels.
  • **kwds: Keywords are passed to ax.axvline(...) if method=='range' or ax.plot(...) otherwise.
Returns:

Tuple containing

  • fig: the figure that was plotted to
  • ax: the axis that was plotted to
class MixedFeature(HyFeature):
273class MixedFeature(HyFeature):
274    """
275    A spectral feature resulting from a mixture of known sub-features.
276    """
277
278    def __init__(self, name, components, **kwds):
279        """
280        Args:
281            components: a list of HyFeature objects representing each end-member.
282            **kwds: keywords are passed to HyFeature.init()
283        """
284
285        # init this feature so that it ~ covers all of its 'sub-features'
286        minw = min([e.pos - e.width / 2 for e in components])
287        maxw = max([e.pos + e.width / 2 for e in components])
288        depth = np.mean([e.depth for e in components])
289
290        if not 'color' in kwds:
291            kwds['color'] = components[0].color
292        super().__init__(name, pos=(minw + maxw) / 2, width=maxw - minw, depth=depth, **kwds)
293
294        # store components
295        self.components = components
296
297    def count(self):
298        return len(self.components)

A spectral feature resulting from a mixture of known sub-features.

MixedFeature(name, components, **kwds)
278    def __init__(self, name, components, **kwds):
279        """
280        Args:
281            components: a list of HyFeature objects representing each end-member.
282            **kwds: keywords are passed to HyFeature.init()
283        """
284
285        # init this feature so that it ~ covers all of its 'sub-features'
286        minw = min([e.pos - e.width / 2 for e in components])
287        maxw = max([e.pos + e.width / 2 for e in components])
288        depth = np.mean([e.depth for e in components])
289
290        if not 'color' in kwds:
291            kwds['color'] = components[0].color
292        super().__init__(name, pos=(minw + maxw) / 2, width=maxw - minw, depth=depth, **kwds)
293
294        # store components
295        self.components = components
Arguments:
  • components: a list of HyFeature objects representing each end-member.
  • **kwds: keywords are passed to HyFeature.init()
def count(self):
297    def count(self):
298        return len(self.components)