import numpy as np
from libpyhat.transform.baseline_code.common import Baseline
[docs]
def kajfosz_kwiatek_baseline(
    bands, intensities, top_width=0, bottom_width=50, exponent=2, tangent=False
):
    """
    This function uses an enhanced version of the algorithm published by
    Kajfosz, J. and Kwiatek, W.M. (1987)  "Non-polynomial approximation of
    background in x-ray spectra." Nucl. Instrum. Methods B22, 78-81.
    top_width:
      Specifies the width of the polynomials which are concave upward.
      The top_width is the full width in energy units at which the
      magnitude of the polynomial is 0.1 of max. The default is 0, which
      means that concave upward polynomials are not used.
    bottom_width:
      Specifies the width of the polynomials which are concave downward.
      The bottom_width is the full width in energy units at which the
      magnitude of the polynomial is 0.1 of max. The default is 50.
    exponent:
      Specifies the power of polynomial which is used. The power must be
      an integer. The default is 2, i.e. parabolas. Higher exponents,
      for example EXPONENT=4, results in polynomials with flatter tops
      and steeper sides, which can better fit spectra with steeply
      sloping backgrounds.
    tangent:
      Specifies that the polynomials are to be tangent to the slope of the
      spectrum. The default is vertical polynomials. This option works
      best on steeply sloping spectra. It has trouble in spectra with
      big peaks because the polynomials are very tilted up inside the
      peaks.
    For more info, see:
    cars9.uchicago.edu/software/idl/mca_utility_routines.html#FIT_BACKGROUND
    """
    REFERENCE_AMPL = 0.1
    MAX_TANGENT = 2
    nchans = len(intensities)
    # Normalize intensities for widths to make sense.
    scale_factor = intensities.max()
    scratch = intensities / scale_factor
    slope = abs(np.diff(bands).mean())
    # Fit functions which come down from top
    if top_width > 0:
        power_funct, max_index = _kk_lookup_table(
            scratch, top_width, exponent, slope, REFERENCE_AMPL
        )
        bckgnd = scratch.copy()
        for center_chan in range(nchans):
            first_chan = max((center_chan - max_index), 0)
            last_chan = min(center_chan + max_index + 1, nchans)
            f = first_chan - center_chan + max_index
            last = last_chan - center_chan + max_index
            lin_offset = scratch[center_chan]
            new_bckgnd = power_funct[f:last] + lin_offset
            old_bckgnd = bckgnd[first_chan:last_chan]
            np.copyto(old_bckgnd, new_bckgnd, where=(new_bckgnd > old_bckgnd))
        # Copy this approximation of background to scratch
        scratch = bckgnd.copy()
    else:
        bckgnd = np.empty_like(scratch)
    # Fit functions which come up from below
    power_funct, max_index = _kk_lookup_table(
        scratch, bottom_width, exponent, slope, REFERENCE_AMPL
    )
    bckgnd.fill(-np.inf)
    for center_chan in range(nchans - 1):
        if tangent:
            # Find slope of tangent to spectrum at this channel
            first_chan = max(center_chan - MAX_TANGENT, 0)
            last_chan = min(center_chan + MAX_TANGENT + 1, nchans)
            denom = center_chan - np.arange(last_chan - first_chan)
            tangent_slope = (
                scratch[center_chan] - scratch[first_chan:last_chan]
            ) / np.maximum(denom, 1)
            tangent_slope = np.sum(tangent_slope) / (last_chan - first_chan)
        first_chan = max(center_chan - max_index, 0)
        last_chan = min(center_chan + max_index + 1, nchans)
        lin_offset = scratch[center_chan]
        if tangent:
            nc = last_chan - first_chan
            lin_offset += (np.arange(nc) - nc / 2.0) * tangent_slope
        # Find the maximum height of a function centered on this channel
        # such that it is never higher than the counts in any channel
        f = first_chan - center_chan + max_index
        last = last_chan - center_chan + max_index
        pf = power_funct[f:last] - lin_offset
        height = (scratch[first_chan:last_chan] + pf).min()
        # We now have the function height. Set the background to the
        # height of the maximum function amplitude at each channel
        new_bckgnd = height - pf
        old_bckgnd = bckgnd[first_chan:last_chan]
        np.copyto(old_bckgnd, new_bckgnd, where=(new_bckgnd > old_bckgnd))
    return bckgnd * scale_factor 
def _kk_lookup_table(spectrum, width, exponent, slope, ref_ampl):
    nchans = len(spectrum)
    chan_width = width / (2.0 * slope)
    denom = chan_width**exponent
    indices = np.arange(-nchans, nchans + 1)
    power_funct = indices**exponent * (ref_ampl / denom)
    power_funct = power_funct[power_funct <= 1]
    max_index = len(power_funct) // 2 - 1
    return power_funct, max_index
[docs]
class KajfoszKwiatek(Baseline):
    def __init__(self, top_width=0, bottom_width=50, exponent=2, tangent=False):
        self.top_width = top_width
        self.bottom_width = bottom_width
        self.exponent = exponent
        self.tangent = tangent
    def _fit_one(self, bands, intensities):
        if self.bottom_width > 0:
            return kajfosz_kwiatek_baseline(
                bands,
                intensities,
                self.top_width,
                self.bottom_width,
                self.exponent,
                self.tangent,
            )
        else:
            print("Bottom width must be >0")
            return
[docs]
    def param_ranges(self):
        return {"top_width": (0, 100, "integer"), "bottom_width": (0, 100, "integer")}