so_magic.utils.linear_mapping   A
last analyzed

Complexity

Total Complexity 16

Size/Duplication

Total Lines 106
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 64
dl 0
loc 106
rs 10
c 0
b 0
f 0
wmc 16

11 Methods

Rating   Name   Duplication   Size   Complexity  
A MapOnLinearSpace.transform() 0 3 1
A MapOnLinearSpace.universal_constructor() 0 5 1
A LinearScale.__len__() 0 3 1
A MapOnLinearSpace.reverse() 0 3 1
A MapOnLinearSpace.__transform_inverted() 0 5 1
A MapOnLinearSpace._get_transform_callback() 0 4 2
A MapOnLinearSpace.target_scale() 0 3 1
A MapOnLinearSpace.from_scale() 0 3 1
A LinearScale.__validate_scale() 0 4 2
A MapOnLinearSpace.__transform() 0 5 1
A LinearScale.create() 0 3 1
1
"""This module exposes the MapOnLinearSpace class and the 'universal_constructor' method to create instances of it.
2
Instances of MapOnLinearSpace can be used to project a number from one linear space to another."""
3
import attr
4
5
6
@attr.s
7
class LinearScale:
8
    """A numerical linear scale (range between 2 numbers) where numbers fall in between.
9
10
    Raises:
11
        ValueError: in case the lower bound is not smaller than the upper
12
13
    Args:
14
        lower_bound (int): the minimum value a number can take on the scale
15
        upper_bound (int): the maximum value a number can take on the scale
16
    """
17
18
    lower_bound = attr.ib(init=True, type=int)
19
    upper_bound = attr.ib(init=True, type=int)
20
21
    @upper_bound.validator
22
    def __validate_scale(self, _attribute, upper_bound):
23
        if upper_bound <= self.lower_bound:
24
            raise ValueError('The linear scale, should have lower_bound < upper_bound.'
25
                             f' Instead lower_bound={self.lower_bound}, upper_bound={upper_bound}')
26
27
    def __len__(self):
28
        """Returns 2, since the lower and upper bound values are sufficient to define a linear scale."""
29
        return 2
30
31
    @classmethod
32
    def create(cls, two_element_list_like):
33
        return LinearScale(*list(two_element_list_like))
34
35
36
@attr.s
37
class MapOnLinearSpace:
38
    """Projection of a number from one linear scale to another.
39
40
    Instances of this class can transform an input number and map it from an initial scale to a target scale.
41
42
    Args:
43
         _from_scale (LinearScale): the scale where the number is initially mapped
44
         _target_scale (LinearScale): the (target) scale where the number should be finally transformed/mapped to
45
         _reverse (bool): whether the target scale is inverted or not
46
    """
47
    _from_scale = attr.ib(init=True, type=LinearScale)
48
    _target_scale = attr.ib(init=True, type=LinearScale)
49
    _reverse = attr.ib(init=True, default=False, type=bool)
50
    _transform_callback = attr.ib(init=False,
51
                                  default=attr.Factory(lambda self: self._get_transform_callback(), takes_self=True))
52
53
    @classmethod
54
    def universal_constructor(cls, from_scale, target_scale, reverse=False):
55
        return MapOnLinearSpace(LinearScale.create(from_scale),
56
                                LinearScale.create(target_scale),
57
                                reverse)
58
59
    def __transform_inverted(self, number):
60
        return ( (self._target_scale.lower_bound - self._target_scale.upper_bound) * number
61
                 + self._target_scale.upper_bound * self._from_scale.upper_bound
62
                 - self._target_scale.lower_bound * self._from_scale.lower_bound ) / (
63
               self._from_scale.upper_bound - self._from_scale.lower_bound)
64
65
    def __transform(self, number):
66
        return ((self._target_scale.upper_bound - self._target_scale.lower_bound) * number
67
                + self._target_scale.lower_bound * self._from_scale.upper_bound
68
                - self._target_scale.lower_bound * self._from_scale.lower_bound) / (
69
                self._from_scale.upper_bound - self._from_scale.lower_bound)
70
71
    def _get_transform_callback(self):
72
        if self._reverse:
73
            return self.__transform_inverted
74
        return self.__transform
75
76
    def transform(self, number):
77
        """Transform the input number to a different linear scale."""
78
        return self._transform_callback(number)
79
80
    @property
81
    def from_scale(self):
82
        return self._from_scale
83
84
    @from_scale.setter
85
    def from_scale(self, from_scale):
86
        self._from_scale = LinearScale.create(from_scale)
87
        self._transform_callback = self._get_transform_callback()
88
89
    @property
90
    def target_scale(self):
91
        return self._target_scale
92
93
    @target_scale.setter
94
    def target_scale(self, target_scale):
95
        self._target_scale = LinearScale.create(target_scale)
96
        self._transform_callback = self._get_transform_callback()
97
98
    @property
99
    def reverse(self):
100
        return self._reverse
101
102
    @reverse.setter
103
    def reverse(self, reverse):
104
        self._reverse = reverse
105
        self._transform_callback = self._get_transform_callback()
106