1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Fuzzy comparisons and aggregations."""
6
7
8import logging
9import math
10
11from firmware_constants import MF
12
13
14DEFAULT_MEMBERSHIP_FUNCTION = {
15    '<=': MF.Z_FUNCTION,
16    '<': MF.Z_FUNCTION,
17    '>=': MF.S_FUNCTION,
18    '>': MF.S_FUNCTION,
19    '==': MF.SINGLETON_FUNCTION,
20    '~=': MF.PI_FUNCTION,
21}
22
23
24"""Define possible score aggregators: average() and product().
25
26A score aggregator collects all scores from every tests, and calculate
27a final score.
28"""
29
30def average(data):
31    """The average of the elements in data."""
32    number = len(data)
33    return math.fsum(data) / number if number > 0 else None
34
35
36def product(data):
37    """The product of the elements in data."""
38    return math.exp(math.fsum([math.log(d) for d in data]))
39
40
41"""Classes of various fuzzy member functions are defined below."""
42
43class FuzzyMemberFunctions(object):
44    """The base class of membership functions."""
45    def __init__(self, para):
46        """Example of parameter: (0.1, 0.3)."""
47        self.para_values = map(float, para)
48
49
50class FuzzySingletonMemberFunction(FuzzyMemberFunctions):
51    """A class provides fuzzy Singleton Membership Function.
52
53    Singleton Membership Function:
54        parameters: (left, middle, right)
55        grade(x) = 0.0,        when x <= left
56                   0.0 to 1.0, when left <= x <= middle
57                   1.0,        when x == middle
58                   1.0 to 0.0, when middle <= x <= right
59                   0.0,        when x >= right
60        E.g., FuzzySingletonMemberFunction((1, 1, 1))
61              Usage: when we want the x == 1 in the ideal condition.
62                     grade = 1.0, when x == 1
63                             0.0, when x != 1
64
65        Note: - When x is near 'middle', the grade would be pretty close to 1.
66              - When x becomes near 'left' or 'right', its grade may drop
67                faster and would approach 0.
68              - A cosine function is used to implement this behavior.
69    """
70    def __init__(self, para):
71        super(FuzzySingletonMemberFunction, self).__init__(para)
72        self.left, self.middle, self.right = self.para_values
73        self.width_right = self.right - self.middle
74        self.width_left = self.middle - self.left
75
76    def grade(self, x):
77        """The grading method of the fuzzy membership function."""
78        if x == self.middle:
79            return 1
80        elif x <= self.left or x >= self.right:
81            return 0
82        elif x > self.middle:
83            return (0.5 + 0.5 * math.cos((x - self.middle) / self.width_right *
84                    math.pi))
85        elif x < self.middle:
86            return (0.5 + 0.5 * math.cos((x - self.middle) / self.width_left *
87                    math.pi))
88
89
90class FuzzySMemberFunction(FuzzyMemberFunctions):
91    """A class provides fuzzy S Membership Function.
92
93    S Membership Function:
94        parameters: (left, right)
95        grade(x) = 1  for x >= right
96                   0  for x <= left
97        E.g., FuzzySMemberFunction((0.1, 0.3))
98              Usage: when we want the x >= 0.3 in the ideal condition.
99                     grade = 1.0,                 when x >= 0.3
100                             between 0.0 and 1.0, when 0.1 <= x <= 0.3
101                             0.0,                 when x <= 0.1
102
103        Note: - When x is less than but near 'right' value, the grade would be
104                pretty close to 1.
105              - When x becomes near 'left' value, its grade may drop faster
106                and would approach 0.
107              - A cosine function is used to implement this behavior.
108    """
109
110    def __init__(self, para):
111        super(FuzzySMemberFunction, self).__init__(para)
112        self.left, self.right = self.para_values
113        self.width = self.right - self.left
114
115    def grade(self, x):
116        """The grading method of the fuzzy membership function."""
117        if x >= self.right:
118            return 1
119        elif x <= self.left:
120            return 0
121        else:
122            return 0.5 + 0.5 * math.cos((x - self.right) / self.width * math.pi)
123
124
125class FuzzyZMemberFunction(FuzzyMemberFunctions):
126    """A class provides fuzzy Z Membership Function.
127
128    Z Membership Function:
129        parameters: (left, right)
130        grade(x) = 1  for x <= left
131                   0  for x >= right
132        E.g., FuzzyZMemberFunction((0.1, 0.3))
133              Usage: when we want the x <= 0.1 in the ideal condition.
134                     grade = 1.0,                 when x <= 0.1
135                             between 0.0 and 1.0, when 0.1 <= x <= 0.3
136                             0.0,                 when x >= 0.3
137
138        Note: - When x is greater than but near 'left' value, the grade would be
139                pretty close to 1.
140              - When x becomes near 'right' value, its grade may drop faster
141                and would approach 0.
142              - A cosine function is used to implement this behavior.
143    """
144
145    def __init__(self, para):
146        super(FuzzyZMemberFunction, self).__init__(para)
147        self.left, self.right = self.para_values
148        self.width = self.right - self.left
149
150    def grade(self, x):
151        """The grading method of the fuzzy membership function."""
152        if x <= self.left:
153            return 1
154        elif x >= self.right:
155            return 0
156        else:
157            return 0.5 + 0.5 * math.cos((x - self.left) / self.width * math.pi)
158
159
160# Mapping from membership functions to the fuzzy member function classes.
161MF_DICT = {
162    # TODO(josephsih): PI, TRAPEZ, and TRIANGLE functions are to be implemented.
163    # MF.PI_FUNCTION: FuzzyPiMemberFunction,
164    MF.SINGLETON_FUNCTION: FuzzySingletonMemberFunction,
165    MF.S_FUNCTION: FuzzySMemberFunction,
166    # MF.TRAPEZ_FUNCTION: FuzzyTrapezMemberFunction,
167    # MF.TRIANGLE_FUNCTION: FuzzyTriangleMemberFunction
168    MF.Z_FUNCTION: FuzzyZMemberFunction,
169}
170
171
172class FuzzyCriteria:
173    """A class to parse criteria string and build the criteria object."""
174
175    def __init__(self, criteria_str, mf=None):
176        self.criteria_str = criteria_str
177        self.mf_name = mf
178        self.mf = None
179        self.default_mf_name = None
180        self.value_range = None
181        self._parse_criteria_and_exit_on_failure()
182        self._create_mf()
183
184    def _parse_criteria(self, criteria_str):
185        """Parse the criteria string.
186
187        Example:
188            Ex 1. '<= 0.05, ~ +0.07':
189                  . The ideal input value should be <= 0.05. If so, it gets
190                    the grade 1.0
191                  . The allowable upper bound is 0.05 + 0.07 = 0.12. Anything
192                    greater than or equal to 0.12 would get a grade 0.0
193                  . Any input value falling between 0.05 and 0.12 would get a
194                    score between 0.0 and 1.0 depending on which membership
195                    function is used.
196        """
197        criteria_list = criteria_str.split(',')
198        tolerable_delta = []
199        op_value = None
200        for c in criteria_list:
201            op, value = c.split()
202            # TODO(josephsih): should support and '~=' later.
203            if op in ['<=', '<', '>=', '>', '==']:
204                primary_op = op
205                self.default_mf_name = DEFAULT_MEMBERSHIP_FUNCTION[op]
206                op_value = float(value)
207            elif op == '~':
208                tolerable_delta.append(float(value))
209            else:
210                return False
211
212        # Syntax error in criteria string
213        if op_value is None:
214            return False
215
216        # Calculate the allowable range of values
217        range_max = range_min = op_value
218        for delta in tolerable_delta:
219            if delta >= 0:
220                range_max = op_value + delta
221            else:
222                range_min = op_value + delta
223
224        if primary_op in ['<=', '<', '>=', '>']:
225            self.value_range = (range_min, range_max)
226        elif primary_op == '==':
227            self.value_range = (range_min, op_value, range_max)
228        else:
229            self.value_range = None
230
231        return True
232
233    def _parse_criteria_and_exit_on_failure(self):
234        """Call _parse_critera and exit on failure."""
235        if not self._parse_criteria(self.criteria_str):
236            logging.error('Parsing criteria string error.')
237            exit(1)
238
239    def _create_mf(self):
240        """Parse the criteria and create its membership function object."""
241        # If a membership function is specified in the test_conf, use it.
242        # Otherwise, use the default one.
243        mf_name = self.mf_name if self.mf_name else self.default_mf_name
244        mf_class = MF_DICT[mf_name]
245        self.mf = mf_class(self.value_range)
246
247    def get_criteria_value_range(self):
248        """Parse the criteria and return its op value."""
249        return self.value_range
250