#!/usr/bin/env python3.8
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
cytoflow.utility.scale
----------------------
Base classes and functions for `cytoflow` scales.
`IScale` -- the `traits.has_traits.Interface` that scales must implement
`ScaleMixin` -- provides useful functionality that scales can inherit
`scale_factory` -- make a new instance of a scale
`register_scale` -- register a new type of scale
`set_default_scale`, `get_default_scale` -- sets and gets the default scale
"""
import numbers
from traits.api import Interface, Str, Instance, Tuple, Array
from .cytoflow_errors import CytoflowError
from .util_functions import is_numeric
from traits.has_traits import HasStrictTraits
[docs]class IScale(Interface):
"""
An interface for various ways we could rescale flow data.
Attributes
----------
name : Str
The name of this view (for serialization, UI, etc.)
experiment : Instance(Experiment)
The experiment this scale is to be applied to. Needed because some
scales have parameters estimated from data.
channel : Str
Which channel to scale. Needed because some scales have parameters
estimated from data.
condition : Str
What condition to scale. Needed because some scales have parameters
estimated from the a condition. Must be a numeric condition; else
instantiating the scale should fail.
statistic : Tuple(Str, Str)
What statistic to scale. Needed because some scales have parameters
estimated from a statistic. The statistic must be numeric or an
iterable of numerics; else instantiating the scale should fail.
data : array_like
What raw data to scale.
"""
id = Str
name = Str
experiment = Instance("cytoflow.experiment.Experiment")
# what are we using to parameterize the scale? set one of these; if
# multiple are set, the first is used.
channel = Str
condition = Str
statistic = Tuple(Str, Str)
error_statistic = Tuple(Str, Str)
data = Array
def __call__(self, data):
"""
Transforms `data` using this scale. Must know how to handle int, float,
and lists, tuples, numpy.ndarrays and pandas.Series of int or float.
Must return the same type passed.
Careful! May return `NaN` if the scale domain doesn't match the data
(ie, applying a log10 scale to negative numbers.
"""
[docs] def inverse(self, data):
"""
Transforms 'data' using the inverse of this scale. Must know how to
handle int, float, and list, tuple, numpy.ndarray and pandas.Series of
int or float. Returns the same type as passed.
"""
[docs] def clip(self, data):
"""
Clips the data to the scale's domain.
"""
[docs] def norm(self, vmin = None, vmax = None):
"""
Return an instance of matplotlib.colors.Normalize, which normalizes
this scale to [0, 1]. Among other things, this is used to correctly
scale a color bar.
"""
[docs]class ScaleMixin(HasStrictTraits):
"""
Provides useful functionality that scales can inherit.
"""
def __init__(self, **kwargs):
# run the traits constructor
super().__init__(**kwargs)
# check that the channel, condition, or statistic is either numeric or
# an iterable of numerics
if self.condition:
if not is_numeric(self.experiment[self.condition]):
raise CytoflowError("Tried to scale the non-numeric condition {}"
.format(self.condition))
elif self.statistic[0]:
stat = self.experiment.statistics[self.statistic]
if is_numeric(stat):
return
else:
try:
for x in stat:
for y in x:
if not isinstance(y, numbers.Number):
raise CytoflowError("Tried to scale a non-numeric "
"statistic {}"
.format(self.statistic))
except TypeError as e:
raise CytoflowError("Error scaling statistic {}"
.format(self.statistic)) from e
# maps name -> scale object
_scale_mapping = {}
_scale_default = "linear"
[docs]def scale_factory(scale, experiment, **scale_params):
"""
Make a new instance of a named scale.
Parameters
----------
scale : string
The name of the scale to build
experiment : Experiment
The experiment to use to parameterize the new scale.
**scale_params : kwargs
Other parameters to pass to the scale constructor
"""
scale = scale.lower()
if scale not in _scale_mapping:
raise CytoflowError("Unknown scale type {0}".format(scale))
return _scale_mapping[scale](experiment = experiment, **scale_params)
[docs]def register_scale(scale_class):
"""
Register a new scale for the `scale_factory` and `ScaleEnum`.
"""
_scale_mapping[scale_class.name] = scale_class
[docs]def set_default_scale(scale):
"""
Set a default scale for `ScaleEnum`
"""
global _scale_default
scale = scale.lower()
if scale not in _scale_mapping:
raise CytoflowError("Unknown scale type {0}".format(scale))
_scale_default = scale
[docs]def get_default_scale():
"""Get the defaults scale set with `set_default_scale`"""
return _scale_default
# register the new scales
import cytoflow.utility.linear_scale # @UnusedImport
import cytoflow.utility.log_scale # @UnusedImport
import cytoflow.utility.logicle_scale # @UnusedImport
# this scale is REALLY SLOW. If you want it for your analysis, you can
# import it into your script, that will register it with the global list.
# import cytoflow.utility.hlog_scale # @UnusedImport