예제 #1
0
    def test_iteration(self, test_name: str):
        """Test that IonizationState instances iterate impeccably."""
        try:
            states = [state for state in self.instances[test_name]]
        except Exception:
            raise AtomicError(f"Unable to perform iteration for {test_name}.")

        try:
            integer_charges = [state.integer_charge for state in states]
            ionic_fractions = np.array(
                [state.ionic_fraction for state in states])
            ionic_symbols = [state.ionic_symbol for state in states]
        except Exception:
            raise AtomicError("An attribute may be misnamed or missing.")

        try:
            base_symbol = isotope_symbol(ionic_symbols[0])
        except InvalidIsotopeError:
            base_symbol = atomic_symbol(ionic_symbols[0])
        finally:
            atomic_numb = atomic_number(ionic_symbols[1])

        errors = []

        expected_charges = np.arange(atomic_numb + 1)
        if not np.all(integer_charges == expected_charges):
            errors.append(
                f"The resulting integer charges are {integer_charges}, "
                f"which are not equal to the expected integer charges, "
                f"which are {expected_charges}.")

        expected_fracs = test_cases[test_name]['ionic_fractions']
        if isinstance(expected_fracs, u.Quantity):
            expected_fracs = (expected_fracs / expected_fracs.sum()).value

        if not np.allclose(ionic_fractions, expected_fracs):
            errors.append(
                f"The resulting ionic fractions are {ionic_fractions}, "
                f"which are not equal to the expected ionic fractions "
                f"of {expected_fracs}.")

        expected_particles = [
            Particle(base_symbol, Z=charge) for charge in integer_charges
        ]
        expected_symbols = [
            particle.ionic_symbol for particle in expected_particles
        ]
        if not ionic_symbols == expected_symbols:
            errors.append(
                f"The resulting ionic symbols are {ionic_symbols}, "
                f"which are not equal to the expected ionic symbols of "
                f"{expected_symbols}.")

        if errors:
            errors.insert(
                0, (f"The test of IonizationState named '{test_name}' has "
                    f"resulted in the following errors when attempting to "
                    f"iterate."))
            errmsg = " ".join(errors)
            raise AtomicError(errmsg)
예제 #2
0
 def _particle_instances(self) -> List[Particle]:
     """
     Return a list of the `~plasmapy.atomic.Particle` class
     instances corresponding to each ion.
     """
     return [
         Particle(self._particle_instance.particle, Z=i)
         for i in range(self.atomic_number + 1)
     ]
예제 #3
0
    def test_keys(self, test):
        input_keys = tests[test]['inputs'].keys()
        particles = [Particle(input_key) for input_key in input_keys]
        expected_keys = [p.particle for p in particles]
        actual_keys = [
            key for key in self.instances[test].ionic_fractions.keys()
        ]

        assert actual_keys == expected_keys, (
            f"For test='{test}', the following should be equal:\n"
            f"  actual_keys = {actual_keys}\n"
            f"expected_keys = {expected_keys}")
예제 #4
0
    def __getitem__(self, value) -> State:
        """Return information for a single ionization level."""
        if isinstance(value, slice):
            raise TypeError("IonizationState instances cannot be sliced.")

        if isinstance(value, Integral) and 0 <= value <= self.atomic_number:
            result = State(
                value,
                self.ionic_fractions[value],
                self.ionic_symbols[value],
                self.number_densities[value],
            )
        else:
            if not isinstance(value, Particle):
                try:
                    value = Particle(value)
                except InvalidParticleError as exc:
                    raise InvalidParticleError(
                        f"{value} is not a valid integer charge or "
                        f"particle.") from exc

            same_element = value.element == self.element
            same_isotope = value.isotope == self.isotope
            has_charge_info = value.is_category(
                any_of=["charged", "uncharged"])

            if same_element and same_isotope and has_charge_info:
                Z = value.integer_charge
                result = State(
                    Z,
                    self.ionic_fractions[Z],
                    self.ionic_symbols[Z],
                    self.number_densities[Z],
                )
            else:
                if not same_element or not same_isotope:
                    raise AtomicError("Inconsistent element or isotope.")
                elif not has_charge_info:
                    raise ChargeError("No integer charge provided.")
        return result
예제 #5
0
 def test_particle_instances(self, test_name):
     """
     Test that `IonizationState` returns the correct `Particle`
     instances.
     """
     instance = self.instances[test_name]
     atom = instance.base_particle
     nstates = instance.atomic_number + 1
     expected_particles = [Particle(atom, Z=Z) for Z in range(nstates)]
     assert expected_particles == instance._particle_instances, (
         f"The expected Particle instances of {expected_particles} "
         f"are not all equal to the IonizationState particles of "
         f"{instance._particle_instances} for test {test_name}.")
예제 #6
0
    def test_that_ionic_fractions_are_set_correctly(self, test_name):

        errmsg = ""

        elements_actual = self.instances[test_name].base_particles
        inputs = tests[test_name]["inputs"]

        if isinstance(inputs, dict):
            input_keys = list(tests[test_name]["inputs"].keys())

            input_keys = sorted(input_keys,
                                key=lambda k: (atomic_number(k), mass_number(k)
                                               if Particle(k).isotope else 0))

            for element, input_key in zip(elements_actual, input_keys):
                expected = tests[test_name]["inputs"][input_key]

                if isinstance(expected, u.Quantity):
                    expected = np.array(expected.value /
                                        np.sum(expected.value))

                actual = self.instances[test_name].ionic_fractions[element]

                if not np.allclose(actual, expected):
                    errmsg += (
                        f"\n\nThere is a discrepancy in ionic fractions for "
                        f"({test_name}, {element}, {input_key})\n"
                        f"  expected = {expected}\n"
                        f"    actual = {actual}")

                if not isinstance(actual, np.ndarray) or isinstance(
                        actual, u.Quantity):
                    raise AtomicError(
                        f"\n\nNot a numpy.ndarray: ({test_name}, {element})")
        else:
            elements_expected = {
                particle_symbol(element)
                for element in inputs
            }

            assert set(
                self.instances[test_name].base_particles) == elements_expected

            for element in elements_expected:
                assert all(
                    np.isnan(
                        self.instances[test_name].ionic_fractions[element]))
        if errmsg:
            pytest.fail(errmsg)
예제 #7
0
    def test_that_particles_were_set_correctly(self, test_name):
        input_particles = tests[test_name]['inputs'].keys()
        particles = [
            Particle(input_particle) for input_particle in input_particles
        ]
        expected_particles = {p.particle for p in particles}
        actual_particles = {
            particle
            for particle in self.instances[test_name].ionic_fractions.keys()
        }

        assert actual_particles == expected_particles, (
            f"For test='{test_name}', the following should be equal:\n"
            f"  actual_particles = {actual_particles}\n"
            f"expected_particles = {expected_particles}")
예제 #8
0
    def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
        """
        Set the ionic fractions.

        Notes
        -----
        The ionic fractions are initialized during instantiation of
        `~plasmapy.atomic.IonizationStates`.  After this, the only way
        to reset the ionic fractions via the ``ionic_fractions``
        attribute is via a `dict` with elements or isotopes that are a
        superset of the previous elements or isotopes.  However, you may
        use item assignment of the `~plasmapy.atomic.IonizationState`
        instance to assign new ionic fractions one element or isotope
        at a time.

        Raises
        ------
        AtomicError
            If the ionic fractions cannot be set.

        TypeError
            If ``inputs`` is not a `list`, `tuple`, or `dict` during
            instantiation, or if ``inputs`` is not a `dict` when it is
            being set.

        """

        # A potential problem is that using item assignment on the
        # ionic_fractions attribute could cause the original attributes
        # to be overwritten without checks being performed.  We might
        # eventually want to create a new class or subclass of UserDict
        # that goes through these checks.  In the meantime, we should
        # make it clear to users to set ionic_fractions by using item
        # assignment on the IonizationStates instance as a whole.  An
        # example of the problem is `s = IonizationStates(["He"])` being
        # followed by `s.ionic_fractions["He"] = 0.3`.

        if hasattr(self, '_ionic_fractions'):
            if not isinstance(inputs, dict):
                raise TypeError(
                    "Can only reset ionic_fractions with a dict if "
                    "ionic_fractions has been set already.")
            old_particles = set(self.base_particles)
            new_particles = {particle_symbol(key) for key in inputs.keys()}
            missing_particles = old_particles - new_particles
            if missing_particles:
                raise AtomicError(
                    "Can only reset ionic fractions with a dict if "
                    "the new base particles are a superset of the "
                    "prior base particles.  To change ionic fractions "
                    "for one base particle, use item assignment on the "
                    "IonizationStates instance instead.")

        if isinstance(inputs, dict):
            original_keys = inputs.keys()
            ionfrac_types = {type(inputs[key]) for key in original_keys}
            inputs_have_quantities = u.Quantity in ionfrac_types

            if inputs_have_quantities and len(ionfrac_types) != 1:
                raise TypeError(
                    "Ionic fraction information may only be inputted "
                    "as a Quantity object if all ionic fractions are "
                    "Quantity arrays with units of inverse volume.")

            try:
                particles = {key: Particle(key) for key in original_keys}
            except (InvalidParticleError, TypeError) as exc:
                raise AtomicError(
                    "Unable to create IonizationStates instance "
                    "because not all particles are valid.") from exc

            # The particles whose ionization states are to be recorded
            # should be elements or isotopes but not ions or neutrals.

            for key in particles.keys():
                is_element = particles[key].is_category('element')
                has_charge_info = particles[key].is_category(
                    any_of=["charged", "uncharged"])

                if not is_element or has_charge_info:
                    raise AtomicError(
                        f"{key} is not an element or isotope without "
                        f"charge information.")

            # We are sorting the elements/isotopes by atomic number and
            # mass number since we will often want to plot and analyze
            # things and this is the most sensible order.

            sorted_keys = sorted(original_keys,
                                 key=lambda k: (
                                     particles[k].atomic_number,
                                     particles[k].mass_number
                                     if particles[k].isotope else 0,
                                 ))

            _elements_and_isotopes = []
            _particle_instances = []
            new_ionic_fractions = {}

            if inputs_have_quantities:
                n_elems = {}

            for key in sorted_keys:
                new_key = particles[key].particle
                _particle_instances.append(particles[key])
                if new_key in _elements_and_isotopes:
                    raise AtomicError(
                        "Repeated particles in IonizationStates.")

                nstates_input = len(inputs[key])
                nstates = particles[key].atomic_number + 1
                if nstates != nstates_input:
                    raise AtomicError(
                        f"The ionic fractions array for {key} must "
                        f"have a length of {nstates}.")

                _elements_and_isotopes.append(new_key)
                if inputs_have_quantities:
                    try:
                        number_densities = inputs[key].to(u.m**-3)
                        n_elem = np.sum(number_densities)
                        new_ionic_fractions[new_key] = np.array(
                            number_densities / n_elem)
                        n_elems[key] = n_elem
                    except u.UnitConversionError as exc:
                        raise AtomicError(
                            "Units are not inverse volume.") from exc
                elif isinstance(inputs[key],
                                np.ndarray) and inputs[key].dtype.kind == 'f':
                    new_ionic_fractions[particles[key].particle] = inputs[key]
                else:
                    try:
                        new_ionic_fractions[particles[key].particle] = \
                            np.array(inputs[key], dtype=np.float)
                    except ValueError as exc:
                        raise AtomicError(
                            f"Inappropriate ionic fractions for {key}."
                        ) from exc

            for key in _elements_and_isotopes:
                fractions = new_ionic_fractions[key]
                if not np.all(np.isnan(fractions)):
                    if np.min(fractions) < 0 or np.max(fractions) > 1:
                        raise AtomicError(
                            f"Ionic fractions for {key} are not between 0 and 1."
                        )
                    if not np.isclose(
                            np.sum(fractions), 1, atol=self.tol, rtol=0):
                        raise AtomicError(
                            f"Ionic fractions for {key} are not normalized to 1."
                        )

            # When the inputs provide the densities, the abundances must
            # not have been provided because that would be redundant
            # or contradictory information.  The number density scaling
            # factor might or might not have been provided.  Have the
            # number density scaling factor default to the total number
            # of neutrals and ions across all elements and isotopes, if
            # it was not provided.  Then go ahead and calculate the
            # abundances based on that.  However, we need to be careful
            # that the abundances are not overwritten during the
            # instantiation of the class.

            if inputs_have_quantities:
                if np.isnan(self.n):
                    new_n = 0 * u.m**-3
                    for key in _elements_and_isotopes:
                        new_n += n_elems[key]
                    self.n = new_n

                new_abundances = {}
                for key in _elements_and_isotopes:
                    new_abundances[key] = np.float(n_elems[key] / self.n)

                self._pars['abundances'] = new_abundances

        elif isinstance(inputs, (list, tuple)):

            try:
                _particle_instances = [
                    Particle(particle) for particle in inputs
                ]
            except (InvalidParticleError, TypeError) as exc:
                raise AtomicError(
                    "Invalid inputs to IonizationStates.") from exc

            _particle_instances.sort(key=lambda p: (
                p.atomic_number, p.mass_number if p.isotope else 0))
            _elements_and_isotopes = [
                particle.particle for particle in _particle_instances
            ]
            new_ionic_fractions = {
                particle.particle: np.full(particle.atomic_number + 1,
                                           fill_value=np.nan,
                                           dtype=np.float64)
                for particle in _particle_instances
            }
        else:
            raise TypeError("Incorrect inputs to set ionic_fractions.")

        for i in range(1, len(_particle_instances)):
            if _particle_instances[
                    i - 1].element == _particle_instances[i].element:
                if not _particle_instances[
                        i - 1].isotope and _particle_instances[i].isotope:
                    raise AtomicError(
                        "Cannot have an element and isotopes of that element.")

        self._particle_instances = _particle_instances
        self._base_particles = _elements_and_isotopes
        self._ionic_fractions = new_ionic_fractions
예제 #9
0
def swept_probe_analysis(probe_characteristic,
                         probe_area,
                         gas_argument,
                         bimaxwellian=False,
                         visualize=False,
                         plot_electron_fit=False,
                         plot_EEDF=False):
    r"""Attempt to perform a basic swept probe analysis based on the provided
    characteristic and probe data. Suitable for single cylindrical probes in
    low-pressure DC plasmas, since OML is applied.

    Parameters
    ----------
    probe_characteristic : ~plasmapy.diagnostics.langmuir.Characteristic
        The swept probe characteristic that is to be analyzed.

    probe_area : ~astropy.units.Quantity
        The area of the probe exposed to plasma in units convertible to m^2.

    gas_argument : argument to instantiate the `Particle` class.
        `str`, `int`, or `~plasmapy.atomic.Particle`
        A string representing a particle, element, isotope, or ion; an
        integer representing the atomic number of an element; or a
        `Particle` instance.

    visualize : bool, optional
        Can be used to plot the characteristic and the obtained parameters.
        Default is False.

    plot_electron_fit : bool, optional
        If True, the fit of the electron current in the exponential section is
        shown. Default is False.

    plot_EEDF : bool, optional
        If True, the EEDF is computed and shown. Default is False.

    Returns
    -------

    Results are returned as Dictionary

    "T_e" : `astropy.units.Quantity`
        Best estimate of the electron temperature in units of eV. Contains
        two values if bimaxwellian is True.

    "n_e" : `astropy.units.Quantity`
        Estimate of the electron density in units of m^-3. See the Notes on
        plasma densities.

    "n_i" : `astropy.units.Quantity`
        Estimate of the ion density in units of m^-3. See the Notes on
        plasma densities.

    "n_i_OML" : `astropy.units.Quantity`
        OML-theory estimate of the ion density in units of m^-3. See the Notes
        on plasma densities.

    "V_F" : `astropy.units.Quantity`
        Estimate of the floating potential in units of V.

    "V_P" : `astropy.units.Quantity`
        Estimate of the plasma potential in units of V.

    "I_es" : `astropy.units.Quantity`
        Estimate of the electron saturation current in units of Am^-2.

    "I_is" : `astropy.units.Quantity`
        Estimate of the ion saturation current in units of Am^-2.

    "hot_fraction" : float
        Estimate of the total hot (energetic) electron fraction.

    Notes
    -----
    This function combines the separate probe analysis functions into a single
    analysis. Results are returned as a Dictionary. On plasma densities: in an
    ideal quasi-neutral plasma all densities should be equal. However, in
    practice this will not be the case. The electron density is the poorest
    estimate due to the hard to obtain knee in the electron current. The
    density provided by OML theory is likely the best estimate as it is not
    dependent on the obtained electron temperature, given that the conditions
    for OML theory hold.

    """
    # Instantiate gas using the Particle class
    gas = Particle(argument=gas_argument)

    # Check (unit) validity of the probe characteristic
    probe_characteristic.check_validity()

    # Obtain the plasma and floating potentials
    V_P = get_plasma_potential(probe_characteristic)
    V_F = get_floating_potential(probe_characteristic)

    # Obtain the electron and ion saturation currents
    I_es = get_electron_saturation_current(probe_characteristic)
    I_is = get_ion_saturation_current(probe_characteristic)

    # The OML method is used to obtain an ion density without knowing the
    # electron temperature. This can then be used to obtain the ion current
    # and subsequently a better electron current fit.
    n_i_OML, fit = get_ion_density_OML(probe_characteristic,
                                       probe_area,
                                       gas,
                                       return_fit=True)

    ion_current = extrapolate_ion_current_OML(probe_characteristic, fit)

    # First electron temperature iteration
    exponential_section = extract_exponential_section(probe_characteristic,
                                                      ion_current=ion_current)
    T_e, hot_fraction = get_electron_temperature(exponential_section,
                                                 bimaxwellian=bimaxwellian,
                                                 return_hot_fraction=True)

    # Second electron temperature iteration, using an electron temperature-
    # adjusted exponential section
    exponential_section = extract_exponential_section(probe_characteristic,
                                                      T_e=T_e,
                                                      ion_current=ion_current)
    T_e, hot_fraction, fit = get_electron_temperature(
        exponential_section,
        bimaxwellian=bimaxwellian,
        visualize=plot_electron_fit,
        return_fit=True,
        return_hot_fraction=True)

    # Extrapolate the fit of the exponential section to obtain the full
    # electron current. This has no use in the analysis except for
    # visualization.
    electron_current = extrapolate_electron_current(probe_characteristic,
                                                    fit,
                                                    bimaxwellian=bimaxwellian)

    # Using a good estimate of electron temperature, obtain the ion and
    # electron densities from the saturation currents.
    n_i = get_ion_density_LM(
        I_is, reduce_bimaxwellian_temperature(T_e, hot_fraction), probe_area,
        gas.mass)
    n_e = get_electron_density_LM(
        I_es, reduce_bimaxwellian_temperature(T_e, hot_fraction), probe_area)

    if visualize:  # coverage: ignore
        with quantity_support():
            fig, (ax1, ax2) = plt.subplots(2, 1)
            ax1.plot(probe_characteristic.bias,
                     probe_characteristic.current,
                     marker='.',
                     color='k',
                     linestyle='',
                     label="Probe current")
            ax1.set_title("Probe characteristic")
            ax2.set_ylim(probe_characteristic.get_padded_limit(0.1))

            ax2.plot(probe_characteristic.bias,
                     np.abs(probe_characteristic.current),
                     marker='.',
                     color='k',
                     linestyle='',
                     label="Probe current")
            ax2.set_title("Logarithmic")
            ax2.set_ylim(probe_characteristic.get_padded_limit(0.1, log=True))

            ax1.axvline(x=V_P.value, color='gray', linestyle='--')
            ax1.axhline(y=I_es.value, color='grey', linestyle='--')
            ax1.axvline(x=V_F.value, color='k', linestyle='--')
            ax1.axhline(y=I_is.value, color='r', linestyle='--')
            ax1.plot(ion_current.bias,
                     ion_current.current,
                     c='y',
                     label="Ion current")
            ax1.plot(electron_current.bias,
                     electron_current.current,
                     c='c',
                     label="Electron current")
            tot_current = ion_current + electron_current
            ax1.plot(tot_current.bias, tot_current.current, c='g')

            ax2.axvline(x=V_P.value, color='gray', linestyle='--')
            ax2.axhline(y=I_es.value, color='grey', linestyle='--')
            ax2.axvline(x=V_F.value, color='k', linestyle='--')
            ax2.axhline(y=np.abs(I_is.value), color='r', linestyle='--')
            ax2.plot(ion_current.bias,
                     np.abs(ion_current.current),
                     label="Ion current",
                     c='y')
            ax2.plot(electron_current.bias,
                     np.abs(electron_current.current),
                     label="Electron current",
                     c='c')
            ax2.plot(tot_current.bias, np.abs(tot_current.current), c='g')
            ax2.set_yscale("log", nonposy='clip')
            ax1.legend(loc='best')
            ax2.legend(loc='best')

            fig.tight_layout()

    # Obtain and show the EEDF. This is only useful if the characteristic data
    # has been preprocessed to be sufficiently smooth and noiseless.
    if plot_EEDF:  # coverage: ignore
        get_EEDF(probe_characteristic, visualize=True)

    # Compile the results dictionary
    results = {
        'V_P': V_P,
        'V_F': V_F,
        'I_es': I_es,
        'I_is': I_is,
        'n_e': n_e,
        'n_i': n_i,
        'T_e': T_e,
        'n_i_OML': n_i_OML
    }

    if bimaxwellian:
        results['hot_fraction'] = hot_fraction

    return results
예제 #10
0
def get_ion_density_OML(probe_characteristic,
                        probe_area,
                        gas,
                        visualize=False,
                        return_fit=False):
    r"""Implement the Orbital Motion Limit (OML) method of obtaining an
    estimate of the ion density.

    Parameters
    ----------
    probe_characteristic : ~plasmapy.diagnostics.langmuir.Characteristic
        The swept probe characteristic that is to be analyzed.

    probe_area : ~astropy.units.Quantity
        The area of the probe exposed to plasma in units convertible to m^2.

    gas : ~astropy.units.Quantity
        The (mean) mass of the background gas in atomic mass units.

    visualize : bool, optional
        If True a plot of the OML fit is shown. Default is False.

    return_fit: bool, optional
        If True the parameters of the fit will be returned in addition to the
        ion density. Default is False.

    Returns
    -------
    n_i_OML : ~astropy.units.Quantity
        Estimated ion density in m^-3.

    Notes
    -----
    The method implemented in this function holds for cylindrical probes in a
    cold ion plasma, ie. :math:T_i=0` eV. With OML theory an expression is found
    for the ion current as function of probe bias independent of the electron
    temperature [mott-smith.langmuir-1926]_:

    .. math::
        I_i \xrightarrow[T_i = 0]{} A_p n_i e \frac{\sqrt{2}}{\pi}
        \sqrt{\frac{e \left( V_F - V \right)}{m_i}}

    References
    ----------
    .. [mott-smith.langmuir-1926] H. M. Mott-Smith, I. Langmuir,
        Phys. Rev. 28, 727-763 (Oct. 1926)

    """

    probe_characteristic.check_validity()

    ion_section = extract_ion_section(probe_characteristic)

    fit = np.polyfit(
        ion_section.bias.to(u.V).value,
        ion_section.current.to(u.mA).value**2, 1)

    poly = np.poly1d(fit)

    slope = fit[0]

    ion = Particle(argument=gas)

    n_i_OML = np.sqrt(-slope * u.mA**2 / u.V * np.pi**2 * ion.mass /
                      (probe_area**2 * const.e**3 * 2))

    if visualize:  # coverage: ignore
        with quantity_support():
            plt.figure()
            plt.scatter(ion_section.bias.to(u.V),
                        ion_section.current.to(u.mA)**2,
                        color='k',
                        marker='.')
            plt.plot(ion_section.bias.to(u.V),
                     poly(ion_section.bias.to(u.V).value),
                     c='g')
            plt.title("OML fit")
            plt.tight_layout()

    if return_fit:
        return n_i_OML.to(u.m**-3), fit

    return n_i_OML.to(u.m**-3)
예제 #11
0
    ],
    [
        return_quantity, (24), {
            'should_warn': False
        }, (5 * u.m / u.s, UserWarning), MissingWarningError
    ],
    [return_arg, u.kg / u.K, {}, u.kg / u.K, None],
    [return_arg, u.kg / u.K, {}, u.kg / u.N, u.UnitsError],
    [return_arg, u.kg, {}, u.g, u.UnitsError],
    [return_arg, u.C, {
        'should_warn': True
    }, (u.C, UserWarning), None],
    [adams_number, 1, {
        'x': 1
    }, u.pc, u.UnitsError],
    [return_arg, Particle('p+'), {},
     Particle('proton'), None],
    [return_arg,
     Particle('e+'), {},
     Particle('e-'), UnexpectedResultError],
    [return_arg, Particle('mu+'), {}, type, InconsistentTypeError],
    [return_arg, (2, ), {}, IOError, MissingExceptionError],
]


@pytest.mark.parametrize(
    "f, args, kwargs, expected, whaterror",
    f_args_kwargs_expected_whaterror,
)
def test_run_test(f, args, kwargs, expected, whaterror):
    """