# Copyright 2021 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
# Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software.
"""This modules contains two classes for generating synthetic gamma spectra."""
import datetime
import random

import numpy as np
import pandas as pd

from riid.sampleset import SampleSet


class GammaSpectraSynthesizer():
    """Creates a set of synthetic gamma spectra."""
    _supported_purposes = ["train", "test"]
    _supported_functions = ["uniform", "log10", "discrete", "list"]

    def __init__(self, seeds: SampleSet, purpose: str = "train",
                 samples_per_seed: int = 100, background_cps: float = 300.0,
                 background_n_cps: float = 4,
                 subtract_background: bool = True, live_time_function: str = "uniform",
                 live_time_function_args=(0.25, 8.0), snr_function: str = "uniform",
                 snr_function_args=(0.01, 100.0), mixture_size: int = 1,
                 mixture_min_contribution: float = 0.1, random_state: int = None):
        """Constructs a synthetic gamma spectra generator.

        Arguments:
            seeds: the known distributions of counts across all channels such
                that all channels sum to 1. Must contain one seed labeled "background."
            purpose: the intended use of the SampleSet.  Recommended values: "train" or "test".
                "seed" is not a valid purpose for SampleSets generated by this class.
            samples_per_seed: the number of synthetic samples to randomly generate per seed.
            background_cps: the constant rate of gammas from background.
            background_n_cps: the constant rate of neutrons from background.
            subtract_background: if True, generated spectra are foreground-only.
                If False, generated spectra are gross spectra (foreground + background).
            live_time_function: the method of sampling for target live time values.
                Options: uniform; log10; discrete; list.
            live_time_function_args: the range of values which are sampled in the fashion
                specified by the `live_time_function` argument.
            snr_function: the method of sampling for target signal-to-noise ratio values.
                Options: uniform; log10; discrete; list.
            snr_function_args: the range of values which are sampled in the fashion
                specified by the `snr_function` argument.
            mixture_size: the number of seeds to mix together.
            mixture_min_contribution: the minimum ratio of counts that a seed must contribute.
            random_state: the random seed value used to reproduce specific data sets.
        """
        self.seeds = seeds
        self.purpose = purpose
        self.detector = seeds.detector
        self.samples_per_seed = samples_per_seed
        self.background_cps = background_cps
        self.background_n_cps = background_n_cps
        self.subtract_background = subtract_background
        self.live_time_function = live_time_function
        self.live_time_function_args = live_time_function_args
        self.snr_function = snr_function
        self.snr_function_args = snr_function_args
        self.mixture_size = mixture_size
        self.mixture_min_contribution = mixture_min_contribution
        self.random_state = random_state

    def __str__(self):
        output = "SyntheticGenerationConfig()\n"
        for k, v in sorted(vars(self).items()):
            output += "  {}: {}\n".format(k, str(v))
        return output[:-1]

    def __getitem__(self, key):
        item = getattr(self, key)
        return item

    def __setitem__(self, key, value):
        setattr(self, key, value)

    def _get_distribution_values(self, function: str, function_args, n_values: int):
        if function not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(function))

        if function == "uniform":
            value = np.random.uniform(*function_args, size=n_values)
        elif function == "log10":
            log10_args = tuple(map(np.log10, function_args))
            value = np.power(10, np.random.uniform(*log10_args, size=n_values))
        elif function == "discrete":
            value = np.random.choice(function_args, size=n_values)
        elif function == "list":
            value = function_args

        return value

    def _get_live_time_targets(self, n_samples: int) -> list:
        """Obtains a list of random SNR target values."""
        return self._get_distribution_values(
            self.live_time_function,
            self.live_time_function_args,
            n_samples
        )

    def _get_snr_targets(self, n_samples: int) -> list:
        """Obtains a list of random SNR target values."""
        return self._get_distribution_values(
            self.snr_function,
            self.snr_function_args,
            n_samples
        )

    def _generate_mixture_seeds(self, fg_seeds, n_samples, n_per_gamma_rates):
        """Randomly mixes the given foreground seeds into new "mixture seeds".
        """
        mix_size = self.mixture_size
        mix_min = self.mixture_min_contribution
        mix_max = 1 - (mix_min * (mix_size - 1))
        MIX_TRIM = 1e-8
        n_seeds = fg_seeds.n_samples
        n_channels = fg_seeds.n_channels

        mix_indices = np.random.rand(n_samples, n_seeds).argsort(axis=1)[:, :mix_size]
        mix_ratios = np.random.rand(n_samples, mix_size).clip(mix_min+MIX_TRIM, mix_max-MIX_TRIM)
        mix_ratios = mix_ratios / mix_ratios.sum(axis=1)[:, None]
        while (mix_ratios < mix_min).sum():
            mix_ratios = mix_ratios.clip(mix_min+MIX_TRIM, mix_max-MIX_TRIM)
            mix_ratios = mix_ratios / mix_ratios.sum(axis=1)[:, None]
        mix_ratios.sort()

        source_matrix = np.zeros([n_samples, n_seeds])
        seed_combined = np.zeros([n_samples, n_channels])
        n_per_g_rate_combined = np.zeros(n_samples)
        for i, (seed, n_per_g_rate) in enumerate(zip(fg_seeds.spectra.values, n_per_gamma_rates)):
            seed = seed.clip(0)
            seed = seed / seed.sum()
            where = (mix_indices == i).astype(bool)
            rows = where.sum(axis=1).astype(bool)
            how_much = mix_ratios[where]
            n_per_g_source = how_much * n_per_g_rate
            contrib = seed * how_much[:, None]
            seed_combined[rows, :] += contrib
            source_matrix[rows, i] = how_much
            n_per_g_rate_combined[rows] += n_per_g_source
        n_per_g_rate_combined[np.isnan(n_per_g_rate_combined)] = 0

        return seed_combined, source_matrix, n_per_g_rate_combined

    def generate(self, verbose=0):
        """Generate a sample set of gamma spectra from the given config.

        Args:
            verbose: determines the verbosity of status messages.

        Returns:
            A SampleSet of synthetic gamma spectra.

        Raises:
            Exception: raised when the config is invalid.
            EmptySampleSetError: raised when the provided SampleSet is empty.
        """
        if self.random_state:
            random.seed(self.random_state)
            np.random.seed(self.random_state)

        isotope_seeds = self.seeds.get_indices(self.seeds.labels != "background")
        if "total_neutron_counts" in self.seeds.collection_information.columns:
            n_per_gamma_rates = self.seeds.collection_information.total_neutron_counts.values / \
                self.seeds.collection_information.fg_counts.values
        else:
            n_per_gamma_rates = np.zeros(isotope_seeds.n_samples)

        isotopes = isotope_seeds.labels
        background_seed = self.seeds.get_indices(self.seeds.labels == "background")
        first_only = np.full(background_seed.n_samples, False)
        first_only[0] = True
        background_seed = background_seed.get_indices(first_only)
        background_spectrum = background_seed.spectra.iloc[0, :].values
        background_spectrum_clipped = background_spectrum.clip(0)
        background_pdf = background_spectrum_clipped / background_spectrum_clipped.sum()
        n_samples = self.samples_per_seed * isotope_seeds.n_samples

        # Generate samples for each isotope seed
        lt_targets = self._get_live_time_targets(n_samples)
        snr_targets = self._get_snr_targets(n_samples)

        bg_counts_expected = lt_targets * self.background_cps
        fg_counts_expected = snr_targets * bg_counts_expected
        total_counts_expected = bg_counts_expected + fg_counts_expected
        fg_contrib = fg_counts_expected / total_counts_expected
        bg_contrib = bg_counts_expected / total_counts_expected

        seed_combined, source_matrix, n_per_gamma_rate_combined = self._generate_mixture_seeds(
            isotope_seeds,
            n_samples,
            n_per_gamma_rates
        )
        seed_combined = fg_contrib[:, None] * seed_combined
        seed_combined += bg_contrib[:, None] * background_seed.spectra.values[0, :]

        lam = seed_combined * total_counts_expected[:, None]
        gross_spectra = np.random.poisson(lam)
        total_counts = gross_spectra.sum(axis=1)

        bg_neutron_counts_expected = lt_targets * self.background_n_cps
        total_neutron_counts = np.random.poisson(
            total_counts * n_per_gamma_rate_combined + bg_neutron_counts_expected
        )
        excess_from_expected = total_counts - bg_counts_expected
        snr_estimates = excess_from_expected / bg_counts_expected
        sigmas = excess_from_expected / np.sqrt(bg_counts_expected)

        sources = pd.DataFrame(columns=isotopes, data=source_matrix)
        if self.mixture_size > 1:
            sources["label"] = "mixture"
        else:
            sources["label"] = [isotopes[i] for i in source_matrix.argmax(axis=1)]
        ecal = self.seeds.ecal_factors[:, 0]
        info = pd.DataFrame.from_dict({
            "live_time": lt_targets,
            "snr_target": snr_targets,
            "snr_estimate": snr_estimates,
            "bg_counts_expected": bg_counts_expected,
            "fg_counts_expected": fg_counts_expected,
            "total_counts": total_counts,
            "sigma": sigmas,
            "ecal_order_0": np.full(n_samples, ecal[0]),
            "ecal_order_1": np.full(n_samples, ecal[1]),
            "ecal_order_2": np.full(n_samples, ecal[2]),
            "ecal_order_3": np.full(n_samples, ecal[3]),
            "ecal_low_e": np.full(n_samples, ecal[4]),
            "real_time": lt_targets,
            "occupancy_flag": np.full(n_samples, 0),
            "total_neutron_counts": total_neutron_counts,
            "tag": np.full(n_samples, " "),
            "date-time": np.full(n_samples, datetime.datetime.now().strftime("%d-%b-%Y %H:%M:%S.00")),
            "descr": np.full(n_samples, None),
        })
        info.iloc[:, :15] = info.iloc[:, :15].astype(float)

        if self.subtract_background:
            spectra = gross_spectra - background_pdf[None, :] * \
                bg_counts_expected[:, None]
        else:
            spectra = gross_spectra

        ss = SampleSet(
            spectra=pd.DataFrame(spectra),
            collection_information=info,
            sources=sources
        )
        ss.purpose = self.purpose
        ss.measured_or_synthetic = "synthetic"
        ss.detector = self.detector
        ss.energy_bin_centers = ss._get_energy_centers()
        if self.mixture_size == 1:
            ss.relabel_to_max_source()

        return ss

    @property
    def seeds(self) -> SampleSet:
        """
        A SampleSet of non-poisson sampled gamma spectra representing the perfect responses given
        by a specific detector when observing a an isotope for an "sufficiently large" live time.
        Each seed generally represents a single source of radiation, such as K40, Th232, Ba133,
        Y88, etc., however other seeds which incorporate sources + shielding are perfectly valid.
        """
        return self._seeds

    @seeds.setter
    def seeds(self, value: SampleSet):
        if "background" not in value.labels:
            raise ValueError("A seed with the label 'background' must be provided.")
        self._seeds = value

    @property
    def purpose(self) -> str:
        """
        The intended purpose of the SampleSet being generated.
        Used to determine where in the data directory to auto-cache the sample set.
        """
        return self._purpose

    @purpose.setter
    def purpose(self, value: str):
        if value not in self._supported_purposes:
            msg = "Invalid purpose. Supported purposes are: {}".format(self._supported_purposes)
            raise ValueError(msg)
        self._purpose = value

    @property
    def detector(self) -> str:
        """
        The unique name of the physical detector associated with the provided seeds.
        Used to determine where in the data directory to auto-cache the sample set.
        """
        return self._detector

    @detector.setter
    def detector(self, value: str):
        self._detector = value

    @property
    def samples_per_seed(self):
        """The number of samples to create per seed (excluding the background seed)."""
        return self._samples_per_seed

    @samples_per_seed.setter
    def samples_per_seed(self, value: int):
        if not isinstance(value, int):
            raise TypeError("Property 'samples_per_seed' key must be of type 'int'!")

        self._samples_per_seed = value

    @property
    def background_cps(self) -> float:
        """Specifies the counts per second contributed by background radiation."""
        return self._background_cps

    @background_cps.setter
    def background_cps(self, value: float):
        self._background_cps = value

    @property
    def background_n_cps(self) -> float:
        """
        The neutron rate from background.
        """
        return self._background_n_cps

    @background_n_cps.setter
    def background_n_cps(self, value: float):
        self._background_n_cps = value

    @property
    def subtract_background(self) -> bool:
        """Specifies whether or not to include counts from background in the final spectra."""
        return self._subtract_background

    @subtract_background.setter
    def subtract_background(self, value: bool):
        self._subtract_background = value

    @property
    def live_time_function(self) -> str:
        """The function used to randomly sample the desired live time space."""
        return self._live_time_function

    @live_time_function.setter
    def live_time_function(self, value: str):
        if value not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(value))
        self._live_time_function = value

    @property
    def live_time_function_args(self) -> tuple:
        """The live time space to be randomly sampled."""
        return self._live_time_function_args

    @live_time_function_args.setter
    def live_time_function_args(self, value):
        self._live_time_function_args = value

    @property
    def snr_function(self) -> str:
        """The function used to randomly sample the desired signal-to-noise (SNR) ratio space."""
        return self._snr_function

    @snr_function.setter
    def snr_function(self, value: str):
        if value not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(value))
        self._snr_function = value

    @property
    def snr_function_args(self) -> tuple:
        """The signal-to-noise (SNR) space to be randomly sampled."""
        return self._snr_function_args

    @snr_function_args.setter
    def snr_function_args(self, value):
        self._snr_function_args = value

    @property
    def mixture_size(self) -> int:
        """The number of seeds to be used to generate a spectrum's foreground."""
        return self._mixture_size

    @mixture_size.setter
    def mixture_size(self, value: int):
        self._mixture_size = value

    @property
    def mixture_min_contribution(self) -> float:
        """
        The minimum percentage that a seed can contribute to a multi-isotope spectrum's total
        foreground counts.
        """
        return self._mixture_min_contribution

    @mixture_min_contribution.setter
    def mixture_min_contribution(self, value: float):
        self._mixture_min_contribution = value

    @property
    def random_state(self) -> int:
        """
        The seed for the random number generator.
        Used when trying to make reproducible SampleSets.
        """
        return self._random_state

    @random_state.setter
    def random_state(self, value: int):
        self._random_state = value


class PassbySynthesizer():
    """Creates synthetic pass-by events as sequences of gamma spectra."""
    _supported_functions = ["uniform", "log10", "discrete", "list"]

    def __init__(self, seeds: SampleSet, events_per_seed: int = 2, sample_interval: float = 0.125,
                 background_cps: float = 300.0, subtract_background: bool = True,
                 dwell_time_function: str = "uniform", dwell_time_function_args=(0.25, 8.0),
                 fwhm_function: str = "discrete", fwhm_function_args=(1,),
                 snr_function: str = "uniform", snr_function_args=(1.0, 10.0),
                 min_fraction: float = 0.005, random_state: int = None):
        """Constructs a synthetic passy-by generator.

        Arguments:
            seeds: the known distributions of counts across all channels such
                that all channels sum to 1. Must contain one seed labeled "background."
            events_per_seed: the number of events to create per seed.
            sample_interval: the live time of each sampled spectrum comprising the
                entire pass-by event.
            background_cps: the constant rate of gammas from background.
            subtract_background: if True, generated spectra are foreground-only.
                If False, generated spectra are gross spectra (foreground + background).
            dwell_time_function: the method of sampling for target background only time
                to occur after sample pass-by.
                Options: uniform; log10; discrete; list.
            dwell_time_function_args: the range of values which are sampled in the fashion
                specified by the `dwell_time_function` argument.
            fwhm_function: the method of sampling for target full width at half maximum
                for the pass-by event.
                Options: uniform; log10; discrete; list.
            fwhm_function_args: the range of values which are sampled in the fashion
                specified by the `fwhm_function` argument.
            snr_function: the method of sampling for target signal-to-noise ratio values.
                Options: uniform; log10; discrete; list.
            snr_function_args: the range of values which are sampled in the fashion
                specified by the `snr_function` argument.
            min_fraction: the SNR threshold, defined as the percentage of the peak, that
                signifies the beginning and end of the pass-by.
            random_state: the random seed value used to reproduce specific data sets.
        """
        self.seeds = seeds
        self.detector = seeds.detector
        self.events_per_seed = events_per_seed
        self.sample_interval = sample_interval
        self.background_cps = background_cps
        self.subtract_background = subtract_background
        self.dwell_time_function = dwell_time_function
        self.dwell_time_function_args = dwell_time_function_args
        self.fwhm_function = fwhm_function
        self.fwhm_function_args = fwhm_function_args
        self.snr_function = snr_function
        self.snr_function_args = snr_function_args
        self.min_fraction = min_fraction
        self.random_state = random_state

    def __str__(self):
        output = "SyntheticGenerationConfig()"
        for k, v in sorted(vars(self).items()):
            output += "  {}: {}".format(k, str(v))
        return output

    def __getitem__(self, key):
        item = getattr(self, key)
        return item

    def __setitem__(self, key, value):
        setattr(self, key, value)

    def _get_distribution_values(self, function: str, function_args, n_values: int):
        if function not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(function))

        if function == "uniform":
            value = np.random.uniform(*function_args, size=n_values)
        elif function == "log10":
            log10_args = tuple(map(np.log10, function_args))
            value = np.power(10, np.random.uniform(*log10_args, size=n_values))
        elif function == "discrete":
            value = np.random.choice(function_args, size=n_values)
        elif function == "list":
            value = function_args

        return value

    def _get_dwell_time_targets(self, n_samples) -> list:
        """Obtains a list of random dwell time target values."""
        return self._get_distribution_values(
            self.dwell_time_function,
            self.dwell_time_function_args,
            n_samples
        )

    def _get_fwhm_targets(self, n_samples) -> list:
        """Obtains a list of random full-width-half-max (FWHM) target values."""
        return self._get_distribution_values(
            self.fwhm_function,
            self.fwhm_function_args,
            n_samples
        )

    def _get_snr_targets(self, n_samples) -> list:
        """Obtains a list of random SNR target values."""
        return self._get_distribution_values(
            self.snr_function,
            self.snr_function_args,
            n_samples
        )

    def _calculate_passby_shape(self, fwhm):
        """Returns a pass-by shape with maximum of 1 which goes from min_fraction to min_fraction of
        signal with specified fwhm.
        """
        lim = np.sqrt((1-self.min_fraction)/self.min_fraction)
        samples = np.arange(-lim, lim, self.sample_interval / fwhm / 2)
        return 1 / (np.power(samples, 2) + 1)

    def _generate_single_passby(self, fwhm, snr, dwell_time, seed_pdf, background_pdf, source):
        """Generates sampleset with a sequence of spectra representative of a single pass-by event.
        """
        event_snr_targets = self._calculate_passby_shape(fwhm) * snr
        n_event_spectra = len(event_snr_targets)
        dwell_targets = np.zeros(int(dwell_time / self.sample_interval))
        snr_targets = np.concatenate((event_snr_targets, dwell_targets))

        n_samples = len(snr_targets)
        live_times = np.ones(n_samples)

        bg_counts_expected = self.background_cps * live_times
        bg_spectra = np.random.poisson(background_pdf * bg_counts_expected[:, None])
        bg_counts = bg_spectra.sum(axis=1)

        fg_counts_expected = self.background_cps * snr_targets * live_times
        fg_spectra = np.random.poisson(seed_pdf * fg_counts_expected[:, None])
        fg_counts = fg_spectra.sum(axis=1)
        net_spectra = bg_spectra + fg_spectra
        total_counts = bg_counts + fg_counts
        excess_from_expected = total_counts - bg_counts_expected
        snr_estimates = excess_from_expected / bg_counts_expected
        sigmas = excess_from_expected / np.sqrt(bg_counts_expected)

        source_data = np.hstack(
            (np.full([n_samples, 1], 1), np.full([n_samples, 1], source))
        )
        sources = pd.DataFrame(columns=[source, "label"], data=source_data)

        collection_information = pd.DataFrame(
                data=np.vstack((
                    live_times,
                    snr_targets,
                    snr_estimates,
                    bg_counts,
                    fg_counts,
                    bg_counts_expected,
                    total_counts,
                    sigmas)
                ).T,
                columns=[
                    "live_time",
                    "snr_target",
                    "snr_estimate",
                    "bg_counts",
                    "fg_counts",
                    "bg_counts_expected",
                    "total_counts",
                    "sigma"
                ]
            )

        if self.subtract_background:
            spectra = net_spectra - bg_spectra
        else:
            spectra = net_spectra
        comments = {
            "fwhm": fwhm,
            "snr": snr,
            "dwell_time": dwell_time,
            "event_length": n_event_spectra,
            "total_length": n_samples,
            "source": source
        }
        ss = SampleSet(
            spectra=pd.DataFrame(spectra),
            collection_information=collection_information,
            sources=sources,
            comments=comments
        )
        ss.detector = self.detector
        ss.purpose = "passby"
        ss.measured_or_synthetic = "synthetic"
        ss.energy_bin_centers = ss._get_energy_centers()

        return ss

    def generate(self, verbose=0):
        """Generate a list of sample sets where each represents a pass-by event.

        Args:
            verbose: determines the verbosity of status messages.

        Returns:
            A list of SampleSets where each SampleSet represents a pass-by event.

        Raises:
            None
        """
        if self.random_state:
            random.seed(self.random_state)
            np.random.seed(self.random_state)

        isotope_seeds = self.seeds.get_indices(self.seeds.labels != "background")
        background_seed = self.seeds.get_indices(self.seeds.labels == "background")
        bs_indices = np.full(background_seed.n_samples, False)
        bs_indices[0] = True
        background_seed = background_seed.get_indices(bs_indices)
        background_spectrum = background_seed.spectra.iloc[0, :].values
        background_pdf = background_spectrum.clip(0)
        background_pdf = background_pdf / background_pdf.sum()

        # Generate samples for each seed
        args = []
        for i, _ in enumerate(isotope_seeds.spectra.index):
            seed_spectrum = isotope_seeds.spectra.iloc[i, :].values
            source = isotope_seeds.labels[i]
            seed_pdf = seed_spectrum.clip(0)
            seed_pdf = np.array(seed_pdf) / seed_pdf.sum()
            n_samples = self.events_per_seed
            if isinstance(self.events_per_seed, dict):
                n_samples = self.events_per_seed[source]
            fwhm_targets = self._get_fwhm_targets(n_samples)
            snr_targets = self._get_snr_targets(n_samples)
            dwell_time_targets = self._get_dwell_time_targets(n_samples)
            for fwhm, snr, dwell in zip(fwhm_targets, snr_targets, dwell_time_targets):
                args.append((fwhm, snr, dwell, seed_pdf, background_pdf, source))

        events = [self._generate_single_passby(*a) for a in args]
        return events

    @property
    def seeds(self) -> SampleSet:
        """
        A SampleSet of non-poisson sampled gamma spectra representing the perfect responses given
        by a specific detector when observing a an isotope for an "sufficiently large" live time.
        Each seed generally represents a single source of radiation, such as K40, Th232, Ba133,
        Y88, etc., however other seeds which incorporate sources + shielding are perfectly valid.
        """
        return self._seeds

    @seeds.setter
    def seeds(self, value: SampleSet):
        if "background" not in value.source_types:
            raise ValueError("A seed with the label 'background' must be provided.")
        self._seeds = value

    @property
    def detector(self) -> str:
        """
        The unique name of the physical detector associated with the provided seeds.
        Used to determine where in the data directory to auto-cache the sample set.
        """
        return self._detector

    @detector.setter
    def detector(self, value: str):
        self._detector = value

    @property
    def sample_interval(self) -> float:
        """The sample interval (in seconds) at which the events are simulated."""
        return self._sample_interval

    @sample_interval.setter
    def sample_interval(self, value: float):
        self._sample_interval = value

    @property
    def events_per_seed(self):
        """The number of samples to create per seed (excluding the background seed)."""
        return self._events_per_seed

    @events_per_seed.setter
    def events_per_seed(self, value):
        if not isinstance(value, int) and not isinstance(value, dict):
            raise TypeError("Property 'events_per_seed' key must be of type 'int' or 'dict'!")

        self._events_per_seed = value

    @property
    def background_cps(self) -> float:
        """Specifies the counts per second contributed by background radiation."""
        return self._background_cps

    @background_cps.setter
    def background_cps(self, value: float):
        self._background_cps = value

    @property
    def subtract_background(self) -> bool:
        """Specifies whether or not to include counts from background in the final spectra."""
        return self._subtract_background

    @subtract_background.setter
    def subtract_background(self, value: bool):
        self._subtract_background = value

    @property
    def dwell_time_function(self) -> str:
        """The function used to randomly sample the desired dwell time space."""
        return self._dwell_time_function

    @dwell_time_function.setter
    def dwell_time_function(self, value: str):
        if value not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(value))
        self._dwell_time_function = value

    @property
    def dwell_time_function_args(self) -> tuple:
        """The dwell time space to be randomly sampled."""
        return self._dwell_time_function_args

    @dwell_time_function_args.setter
    def dwell_time_function_args(self, value):
        self._dwell_time_function_args = value

    @property
    def fwhm_function(self) -> str:
        """The function used to randomly sample the desired full-width-half-max (FWHM) ratio space.
        """
        return self._fwhm_function

    @fwhm_function.setter
    def fwhm_function(self, value: str):
        if value not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(value))
        self._fwhm_function = value

    @property
    def fwhm_function_args(self) -> tuple:
        """The full-width-half-max (FWHM) space to be randomly sampled."""
        return self._fwhm_function_args

    @fwhm_function_args.setter
    def fwhm_function_args(self, value):
        self._fwhm_function_args = value

    @property
    def snr_function(self) -> str:
        """The function used to randomly sample the desired signal-to-noise (SNR) ratio space."""
        return self._snr_function

    @snr_function.setter
    def snr_function(self, value: str):
        if value not in self._supported_functions:
            raise ValueError("{} is not a valid function.".format(value))
        self._snr_function = value

    @property
    def snr_function_args(self) -> tuple:
        """The signal-to-noise (SNR) space to be randomly sampled."""
        return self._snr_function_args

    @snr_function_args.setter
    def snr_function_args(self, value):
        self._snr_function_args = value

    @property
    def min_fraction(self) -> float:
        """Specifies the percentage of the peak amplitude to exclude."""
        return self._min_fraction

    @min_fraction.setter
    def min_fraction(self, value: float):
        self._min_fraction = value

    @property
    def random_state(self) -> int:
        """
        The seed for the random number generator.
        Used when trying to make reproducible SampleSets.
        """
        return self._random_state

    @random_state.setter
    def random_state(self, value: int):
        self._random_state = value
