Source code for sdt.config

# SPDX-FileCopyrightText: 2020 Lukas Schrangl <lukas.schrangl@tuwien.ac.at>
#
# SPDX-License-Identifier: BSD-3-Clause

"""Mechanism for getting and setting default function parameters
=============================================================

Typically, :py:class:`pandas.DataFrames` containing single molecule
localization data would have x coordinates in the "x" column, y coordinates in
the y column, the total intensity in the "mass" column and so on. Sometimes,
this is however not the case, e.g. when multiple DataFrames have been
concatenated using a MultiIndex. In that case, it is necessary to be able
to tell a function that takes the DataFrame as an input, that it has to look
for the x coordinate e.g. in the ``("channel1", "x")`` column.

The :py:mod:`sdt.config` module contains function decorators that provide
sensible default values (e.g. ``["x", "y"]`` for coordinate columns), which can
be changed by the user. There exist the :py:func:`set_columns` decorator which
is used for setting DataFrame column names and teh :py:func:`use_defaults`
decorator, which for all other kind of default arguments.

:py:func:`set_columns` gets its defaults for :py:attr:`columns`, which can
be changed by the user for a global effect. Similarly, :py:func:`use_defaults`
reads :py:attr:`rc`.


Examples
--------

Define a function that will take the DataFrame column names from the
`column` argument:

>>> @set_columns
... def get_mass(data, columns={}):
...     return data[columns["mass"]]

Thanks to :py:func:`set_columns`, the `columns` dict will have sensible
default values (which can be changed globally by the user by setting the
corresponding items in :py:attr:`columns`). Additionally, any user of the
`get_mass` function can override the column names when calling the function.


Programming reference
---------------------

.. autofunction:: set_columns
.. autofunction:: use_defaults
.. autodata:: columns
.. autodata:: rc
"""
import inspect
import functools


rc = dict(channel_names=["channel1", "channel2"],)
"""Global config dictionary"""


columns = dict(
    coords=["x", "y"],
    time="frame",
    mass="mass",
    signal="signal",
    bg="bg",
    bg_dev="bg_dev",
    particle="particle")
"""Default column names in :py:class:`pandas.DataFrame`"""


[docs]def use_defaults(func): """Decorator to apply default values to functions If any function argument whose name is a key in :py:attr:`rc` is `None`, set its value to what is specified in :py:attr:`rc`. Parameters ---------- func : function Function to be decorated Returns ------- function Modified function Examples -------- >>> @use_defaults ... def f(channel_names=None): ... return channel_names >>> ['channel1', 'channel2'] ['channel1', 'channel2'] >>> f() ['channel1', 'channel2'] >>> f(["ch1", "ch2", "ch3"]) ['ch1', 'ch2', 'ch3'] >>> config.rc["channel_names"] = ["channel4"] >>> f() ['channel4'] """ sig = inspect.signature(func) @functools.wraps(func) def wrapper(*args, **kwargs): ba = sig.bind(*args, **kwargs) ba.apply_defaults() for name, value in ba.arguments.items(): if value is None: ba.arguments[name] = rc.get(name, None) return func(*ba.args, **ba.kwargs) wrapper.__signature__ = sig return wrapper
[docs]def set_columns(func): """Decorator to set default column names for DataFrames Use this on functions that accept a dict as the `columns` argument. Values from :py:attr:`columns` will be added for any key not present in the dict argument. This is intended as a way to be able to use functions on DataFrames with non-standard column names. Parameters ---------- func : function Function to be decorated Returns ------- function Modified function Examples -------- Create some data: >>> a = numpy.arange(6).reshape((-1, 2)) >>> df = pandas.DataFrame(a, columns=["mass", "other_mass"]) >>> df mass other_mass 0 0 1 1 2 3 2 4 5 Example function which should return the "mass" column from a single molecule data DataFrame: >>> @set_columns ... def get_mass(data, columns={}): ... return data[columns["mass"]] >>> get_mass(df) 0 0 1 2 2 4 Name: mass, dtype: int64 However, if for some reason the "other_mass" column should be used instead, this can be achieved by >>> get_mass(df, columns={"mass": "other_mass"}) 0 1 1 3 2 5 Name: other_mass, dtype: int64 """ sig = inspect.signature(func) @functools.wraps(func) def wrapper(*args, **kwargs): ba = sig.bind(*args, **kwargs) ba.apply_defaults() cols = columns.copy() cols.update(ba.arguments["columns"]) ba.arguments["columns"] = cols return func(*ba.args, **ba.kwargs) wrapper.__signature__ = sig return wrapper