├── .box ├── CODE fuzzy.ipynb ├── Fuzzy-Tutorial_1_Gra_Leh30Teil.pdf ├── FuzzyLogicandControlSoftwareandHardwareApplicationsChapter4FuzzyRuleBasedEx.pdf ├── TEST fuzzy.ipynb ├── fuzzylogic_model.mdj ├── notizen für fuzzy logic and control └── version 1 │ ├── base.py │ ├── defuzzification.py │ ├── example1.py │ ├── fuzzification.py │ ├── hedges.py │ ├── inference.py │ ├── operators.py │ ├── optimizedbase.py │ ├── setmodifications.py │ └── tools.py ├── .github └── workflows │ └── docsupdater.yaml ├── .gitignore ├── .readthedocs.yaml ├── .vscode └── settings.json ├── CITATION.cff ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Showcase.ipynb ├── Tipping Problem.ipynb ├── ai workflow.md ├── docs │ ├── Discussion.md │ ├── How-To.md │ ├── Showcase.md │ ├── Showcase_files │ │ ├── Showcase_10_0.png │ │ ├── Showcase_12_1.png │ │ ├── Showcase_13_1.png │ │ ├── Showcase_15_0.png │ │ ├── Showcase_5_0.png │ │ ├── Showcase_6_0.png │ │ ├── Showcase_7_0.png │ │ └── Showcase_8_0.png │ ├── classes.md │ ├── combinators.md │ ├── functions.md │ ├── hedges.md │ ├── index.md │ ├── rules.md │ └── truth.md ├── mkdocs.yml └── requirements.txt ├── fuzzylogic.code-workspace ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.cfg ├── setup.py ├── src ├── .vscode │ └── settings.json ├── __init__.py └── fuzzylogic │ ├── __init__.py │ ├── classes.py │ ├── combinators.py │ ├── defuzz.py │ ├── estimate.py │ ├── functions.py │ ├── hedges.py │ ├── neural_network.py │ ├── tools.py │ └── truth.py └── tests ├── test.py ├── test_caro.py ├── test_defuzz.py ├── test_functionality.py ├── test_singleton.py ├── test_units.py └── thebariumoxide_test.py /.box/Fuzzy-Tutorial_1_Gra_Leh30Teil.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/.box/Fuzzy-Tutorial_1_Gra_Leh30Teil.pdf -------------------------------------------------------------------------------- /.box/FuzzyLogicandControlSoftwareandHardwareApplicationsChapter4FuzzyRuleBasedEx.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/.box/FuzzyLogicandControlSoftwareandHardwareApplicationsChapter4FuzzyRuleBasedEx.pdf -------------------------------------------------------------------------------- /.box/notizen für fuzzy logic and control: -------------------------------------------------------------------------------- 1 | core - da wo 1 ist 2 | boundaries - überall wo nicht 1 und nicht 0 3 | support - überall wo nicht 0 4 | 5 | schreibweise für fuzzy sets: 6 | A = µ1 / u1 + µ2 / u2 + µ3 / u3 7 | oben stehen die membership values, unten die werte; + ist funktions-theoretische vereinigung 8 | 9 | vereinigung von zwei konvexen megen -> konvexe menge 10 | "normal fuzzy set" := fuzzy set mit mindestens einem element mit µ = 1 11 | bei fuzzy sets mit exakt einem element mit µ = 1 heißt dieses element "prototype" 12 | 13 | eine funktion die von einer menge A auf eine menge B abbildet: 14 | f(A) = f(µ1/ u1 + µ2 / u2 + µ3 + u3) = µ1 / f(u1) + µ2 / f(u2) + µ3 / f(u3) 15 | (erweiterungsprinzip nach zadeh & yager) 16 | 17 | wenn man zwei universen auf eines abbilden will funktioniert das kartesisch: 18 | U1 x U2 -> V 19 | hier ist A ein fuzzy set das über den universen U1 und U2 definiert ist: 20 | f(A) = {min(µ1(i), µ1(j) / f(i,j) \ i element of U1, j element of U2} 21 | where (µ1(i) and (µ2(j) are the separable membership projections of µ(ij) from the 22 | Cartesian space Ui x U2 when ji(i j) can not be determined. 23 | - dazu gilt analog zur wahrscheinlichkeitstheorie die bedingung der 24 | "non-interaktion" ("unabhängigkeit" in der wahrscheinlichkeitstheorie) 25 | in FL wird min() statt prod() verwendet 26 | 27 | beispiel: 28 | U1 = U2 = {1,2,..9,10} 29 | A(auf U1) = 0.6/1 + 1/2 + 0.8/3 30 | B(auf U2) = 0.8/5 + 1/6 + 0.7/7 31 | V = {5,6,..,18,12} 32 | A x B = (0.6/1 + 1/2 + 0.8/3)x (0.8/5 + 1/6 + 0.7/7) 33 | = min(0.6,0.8)/5 + min(0.6,1)/6 + ... + min(0.8,1)/18 + min(0.8, 0.7)/21 34 | = 0.6/5 + 0.6/6 + 0.6/7 + 0.8/10 + 1/12 + 0.7/14 + 0.8/15 + 0.8/18 + 0.7/21 35 | 36 | falls es zwei oder mehr wege gibt eine zahl zu produzieren wird zuerst 37 | min() und dann max() angewandt: 38 | a = 0.2/1 + 1/2 + 0.7/4 39 | b = 0.5/1 + 1/2 40 | 41 | a*b = min(0.2,0.5)/1 + max(min(0.2,1)), min(0.5, 1))/2 + max(min(0.7,0.5, min(1,1))/4, min(0.7,1)/8 42 | = 0.2/1 + 0.5/2 + 1/4 + 0.7/8 43 | 44 | v = binary OR, u (rundes v) = set UNION 45 | /\ = binary AND, rundes /\ = set INTERSECTION 46 | 47 | union (OR): max.. 48 | intersection (AND): min.. 49 | complement: 1- .. 50 | containment: R contains S <=> µR(x,y) <= µS(x,y) 51 | identity: {} -> 0 and X -> E 52 | 53 | composition (abbildung X -> Y -> Z ~ X -> Z) ~ "defuzzifizierung": 54 | max-min: 55 | T = R o S 56 | *das OR und AND stimmen so - obwohl es max-min heißt* 57 | µT(x,z) = OR(µR(x,y) AND µS(y,z) for y element of Y) 58 | 59 | max-prod (manchmal auch max-dot): 60 | T = R o S 61 | µT(x,z) = OR(µR(x,y) * µS(y,z) for y element of Y) 62 | 63 | beispiel: 64 | (R= x zeilen, y spalten) 65 | R = [ 66 | [1,0,1,0], 67 | [0,0,0,1], 68 | [0,0,0,0], 69 | ] 70 | (S = y zeilen, z spalten) 71 | S = [ 72 | [0,1], 73 | [0,0], 74 | [0,1], 75 | [0,0], 76 | ] 77 | 78 | T = [ 79 | [0,1], 80 | [0,0], 81 | [0,0], 82 | ] 83 | 84 | dazu muss lediglich multiplikation als min (AND) und 85 | addition als max (OR) implementiert werden - 86 | alles andere ist pure matrizenmultiplikation! 87 | 88 | [0,1] heißt auch "einheitsinterval" oder "unit interval" 89 | 90 | implication: 91 | P -> Q => x is A then x is B 92 | T(P -> Q) = T(-P) or Q) = max(T(~P), T(Q)) 93 | P -> Q is: IF x is A THEN y is B 94 | 95 | IF x is A THEN y is B ELSE y is C: 96 | R = (A x B) OR (~A x C) 97 | 98 | 99 | 100 | u(t) = Kp . e(t) 101 | for a proportional or P controller; 102 | u(t) = Kp . e(t) + KI . integral(e(t))dt 103 | for a proportional plus integral or PI controller; 104 | u(t) = Kp. e(t) + KD. e'(t) 105 | for a proportional plus derivative or PD controller; 106 | u(t) = Kp . e(t) + KI . integral(e(t))dt + KD. e'(t) 107 | or a proportional plus derivative plus integral or PID controller, 108 | 109 | where e(t), e'(t), and integral(e(t))dt are the output error, error derivative, and error integral, respectively; 110 | 111 | The problem of control system design is defined as obtaining the generally 112 | nonlinear function h(.) in the case of nonlinear systems, coefficients Kp, KI, and KD in 113 | the case of an output-feedback, and coefficients ki, k2,..., and kn, in the case of state- 114 | feedback policies for linear system models. 115 | 116 | i) Large scale systems are decentralized and decomposed into a collection 117 | of decoupled sub-systems. 118 | ii) The temporal variations of plant dynamics are assumed to be "slowly 119 | varying." 120 | iii) The nonlinear plant dynamics are locally linearized about a set of 121 | operating points. 122 | iv) A set of state variables, control variables, or output features are made 123 | available. 124 | v) A simple P, PD, PID (output feedback), or state-feedback controller is 125 | designed for each de coupled system. The controllers are of regulatory 126 | type and are fast enough to perform satisfactorily under tracking control 127 | situations. Optimal controllers can also be tried using LQR or LQG 128 | techniques. 129 | vi) The first five steps mentioned above introduce uncertainties. There are 130 | also uncertainties due to external environment. The controller design 131 | should be made as close as possible to the optimal one based on the 132 | control engineer's all best available knowledge, in the form of I/O 133 | numerical observations data, analytic, linguistic, intuitive, and etc., 134 | information regarding the plant dynamics and external world. 135 | vii) A supervisory control system, either automatic or a human expert 136 | operator, forms an additional feedback control loop to tune and adjust 137 | the controller's parameters in order to compensate the effects of 138 | uncertainties and variations due to unmodeled dynamics. 139 | 140 | 141 | Six basic assumptions are commonly made whenever a fuzzy logic-based control policy 142 | is selected. These assumptions are outlined below: 143 | i) The plant is observable and controllable: State, input, and output 144 | variables are available for observation and measurement or 145 | computation. 146 | ii) There exists a body of knowledge in the form of expert production 147 | linguistic rules, engineering common sense, intuition, or an analytic 148 | model that can be fuzzifled and the rules be extracted. 149 | iii) A solution exists. 150 | vi) The control engineer is looking for a good enough solution and not 151 | necessarily the optimum one. 152 | v) We desire to design a controller to the best of our available knowledge 153 | and within an acceptable precision range. 154 | vi) The problems of stability and optimality are open problems. 155 | 156 | A fuzzy production rule system consists of four structures: 157 | i) A set of rules which represents the policies and heuristic strategies of 158 | the expert decision-maker. 159 | ii) A set of input data assessed immediately prior to the actual decision. 160 | iii) A method for evaluating any proposed action in terms of its conformity 161 | to the expressed rules, given the available data, 162 | iv) A method for generating promising actions and for determining when to 163 | stop searching for better ones. 164 | 165 | A fuzzy production rule system consists of four structures: 166 | i) A set of rules which represents the policies and heuristic strategies of 167 | the expert decision-maker. 168 | ii) A set of input data assessed immediately prior to the actual decision. 169 | iii) A method for evaluating any proposed action in terms of its conformity 170 | to the expressed rules, given the available data, 171 | iv) A method for generating promising actions and for determining when to 172 | stop searching for better ones. 173 | 174 | In general, a value of a linguistic variable is a composite term which is a concatenation of 175 | atomic terms. These atomic terms may be divided into four categories: 176 | i) primary terms which are labels of specified fuzzy subsets of the 177 | universe of discourse (e.g., hot, cold, hard, lower, etc., in the preceding 178 | example). 179 | ii) The negation NOT and connectives "AND" and "OR." 180 | iii) Hedges, such as "very," "much," "slightly," "more or less," etc. 181 | iv) Markers, such as parentheses. 182 | 183 | IF A THEN B entspricht: 184 | # Zadeh's implication oder classic implication 185 | # falls µB(y) < µA(x) wird (44) zu (45) reduziert 186 | (44) µ(x,y) = max(min(µA(x), µB(y)), 1- µA(x)) 187 | (45) µ(x,y) = max((µB(y), (1 - µA(x))) 188 | 189 | # correlation-minimum oder Mamdani's implication 190 | # wird auch für das fuzzy-kreuzprodukt verwendet 191 | # falls µA(x) >=0.5 und µB(y) >=0.5 192 | # wird Zadeh's implication zu Mamdani's implication reduziert 193 | (46) µ(x,y) = min(µA(x), µB(y)) 194 | 195 | # Luckawics implication 196 | (47) µ(x,y) = min(1, (1 - µA(x) + µB(y))) 197 | 198 | # bounded sum implication 199 | (48) µ(x,y) = min(1, (µA(x) + µB(y))) 200 | 201 | (49) µ(x,y) = min(1, [µB(y) / µA(x)]) 202 | 203 | # eine form des correlation-product (hebbian networks: conditioning) 204 | # vom Author bevorzugt 205 | (50) µ(x,y) = max(µA(x) * µB(y), (1 - µA(x))) 206 | 207 | # eine form des correlation-product (hebbian networks: reinforcement) 208 | (51) µ(x,y) = µA(x) * µB(y) 209 | 210 | (52) µ(x,y) = (µB(y))**µA(x) 211 | 212 | # Gödel's implication oder "alpha" implication 213 | (53) µ(x,y) = {µB(y) for µB(y) < µA(x); 1 otherwise} 214 | 215 | Methods for composition of fuzzy relations: 216 | I. Max-Min composition 217 | yk = x • Rk 218 | µ(y) = max(min(µ(x), µR(x, y) for x in X) 219 | 220 | II. Max-Prod composition 221 | yk = x * Rk 222 | µ(y) = max(µ(x) • µ(x, y) for x in X) 223 | 224 | III. Min-Max composition 225 | yk = x † Rk 226 | µ(y) = min(max(µ(x), µ(x,y) for x in X)) 227 | 228 | IV. Max-Max composition 229 | yk = x o Rk 230 | µ(y) = max(max(µ(x), µ(x,y)) for x in X) 231 | 232 | V. Min-Min composition 233 | µk = x ¤ Rk 234 | µ(y) = min(min(µ(x), µ(x, y) for x in X)) 235 | 236 | VI. (p, q) composition 237 | µk = x ºpq Rk 238 | µ(y) = max_p(min_q(µ(x), µ(x,y))) 239 | where max_p a(x) = inf.(x) for x in X 240 | {1, [[a(x1)]**p + [a[(x2)**p + ... + a(xn)**p)*1/p} 241 | and 242 | min_q [a(x), b(x)] = 1 - min((1,(1 - a(x))**q + (1 - b(x))**q)*1/q) 243 | 244 | VII. Sum-Prod composition 245 | yk = x x Rk 246 | µ(y) = f(sum(µ(x) * µ(x,y))) 247 | where f(*) is a logistic function that limits the 248 | value of the function within the unit interval 249 | mainly used in ANN for mapping between parallel layers 250 | in a multi-layer network. 251 | 252 | VIII. Max-Ave composition 253 | yk = x * av.Rk 254 | µ(y) = 1/2*max(µ(x) + µ(x,y) for x in X) 255 | 256 | 257 | # Fuzzy Control (page 175) 258 | # A Fuzzy Two-Axis Mirror Controller for Laser Beam Alignment 259 | 260 | #VAR Error 261 | #TYPE signed byte /* C type of'signed char' */ 262 | #MIN -128 /* universe of discourse min */ 263 | #MAX 127 /* universe of discourse max */ 264 | #/* Membership functions for Error (ZE, PS, NS, PM, NM). */ 265 | #MEMBER ZE 266 | #POINTS -20 0 0 1 20 0 267 | #END 268 | #VAR dError 269 | #TYPE signed byte 270 | #MIN-100 271 | #*/ 272 | #MAX 100 273 | #*/ /* C type of 'signed char' */ 274 | #/* universe of discourse min 275 | #/* universe of discourse max 276 | # 277 | #MEMBER ZE 278 | #POINTS -30 0 0 1 30 0 279 | #END 280 | 281 | #FUZZY Alignment_rules 282 | #RULE Rulel 283 | #IF Error IS PM AND dError IS ZE THEN 284 | #Speed IS NM 285 | #END 286 | # ... 287 | #/* The following three CONNECT Objects specify that Error 288 | #* and dError are inputs to the Alignment_rules knowledge base 289 | #* and Speed output from Alignment_rules. 290 | #CONNECT 291 | #FROM Error 292 | #TO Alignment_rules 293 | #END 294 | # ... 295 | 296 | transformed-viscosity = (integer) (10*logiQ (viscosity) + .5). -------------------------------------------------------------------------------- /.box/version 1/base.py: -------------------------------------------------------------------------------- 1 | # from pylab import plot, show 2 | from numpy import arange 3 | 4 | domains = {} 5 | rules = [] 6 | 7 | 8 | class FuzzyFunction(object): 9 | """All FuzzyFunction classes are subclassed from this. 10 | 11 | FuzzyFunction classes are initialized with parameters and then used by calls 12 | with one single value. All FuzzyFunctions map arbitrary values to [0,1]. 13 | """ 14 | 15 | def __call__(self, x): 16 | return self.f(x) 17 | 18 | def __invert__(self): 19 | return NOT(self) 20 | 21 | def __and__(self, other): 22 | return AND(self, other) 23 | 24 | def __or__(self, other): 25 | return OR(self, other) 26 | 27 | def __repr__(self): 28 | return "{0}({1})".format(self.__class__.__name__, self.__dict__) 29 | 30 | def __str__(self): 31 | return "{0}({1})".format(self.__class__.__name__, self.__dict__) 32 | 33 | 34 | # These basic operators are defined here to avoid an import-loop 35 | 36 | 37 | class AND(FuzzyFunction): 38 | def __init__(self, a, b): 39 | self.a = a 40 | self.b = b 41 | 42 | def __call__(self, x): 43 | return min(self.a(x), self.b(x)) 44 | 45 | 46 | class OR(FuzzyFunction): 47 | def __init__(self, a, b): 48 | self.a = a 49 | self.b = b 50 | 51 | def __call__(self, x): 52 | return max(self.a(x), self.b(x)) 53 | 54 | 55 | class NOT(FuzzyFunction): 56 | def __init__(self, f): 57 | self.f = f 58 | 59 | def __call__(self, x): 60 | return 1 - self.f(x) 61 | 62 | 63 | class Domain(object): 64 | def __init__(self, name, limits, resolution=1.0): 65 | """ 66 | A domain normally is a linguistic variable in a system that usually 67 | corresponds with a physical unit and a range of valid values. 68 | Fuzzy-Sets or 'linguistic terms' are defined within domains by their 69 | function which map measurements within the domain onto degrees of 70 | membership (floats in the [0,1] range) for that linguistic term. 71 | 72 | 73 | name 74 | Full name of the domain 75 | 76 | limits 77 | Tuple with minimum and maximum values, order determines the graph 78 | for visualisation. Theoretically inf is allowed, but there's 79 | no way to handle it correctly. 80 | 81 | resolution 82 | Float that indicates the error margines of measurement and the steps 83 | for visualisation. 84 | """ 85 | # the fuzzysets as attributes 86 | self.sets = {} 87 | assert limits[0] <= limits[1], "Lower limit > upper limit!" 88 | self.name = name 89 | self.limits = limits 90 | self.resolution = resolution 91 | domains[name] = self 92 | 93 | def __str__(self): 94 | return "Domain(%s)" % self.name 95 | 96 | def __getattr__(self, name): 97 | if name in self.sets: 98 | return self.sets[name] 99 | else: 100 | raise AttributeError 101 | 102 | def __setattr__(self, name, fuzzyfunc): 103 | # it's not actually a fuzzyfunc but a domain attr 104 | if name in ["name", "limits", "resolution", "sets"]: 105 | object.__setattr__(self, name, fuzzyfunc) 106 | # we've got a fuzzyset within this domain defined by a fuzzyfunc 107 | else: 108 | self.sets[name] = fuzzyfunc 109 | 110 | def plot(self): 111 | r = arange(self.limits[0], self.limits[1] + self.resolution, self.resolution) 112 | for s in self.sets.values(): 113 | print(r, s) 114 | # plot(s(r)) 115 | # show() 116 | 117 | def __call__(self, x): 118 | for name, f in self.sets.items(): 119 | print(name, f(x)) 120 | 121 | 122 | class Rule(object): 123 | """Collection of fuzzysets and operators for inference. 124 | 125 | A rule has two mandatory arguments: IF and THEN, both being valid 126 | python expressions that evaluate to functions that take a single argument 127 | (usually fuzzyfunctions). 128 | For evaluation and inference of IF and THEN default functions or those 129 | provided via optional arguments are used. 130 | 131 | To evaluate concrete values (for defuzzification) simply call the Rule 132 | as a function. 133 | """ 134 | 135 | def __init__(self, IF, THEN, believe=1, and_=None, or_=None, not_=None, inference_=None): 136 | """The core fuzzy logic. 137 | 138 | IF is a string representing a condition using qualified fuzzy sets 139 | (that is the *name* of the domain and the *name* of the set NOT 140 | the variable that was used for the domain!). 141 | THEN is a string representing a lingual hedge with a SINGLE fuzzy set 142 | or another rule. 143 | believe is a factor that is applied to the result of the rule, 144 | taking into account that rules may be subject to uncertainty. 145 | and_, or_ and not_ are the FuzzyFunctions that are used to 146 | implement the given operations for this rule. A user may choose to 147 | use a different function for a specific case and override these. 148 | """ 149 | if and_ is not None: 150 | FuzzyFunction.__and__ = and_ 151 | if or_ is not None: 152 | FuzzyFunction.__or__ = or_ 153 | if not_ is not None: 154 | FuzzyFunction.__invert__ = not_ 155 | 156 | # self.inference = inference_ if inference is not None else MAXMIN 157 | 158 | locals().update(domains) 159 | 160 | self.if_result = eval(IF) 161 | rules.append(self) 162 | 163 | def __call__(self): 164 | return self.if_result 165 | 166 | 167 | if __name__ == "__main__": 168 | import cProfile 169 | from timeit import Timer 170 | 171 | from . import fuzzification as fuzzy 172 | 173 | temp = 25 174 | month = 9 175 | 176 | a = fuzzy.constant(0) 177 | 178 | year = Domain("year", limits=(1, 12)) 179 | 180 | def definition(): 181 | year.summer = fuzzy.trapezoid(4, 6, 7, 9, inverse=True) 182 | 183 | year = Domain("year", limits=(1, 12)) 184 | year.summer = fuzzy.trapezoid(4, 6, 7, 9) 185 | 186 | def call(): 187 | return a(8.6) 188 | 189 | cProfile.run("definition()") 190 | print(Timer(definition).timeit()) 191 | 192 | cProfile.run("call") 193 | print(Timer(call).timeit()) 194 | 195 | temp = Domain("temperature", limits=(0, 100)) 196 | temp.hot = fuzzy.linear(5, 35) 197 | 198 | import doctest 199 | 200 | doctest.testmod() 201 | -------------------------------------------------------------------------------- /.box/version 1/defuzzification.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Defuzzyfication is the process of mapping a set in the unit interval gained from 4 | fuzzyfication and inference back to a value with a physical unit. 5 | 6 | There are multiple ways of achieving this with more or less precision, ambiguities and 7 | required processing power. 8 | 9 | All of these functions take a 2-dimensional argument of the form [arange(x), y_values] 10 | with y_values in the unit interval. 11 | to allow easy plotting and use of numpy functions. 12 | All functions return a single unit interval that can be directly mapped to a control value. 13 | """ 14 | 15 | import numpy as np 16 | 17 | 18 | def maximum(fuzzyset): 19 | """Calculates the (generalized) maximum of a fuzzyset. 20 | 21 | This is needed for defuzzification. 22 | 23 | First we determine a suitable step within the range, then we go from 24 | left to right until we hit a maximum. Then we go from right to left, 25 | until we find a maximum. As we can be sure that the function is 26 | triangular, the maximum must be at the same height, however, 27 | we might need to calculate the average of an eventual "saddle". 28 | """ 29 | L = fuzzyset.lower 30 | U = fuzzyset.upper 31 | func = fuzzyset.func 32 | step = (U - L) / 100. 33 | 34 | values = [func(x) for x in np.nrange(L, U, step)] 35 | m = max(values) 36 | # We get the lower maximum 37 | lower_max_i = values.index(m) 38 | lower_max = L + lower_max_i*step 39 | 40 | # We get the upper maximum 41 | values.reverse() 42 | upper_max_i = values.index(m) 43 | upper_max = U - upper_max_i * i 44 | 45 | return (upper_max - lower_max) / 2. 46 | 47 | def center_of_gravity(fuzzyset): 48 | pass 49 | -------------------------------------------------------------------------------- /.box/version 1/example1.py: -------------------------------------------------------------------------------- 1 | from base import Domain, Rule 2 | import fuzzyfication as fuzzy 3 | 4 | 5 | # http://petro.tanrei.ca/fuzzylogic/fuzzy_negnevistky.html 6 | 7 | # the stuff a system architect needs to write 8 | funding = Domain("project_funding", (0, 100)) 9 | funding.inadequate = fuzzy.linear(30, 20) 10 | funding.marginal = fuzzy.triangular(20, 80) 11 | funding.adequate = fuzzy.linear(60, 80) 12 | 13 | staffing = Domain("project_staffing", (0, 100)) 14 | staffing.small = fuzzy.linear(60, 30) 15 | staffing.large = fuzzy.linear(40, 60) 16 | 17 | 18 | risk = Domain("project_risk", (0, 100)) 19 | risk.low = fuzzy.linear(40, 20) 20 | risk.normal = fuzzy.triangular(20, 80) 21 | risk.high = fuzzy.linear(60, 80) 22 | 23 | staffing.plot() 24 | 25 | funding(25) 26 | staffing(55) 27 | 28 | 29 | # the stuff a user needs to write 30 | # max(project_funding.adequat(25), project_staffing.small(55)), 31 | Rule("project_funding.adequate or project_staffing.small", "risk.low") 32 | Rule("project_funding.marginal and project_staffing.large", "risk.normal") 33 | Rule("project_funding.inadequate", "risk.high") 34 | 35 | # the system evaluates concrete values 36 | -------------------------------------------------------------------------------- /.box/version 1/fuzzification.py: -------------------------------------------------------------------------------- 1 | """Functions that map arbitrary values to the unit interval. 2 | 3 | Quote from 'Fuzzy Logic and Control: Software and Hardware Applications': 4 | 5 | "A convex fuzzy set is described by a membership function whose 6 | membership values are strictly monotonically increasing, or whose 7 | membership values are strictly monotonically decreasing, or whose 8 | membership values are strictly monotonically increasing then strictly 9 | monotonically decreasing with increasing values for elements in the universe." 10 | 11 | We don't comply (as actually does the book) with that definition as we 12 | use functions that are not *strictly* monotonic. 13 | 14 | 15 | :Author: Anselm Kiefner 16 | :Version: 3 17 | :Date: 2013-02-15 18 | :Status: superseded 19 | """ 20 | 21 | from math import exp, log 22 | 23 | from numpy import clip 24 | 25 | from .base import FuzzyFunction 26 | 27 | 28 | class constant(FuzzyFunction): 29 | """Return a fixed value no matter what input. 30 | >>> constant(6)(2) 31 | Traceback (most recent call last): 32 | ... 33 | AssertionError: not within the unit interval 34 | 35 | >>> constant(0.7)(9) 36 | 0.7 37 | """ 38 | 39 | def __init__(self, fix): 40 | assert 0 <= fix <= 1, 'not within the unit interval' 41 | self.fix = fix 42 | 43 | def __call__(self, x): 44 | return self.fix 45 | 46 | 47 | class singleton(FuzzyFunction): 48 | __slots__ = ['value'] 49 | 50 | """A single exact value.""" 51 | 52 | def __init__(self, value): 53 | self.value = value 54 | 55 | def __call__(self, x): 56 | return 1 if x == self.value else 0 57 | 58 | 59 | class linearA(FuzzyFunction): 60 | """A textbook linear function with y-axis section and gradient. 61 | variables 62 | -------- 63 | b: float 64 | y-axis section - function is only *linear* if b == 0 65 | m: float 66 | gradient - if m == 0, the function is constant 67 | 68 | >>> linearA(2)(3) 69 | 1 70 | >>> linearA(-1, 2)(1.5) 71 | 0.5 72 | """ 73 | 74 | def __init__(self, m=0, b=0): 75 | self.m = m 76 | self.b = b 77 | 78 | def __call__(self, x): 79 | return clip(self.m * x + self.b, 0, 1) 80 | 81 | 82 | class linear(FuzzyFunction): 83 | """Variant of the linear function with gradient being determined by bounds. 84 | 85 | The bounds determine minimum and maximum value-mappings, 86 | but also the gradient. As [0, 1] must be the bounds for y-values, 87 | left and right bounds specify 2 points on the graph, for which the formula 88 | f(x) = y = (y2 - y1) / (x2 - x1) * (x - x1) + y1 = (y2 - y1) / (x2 - x1) * 89 | (x - x2) + y2 90 | 91 | (right_y - left_y) / ((right - left) * (x - self.left) + left_y) 92 | works. 93 | 94 | vars 95 | ---- 96 | top: float 97 | the x-value, where f(x) = 1 98 | bottom: float 99 | the x-value, where f(x) = 0 100 | top_y: float 101 | y-value of the left limit 102 | bottom_y: float 103 | y-value of the right limit 104 | 105 | >>> f = linear(1, 10) 106 | >>> [round(f(x), 2) for x in arange(1, 11)] 107 | [0.0, 0.11, 0.22, 0.33, 0.44, 0.56, 0.67, 0.78, 0.89, 1.0] 108 | 109 | >>> f = linear(10, 1) 110 | >>> [round(f(x), 2) for x in arange(1, 11)] 111 | [1.0, 0.89, 0.78, 0.67, 0.56, 0.44, 0.33, 0.22, 0.11, 0.0] 112 | 113 | >>> f = linear(3, 5) 114 | >>> [round(f(x), 2) for x in arange(1, 6)] 115 | [0.0, 0.0, 0.0, 0.5, 1.0] 116 | 117 | >>> f = linear(7, 5) 118 | >>> [round(f(x), 2) for x in arange(5, 12)] 119 | [1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0] 120 | """ 121 | 122 | def __init__(self, bottom, top, top_y=1, bottom_y=0): 123 | self.top = float(top) 124 | self.bottom = float(bottom) 125 | self.top_y = top_y 126 | self.bottom_y = bottom_y 127 | self.gradient = (self.top_y - self.bottom_y) / (self.top - self.bottom) 128 | 129 | def __call__(self, x): 130 | return clip(self.gradient * (x - self.bottom) + self.bottom_y, 0, 1) 131 | 132 | 133 | class logistic(FuzzyFunction): 134 | """Derived from the logistic function. 135 | 136 | >>> f = logistic() 137 | >>> f(0) 138 | 0.01 139 | >>> round(f(100000), 2) 140 | 0.99 141 | >>> f(-5) 142 | 0 143 | """ 144 | 145 | def __init__(self, G=0.99, k=1, f_0=0.01): 146 | self.G = G 147 | self.k = k 148 | self.foo = (G / f_0 - 1.) # precalculated for a little performance 149 | 150 | def __call__(self, x): 151 | if x < 0: 152 | return 0 153 | return self.G * 1 / (1 + exp(-self.k * self.G * x) * self.foo) 154 | 155 | 156 | class rectangular(FuzzyFunction): 157 | """Basic rectangular function that returns the core_y for the core else 0. 158 | 159 | graph 160 | ----- 161 | ______ 162 | | | 163 | ____| |___ 164 | 165 | 166 | left: float 167 | minimum x-axis value for the rectangle 168 | right: float 169 | maximum x-axis value for the rectangle 170 | core_y: float 171 | "height" of the rectangle 172 | 173 | """ 174 | def __init__(self, left, right, core_y=1): 175 | assert left < right, "Left must be smaller than right." 176 | self.left = left 177 | self.right = right 178 | self.core_y = core_y 179 | 180 | def __call__(self, x): 181 | return self.core_y if self.left <= x <= self.right else 0 182 | 183 | 184 | class triangular(FuzzyFunction): 185 | """Basic triangular norm as combination of two linear functions. 186 | 187 | graph 188 | ----- 189 | /\ 190 | ____/ \___ 191 | 192 | 193 | vars 194 | ---- 195 | left 196 | minimal non-0 x-value 197 | right 198 | maximal non-0 x-value 199 | core 200 | x-value of the maximal y-value 201 | if None, the peak is in the middle of left and right 202 | core_y 203 | maximal y-value, normally 1 204 | 205 | """ 206 | def __init__(self, left, right, core=None, core_y=1): 207 | assert left < right, "left must be smaller than right" 208 | self.left = left 209 | self.right = right 210 | self.core = core if core is not None else (left + right) / 2. 211 | assert left < self.core < right, "core must be between the limits" 212 | self.core_y = core_y 213 | # piecewise-linear left side 214 | self.left_linear = linear(left, self.core, bottom_y=0, top_y=core_y) 215 | self.right_linear = linear(self.core, right, bottom_y=core_y, top_y=0) 216 | 217 | def __call__(self, x): 218 | return self.left_linear(x) if x <= self.core else self.right_linear(x) 219 | 220 | 221 | class trapezoid(FuzzyFunction): 222 | """Combination of rectangular and triangular, for convenience. 223 | 224 | graph 225 | ----- 226 | ____ 227 | / \ 228 | ____/ \___ 229 | 230 | vars 231 | ---- 232 | 233 | left 234 | for x <= left: f(x) == 0 235 | core_left and core_right 236 | for core_left <= x <= core_right: f(x) == core_y 237 | right 238 | for x >= right: f(x) == 0 239 | """ 240 | 241 | def __init__(self, left, core_left, core_right, right, height=1, 242 | inverse=False): 243 | assert left < core_left <= core_right < right, "Overlapping params." 244 | assert height >= 0, 'Core must not be negative' 245 | self.height = height 246 | self.left = left 247 | self.core_left = core_left 248 | self.core_right = core_right 249 | self.right = right 250 | self.inverse = inverse 251 | 252 | def __call__(self, x): 253 | if x < self.left: 254 | return 0 if not self.inverse else 1 255 | elif x < self.core_left: 256 | return (linear(self.left, self.core_left, right_y=self.height) 257 | if not self.inverse else 258 | linear(self.left, self.core_left, right_y=self.height, 259 | inverse=True)) 260 | elif x < self.core_right: 261 | return self.height if not self.inverse else self.inverse( 262 | self.height) 263 | elif x < self.right: 264 | return (linear(self.core_right, self.right, left_y=self.height, 265 | right_y=0) 266 | if not self.inverse else 267 | linear(self.core_right, self.right, left_y=self.height, 268 | inverse=True)) 269 | else: 270 | return 0 if not self.inverse else 1 271 | 272 | 273 | class sigmoid(FuzzyFunction): 274 | """Calculates a weight based on the sigmoid function. 275 | 276 | We specify the lower limit where f(x) = 0.1 and the 277 | upper with f(x) = 0.9 and calculate the steepness and elasticity 278 | based on these. We don't need the general logistic function as we 279 | operate on [0,1]. 280 | 281 | vars 282 | ---- 283 | left:float 284 | minimum x-value with f(x) = left_y (normally 0.1) 285 | right:float 286 | maximum x-value with f(x) = right_y (normally 0.9) 287 | left_y: float 288 | minimal y-value of the function 289 | right_y: float 290 | maximal y-value of the function 291 | 292 | >>> f = sigmoid(0, 1) 293 | >>> f(0) 294 | 0.1 295 | >>> round(f(100000), 2) 296 | 1.0 297 | >>> f(-100000) 298 | 0.0 299 | 300 | TODO: make actual use of left_y, right_y 301 | 302 | """ 303 | 304 | def __init__(self, left, right, left_y=0.1, right_y=0.9): 305 | assert left < right, "left must be smaller than right" 306 | self.k = -(4. * log(3)) / (left - right) 307 | self.o = 9 * exp(left * self.k) 308 | 309 | def __call__(self, x): 310 | try: 311 | return 1. / (1. + exp(-self.k * x) * self.o) 312 | except OverflowError: 313 | return 0.0 314 | 315 | 316 | class sigmoidB(FuzzyFunction): 317 | """A sigmoid variant with only one parameter. 318 | 319 | There's a catch: the curve starts high and grows low. 320 | 321 | 322 | >>> f = sigmoidB() 323 | 324 | >>> f(-1000) 325 | 1.0 326 | >>> f(0) 327 | 0.5 328 | >>> f(1000) 329 | 0.0 330 | >>> round(f(-20), 2) 331 | 0.99 332 | >>> round(f(20), 2) 333 | 0.01 334 | """ 335 | def __init__(self, k=0.229756): 336 | self.k = k 337 | 338 | def __call__(self, x): 339 | if x < -20: 340 | return 1.0 341 | if x > 20: 342 | return 0.0 343 | 344 | return 1 / (1 + exp(x * self.k)) 345 | 346 | 347 | class triangular_sigmoid(FuzzyFunction): 348 | def __init__(self, left, right, left_y=0, right_y=0, core=None, core_y=1): 349 | assert left < right, "left must be smaller than right" 350 | if core is not None: 351 | assert left < core < right, "core must be between the limits" 352 | self.core = core if core is not None else (left + right) / 2. 353 | self.core_y = core_y 354 | self.left_sigmoid = sigmoid(left, self.core, left_y, core_y) 355 | self.right_sigmoid = sigmoid(self.core, right, core_y, right_y) 356 | 357 | def __call__(self, x): 358 | if x < self.core: 359 | return self.left_sigmoid(x) 360 | elif self.core < x: 361 | return self.right_sigmoid(x) 362 | else: 363 | return self.core_y 364 | 365 | 366 | class gaussianA(FuzzyFunction): 367 | """Defined by ae^(-b(x-x0)^2), a gaussian distribution. 368 | Basically a triangular sigmoid function, it comes close to human perception. 369 | 370 | vars 371 | ---- 372 | a 373 | defines the maximum y-value of the graph 374 | b 375 | defines the steepness 376 | x0 377 | defines the symmetry center of the graph 378 | """ 379 | 380 | def __init__(self, b, x0, a=1): 381 | self.a = a 382 | self.b = b 383 | self.x0 = x0 384 | 385 | def __call__(self, x): 386 | return self.a * exp(-self.b * (x - self.x0)**2) 387 | 388 | 389 | class gaussian(FuzzyFunction): 390 | """Variant of the gaussian function that takes similar parameters as the others. 391 | 392 | left 393 | lower x-value for which f(x) == peak/10. (thus usually 0.1) 394 | right 395 | upper x-value for which f(x) == peak/10. 396 | peak 397 | maximum y-value of the graph 398 | """ 399 | def __init__(self, left, right, peak=1): 400 | self.left = left 401 | self.right = right 402 | self.peak = peak 403 | 404 | 405 | if __name__ == "__main__": 406 | import doctest 407 | doctest.testmod() 408 | -------------------------------------------------------------------------------- /.box/version 1/hedges.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lingual hedges modify curves describing truthvalues. 3 | """ 4 | 5 | 6 | def very(fit): 7 | return fit ** 2 8 | 9 | 10 | def plus(fit): 11 | return fit ** 1.25 12 | 13 | 14 | def minus(fit): 15 | return fit ** 0.75 16 | 17 | 18 | def highly(fit): 19 | return minus(very(very(fit))) 20 | 21 | 22 | if __name__ == "__main__": 23 | import doctest 24 | doctest.testmod() 25 | -------------------------------------------------------------------------------- /.box/version 1/inference.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def MAXMIN(sets): 4 | pass 5 | 6 | def MAXPROD(sets): 7 | pass 8 | -------------------------------------------------------------------------------- /.box/version 1/operators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Different linguistic terms (FuzzySets) are combined with these functions 3 | within rules. 4 | """ 5 | 6 | from base import FuzzyFunction 7 | 8 | 9 | class AND(FuzzyFunction): 10 | def __new__(cls, *args): 11 | return FuzzyFunction.__new__(cls, *args) 12 | 13 | def __init__(self, a, b): 14 | self.a = a 15 | self.b = b 16 | 17 | def __call__(self, x): 18 | return min(self.a(x), self.b(x)) 19 | 20 | 21 | class OR(FuzzyFunction): 22 | def __init__(self, a, b): 23 | self.a = a 24 | self.b = b 25 | 26 | def __call__(self, x): 27 | return max(self.a(x), self.b(x)) 28 | 29 | 30 | class NOT(FuzzyFunction): 31 | def __new__(cls, *args): 32 | return FuzzyFunction.__new__(cls, *args) 33 | 34 | def __init__(self, f): 35 | self.f = f 36 | 37 | def __call__(self, x): 38 | return 1 - self.f(x) 39 | 40 | 41 | def product(a, b): 42 | """Conjunction (AND) version.""" 43 | return a * b 44 | 45 | 46 | def bounded_sum(a, b): 47 | """Disjunction variant.""" 48 | a + b - a * b 49 | 50 | 51 | def lukasiewicz_OR(a, b): 52 | return max(0, a + b - 1) 53 | 54 | 55 | def lukasiewicz_AND(a, b): 56 | return min(a + b, 1) 57 | 58 | 59 | def einstein_sum_OR(a, b): 60 | """One of a few possible OR operators in fuzzy logic. 61 | 62 | a, b -- degrees of membership [0, 1] 63 | """ 64 | return (a + b) / (1 + a * b) 65 | 66 | 67 | def einstein_product_AND(a, b): 68 | """One of a few pos0sible AND operators in fuzzy logic. 69 | 70 | a, b -- degrees of membership [0, 1] 71 | """ 72 | return (a * b) / (2 - (a + b - a * b)) 73 | 74 | 75 | def hamacher_sum_OR(a, b): 76 | """Another version of the fuzzy OR.""" 77 | return (a + b - 2 * a * b) / (1 - a * b) 78 | 79 | 80 | def hamacher_product_AND(a, b): 81 | """Another version of the fuzzy AND.""" 82 | return (a * b) / (a + b - a * b) 83 | 84 | 85 | class gamma(object): 86 | """A compromise between fuzzy AND and OR. 87 | 88 | g (gamma-factor) 89 | 0 < g < 1 (g == 0 -> AND; g == 1 -> OR) 90 | """ 91 | def __init__(self, g): 92 | self.g = g 93 | 94 | def __call__(self, a, b): 95 | return (a * b) ** (1 - self.g) * ((1 - a) * (1 - b)) ** self.g 96 | -------------------------------------------------------------------------------- /.box/version 1/optimizedbase.py: -------------------------------------------------------------------------------- 1 | 2 | from numpy import arange 3 | from fuzzification import * 4 | 5 | from pylab import plot, show 6 | 7 | 8 | from timeit import Timer 9 | import cProfile 10 | 11 | 12 | 13 | class Domain(object): 14 | #__slots__ = ['name', 'limits', 'sets', 'resolution'] 15 | 16 | sets = {} 17 | 18 | def __init__(self, name, limits=(float('-inf'), float('+inf')), resolution=1.0): 19 | """ 20 | A domain normally is a single variable in a system that has a 21 | physical unit and a range of valid values. 22 | FuzzySets are defined within domains and only meaningful within their limits. 23 | 24 | Dmains (also called 'universes of discourse') can be used as keys in dicts. 25 | 26 | 27 | limits 28 | Tuple with minimum and maximum values, order determines the graph 29 | for visualisation. 30 | resolution 31 | Float that indicates the error margines of measurement and the steps 32 | for visualisation. 33 | """ 34 | assert limits[0] <= limits[1], 'Lower limit > upper limit!' 35 | self.name = name 36 | self.limits = limits 37 | self.resolution = resolution 38 | 39 | def __repr__(self): 40 | return self.name 41 | 42 | def __str__(self): 43 | return "Domain(%s)" % self.name 44 | 45 | def __getattr__(self, name): 46 | return self.sets[name] 47 | 48 | def __setattr__(self, name, value): 49 | if name == 'name': 50 | self.__dict__['name'] = value 51 | elif name == 'limits': 52 | self.__dict__['limits'] = value 53 | elif name == 'resolution': 54 | self.__dict__['resolution'] = value 55 | else: 56 | assert isinstance(value, FuzzyFunction), '%s is not a FuzzyFunction!' % value 57 | self.sets[name] = FuzzySet(self.name, value) 58 | 59 | class FuzzySet(object): 60 | __slots__ = ['domain', 'func'] 61 | 62 | def __init__(self, domain, fuzzyfunc=constant(1)): 63 | """A Fuzzy Set is responsible for mapping concrete values to fits and back. 64 | 65 | Within a domain of discourse, the same functions for (de-)fuzzyification 66 | should be used for the same meaning. 67 | 68 | This maps a certain value within the domain of discourse 69 | (temperature, length, velocity, ..) to a degree of membership. 70 | Using the specified function. 71 | 72 | vars 73 | ---- 74 | 75 | domain 76 | String to identify values of the same domain 77 | fuzzyfunc 78 | Function that is called exactly with 1 float and it returns a float 79 | within [0,1] to map values to degrees of membership. 80 | Functions of the fuzzification module take parameters at instantiation 81 | to define the behavior. 82 | 83 | 84 | """ 85 | self.domain = domain 86 | self.func = fuzzyfunc 87 | 88 | def __call__(self, value): 89 | return self.func(value) 90 | 91 | def __and__(self, other): 92 | assert isinstance(other, FuzzySet), 'Only FuzzySets can be combined, other is %s' % type(other) 93 | return FuzzySet(self.domain, lambda x: min(self(x), other(x))) 94 | 95 | def __or__(self, other): 96 | assert isinstance(other, FuzzySet), 'Only FuzzySets can be combined, other is %s' % type(other) 97 | return FuzzySet(self.domain, lambda x: max(self(x), other(x))) 98 | 99 | def __invert__(self): 100 | return FuzzySet(self.domain, ~self.func) 101 | 102 | def __iter__(self): 103 | for x in arange(self.domain.limits[0], self.domain.limits[1] + 1, self.domain.resolution): 104 | yield self.func(x) 105 | 106 | def draw(self): 107 | try: 108 | r = arange(self.domain.limits[0], self.domain.limits[1] + 1, self.domain.resolution) 109 | v = [self.func(x) for x in r] 110 | plot(r, v) 111 | show() 112 | except ValueError: 113 | print "Can't draw fuzzyset, domain %s has no limits." % self.domain 114 | 115 | 116 | class Rule(object): 117 | def __init__(self, IF, THEN, believe=1): 118 | pass 119 | 120 | if __name__ == "__main__": 121 | temp = 25 122 | month = 9 123 | 124 | d = Domain('d') 125 | a = FuzzySet(d) 126 | 127 | def test(): 128 | return ~~~~~~a(8.6) 129 | 130 | cProfile.run('test()') 131 | print Timer(test).timeit() 132 | 133 | temp = Domain("temperature", limits=(0,100)) 134 | temp.hot = linearB(5, 35) 135 | 136 | mild = temp.hot & ~temp.hot 137 | 138 | 139 | import doctest 140 | doctest.testmod() 141 | -------------------------------------------------------------------------------- /.box/version 1/setmodifications.py: -------------------------------------------------------------------------------- 1 | """These functions operate on a FuzzySet at a time to modify its shape. 2 | 3 | 4 | sources 5 | ------- 6 | 7 | http://kik.informatik.fh-dortmund.de/abschlussarbeiten/fuzzyControl/operatoren.html 8 | """ 9 | 10 | 11 | def shift_to_middle(x): 12 | """This function maps [0,1] -> [0,1] with bias towards 0.5. 13 | 14 | For instance this is needed to dampen extremes. 15 | """ 16 | return 1/2 + 4 * (x - 1/2)**3 17 | 18 | def inverse(x): 19 | return 1-x 20 | 21 | def concentration(x): 22 | """Used to reduce the amount of values the set includes and to dampen the 23 | membership of many values. 24 | """ 25 | return x**2 26 | 27 | def contrast_intensification(x): 28 | """Increases the membership of values that already strongly belong to the set 29 | and dampens the rest. 30 | """ 31 | 32 | if x < 0.5: 33 | return 2 * x**2 34 | else: 35 | return 1 - 2(1 - x**2) 36 | 37 | def dilatation(x): 38 | """Expands the set with more values and already included values are enhanced.""" 39 | return x ** 1./2. 40 | 41 | def multiplication(x, n): 42 | """Set is multiplied with a constant factor, which changes all membership values.""" 43 | return x * n 44 | 45 | def negation(x): 46 | return 1 - x 47 | -------------------------------------------------------------------------------- /.box/version 1/tools.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | 5 | def normalize(lst): 6 | high = max(lst) 7 | return [value/float(high) for value in lst] 8 | 9 | def center_of_gravity(A): 10 | """ 11 | >>> from fuzzification import singleton 12 | >>> r = np.arange(0, 10, 1) 13 | >>> f = singleton(3) 14 | >>> A = [f(x) for x in r] 15 | >>> center_of_gravity(A) 16 | 3.0 17 | >>> from fuzzification import rectangular 18 | >>> f = rectangular(2,4) 19 | >>> A = [f(x) for x in r] 20 | >>> center_of_gravity(A) 21 | 3.0 22 | >>> from fuzzification import linear 23 | >>> f = linear(0,1) 24 | >>> A = [f(x) for x in r] 25 | >>> center_of_gravity(A) 26 | 5.0 27 | """ 28 | return (1. / np.sum(A)) * sum(a*x for a,x in enumerate(A)) 29 | 30 | if __name__ == "__main__": 31 | import doctest 32 | doctest.testmod() 33 | 34 | import cProfile 35 | from timeit import Timer 36 | 37 | from .fuzzification import constant 38 | 39 | def test1(): 40 | r = np.arange(0,100,1) 41 | f = constant(1) 42 | A = [f(x) for x in r] 43 | center_of_gravity(A) 44 | 45 | f = constant(1) 46 | r = np.arange(0, 100, 1) 47 | A = np.array([f(x) for x in r]) 48 | 49 | def test2(): 50 | center_of_gravity(A) 51 | 52 | 53 | if __name__ == "__main__": 54 | import doctest 55 | doctest.testmod() 56 | -------------------------------------------------------------------------------- /.github/workflows/docsupdater.yaml: -------------------------------------------------------------------------------- 1 | name: Update docs/docs/index.md 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update-docs: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Generate docs/docs/index.md 17 | run: | 18 | echo "Generating docs/docs/index.md..." 19 | cat README.md > docs/docs/index.md 20 | 21 | - name: Commit changes 22 | run: | 23 | git add docs/docs/index.md 24 | if [ -n "$(git status --porcelain)" ]; then 25 | git commit -m "Update docs/docs/index.md" 26 | git push 27 | else 28 | echo "No changes to commit" 29 | fi 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hypothesis 2 | .git 3 | .cache/* 4 | __pycache__ 5 | .pytest_cache/* 6 | .ipynb_checkpoints/ 7 | .pytest_cache 8 | build/* 9 | dist/* 10 | DEV* 11 | TODO-fuzzy 12 | box 13 | 14 | /fuzzylogic-* 15 | workspace.code-workspace 16 | src/fuzzylogic.egg* 17 | src/fuzzylogic/.test.py 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | mkdocs: 20 | configuration: docs/mkdocs.yml 21 | fail_on_warning: false 22 | 23 | # Optional but recommended, declare the Python requirements required 24 | # to build your documentation 25 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 26 | python: 27 | install: 28 | - requirements: docs/requirements.txt 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | "python.analysis.extraPaths": [ 8 | "./src" 9 | ] 10 | } -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Kiefner" 5 | given-names: "Anselm" 6 | orcid: "https://orcid.org/0000-0002-8009-0733" 7 | title: "FuzzyLogic for Python" 8 | version: 1.2.0 9 | doi: 10.5281/zenodo.6881817 10 | date-released: 2022-02-15 11 | url: "https://github.com/amogorkon/fuzzylogic" 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you want to contribute, you can do so by pull requests on github or by posting issues. 2 | Formatting will be enforced by https://github.com/psf/black, so you may or may not follow PEP8. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2025 Anselm Kiefner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuzzy Logic for Python 3 2 | 3 | [![license](https://img.shields.io/github/license/amogorkon/fuzzylogic)](https://github.com/amogorkon/fuzzylogic/blob/master/LICENSE) 4 | [![stars](https://img.shields.io/github/stars/amogorkon/fuzzylogic?style=plastic)](https://github.com/amogorkon/fuzzylogic/stargazers) 5 | [![forks](https://img.shields.io/github/forks/amogorkon/fuzzylogic?style=plastic)](https://github.com/amogorkon/fuzzylogic/network/members) 6 | [![Downloads](https://pepy.tech/badge/fuzzylogic)](https://pepy.tech/project/fuzzylogic) 7 | 8 | 9 | This is the fourth time I rebuilt this library from scratch to find the sweet spot between ease of use (beautiful is better than ugly!), testability (simple is better than complex!) and potential for performance optimization (practicality beats purity!). 10 | 11 | ### Why a new library? 12 | The first time I was confronted with fuzzy logic, I fell in love with the concept, but after reading books and checking out libraries etc. I found it frustrating how most people make fuzzy logic appear complicated, hard to handle and incorporate in code. 13 | Sure, there are frameworks that allow modelling of functions via GUI, but that's not a solution for a coder, right? Then there's a ton of mathematical research and other cruft that no normal person has time and patience to work through before trying to explore and applying things. Coming from this direction, there are also a number of script-ish (DSL) language frameworks that try to make the IF THEN ELSE pattern work (which I also tried in python, but gave it up because it just looks ugly). 14 | And yes, it's also possible to implement the whole thing completely in a functional style, but you really don't want to work with a recursive structure of 7+ steps by hand, trying not to miss a (..) along the way. 15 | Finally, most education on the subject emphasize sets and membership functions, but fail to mention the importance of the domain (or "universe of discourse"). It's easy to miss this point if you get lost with set operations and membership values, which are actually not that difficult once you can *play* and *explore* how these things look and work! 16 | 17 | ### The Idea 18 | So, the idea is to have three parts that work together: domains, sets and rules. Each of these classes wrap additional logic around basic building blocks - Set gives logical operations to simple functions, Domain gives additional logic to numpy arrays and groups Sets together while Rule combines different Domains. You start modelling your system by defining your domain of interest. Then you think about where your interesting points are in that domain and look for a function that might do what you want. In general, fuzzy.functions map any value to [0,1], that's all. Simply wrap the function in a Set and assign it to the domain in question. Once assigned, you can plot that set and see if it actually looks how you imagined. Now that you have one or more sets, you also can start to combine them with set operations &, |, ~, etc. It's fairly straight forward. 19 | Finally, use the Rules to map input domain to output domain to actually control stuff. 20 | ### Warning: Magic 21 | To make it possible to write fuzzy logic in the most pythonic and simplest way imaginable, it was necessary to employ some magic tricks that normally are discouraged, but at least there's no black magic involved (aka meta-programming etc.), so things are easy to debug if there is a problem. Most notably: 22 | * all functions are recursive closures (which makes it kinda hard to serialize things, if you really want to do that) 23 | * The main classes use a lot of dunder methods to implement their logic, which can be a bit daunting at first glance 24 | * Domain and Set uses an assignment trick to make it possible to instantiate Set() without passing domain and name over and over (yet still be explicit, just not the way one would normally expect). This also allows to call sets as Domain.attributes, which also normally shouldn't be possible (since they are technically not attributes). However, this allows interesting things like dangling sets (sets without domains) that can be freely combined with other sets to avoid cluttering of domain-namespaces and just have the resulting set assigned to a domain to work with. 25 | 26 | # Installation 27 | Just enter 28 | `python -m pip install fuzzylogic` 29 | in a commandline prompt and you should be good to go! 30 | 31 | It's even more fun to experiment with it in [Jupyter Lab](https://jupyter.org) :-) 32 | 33 | # Documentation 34 | Thanks to [Atul Kushwaha](https://github.com/coderatul), we now have an amazing [documentation](https://fuzzylogic.readthedocs.io/en/latest/) including our [Showcase](https://github.com/amogorkon/fuzzylogic/blob/master/docs/Showcase.ipynb) - check it out! 35 | 36 | # Office Hours 37 | You can also contact me one-on-one! Check my [office hours](https://calendly.com/amogorkon/officehours) to set up a meeting :-) 38 | 39 | -- Anselm Kiefner 40 | -------------------------------------------------------------------------------- /docs/Tipping Problem.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"\n", 10 | "https://pythonhosted.org/scikit-fuzzy/auto_examples/plot_tipping_problem.html\n", 11 | "\"\"\"\n", 12 | "\n", 13 | "from fuzzylogic.classes import Domain, Rule\n", 14 | "from fuzzylogic.functions import R, S, triangular\n", 15 | "\n", 16 | "res = 1\n", 17 | "\n", 18 | "food = Domain(\"food_quality\", 0, 10, res=res)\n", 19 | "serv = Domain(\"service_quality\", 0, 10, res=res)\n", 20 | "tip = Domain(\"tip_amount\", 0, 25, res=res)\n", 21 | "\n", 22 | "\n", 23 | "food.lo = S(0, 5)\n", 24 | "food.md = triangular(0, 10)\n", 25 | "food.hi = R(5, 10)\n", 26 | "\n", 27 | "serv.lo = S(0, 5)\n", 28 | "serv.md = triangular(0, 10)\n", 29 | "serv.hi = R(5, 10)\n", 30 | "\n", 31 | "tip.lo = S(0, 13)\n", 32 | "tip.md = triangular(0, 25)\n", 33 | "tip.hi = R(13, 25)\n", 34 | "\n", 35 | "R1 = Rule({(food.lo, serv.lo): tip.lo})\n", 36 | "R2 = Rule({serv.md: tip.md})\n", 37 | "R2x = Rule({(serv.md,): tip.md})\n", 38 | "R2a = Rule({(food.lo | food.md | food.hi, serv.md): tip.md})\n", 39 | "R2b = Rule({(food.lo & food.md & food.hi, serv.md): tip.md})\n", 40 | "R3 = Rule({(food.hi, serv.hi): tip.hi})\n", 41 | "\n", 42 | "rules = Rule({\n", 43 | " (food.lo, serv.lo): tip.lo,\n", 44 | " food.md: tip.md,\n", 45 | " (food.hi, serv.hi): tip.hi,\n", 46 | " })\n", 47 | "\n", 48 | "values = {food: 6.5, serv: 9.8}\n" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 2, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "data": { 58 | "text/plain": [ 59 | "(np.float64(12.019230769230766), np.float64(12.019230769230766))" 60 | ] 61 | }, 62 | "execution_count": 2, 63 | "metadata": {}, 64 | "output_type": "execute_result" 65 | } 66 | ], 67 | "source": [ 68 | "R2(values), R2x(values)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 3, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "data": { 78 | "text/plain": [ 79 | "(0, 0.7, 0.3)" 80 | ] 81 | }, 82 | "execution_count": 3, 83 | "metadata": {}, 84 | "output_type": "execute_result" 85 | } 86 | ], 87 | "source": [ 88 | "food.lo(6.5), food.md(6.5), food.hi(6.5)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 4, 94 | "metadata": {}, 95 | "outputs": [ 96 | { 97 | "data": { 98 | "text/plain": [ 99 | "(0, 0.039999999999999813, 0.9600000000000002)" 100 | ] 101 | }, 102 | "execution_count": 4, 103 | "metadata": {}, 104 | "output_type": "execute_result" 105 | } 106 | ], 107 | "source": [ 108 | "serv.lo(9.8), serv.md(9.8), serv.hi(9.8)" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 5, 114 | "metadata": {}, 115 | "outputs": [ 116 | { 117 | "data": { 118 | "text/plain": [ 119 | "(0, 0.7, 0.9600000000000002)" 120 | ] 121 | }, 122 | "execution_count": 5, 123 | "metadata": {}, 124 | "output_type": "execute_result" 125 | } 126 | ], 127 | "source": [ 128 | "max(food.lo(6.5), serv.lo(9.8)), max(food.md(6.5), serv.md(9.8)), max(food.hi(6.5), serv.hi(9.8))" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 6, 134 | "metadata": {}, 135 | "outputs": [ 136 | { 137 | "data": { 138 | "text/plain": [ 139 | "np.float64(20.512820512820507)" 140 | ] 141 | }, 142 | "execution_count": 6, 143 | "metadata": {}, 144 | "output_type": "execute_result" 145 | } 146 | ], 147 | "source": [ 148 | "R3(values)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 10, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "data": { 158 | "text/plain": [ 159 | "0.039999999999999813" 160 | ] 161 | }, 162 | "execution_count": 10, 163 | "metadata": {}, 164 | "output_type": "execute_result" 165 | } 166 | ], 167 | "source": [ 168 | "serv.md(9.8)" 169 | ] 170 | } 171 | ], 172 | "metadata": { 173 | "kernelspec": { 174 | "display_name": "Python 3", 175 | "language": "python", 176 | "name": "python3" 177 | }, 178 | "language_info": { 179 | "codemirror_mode": { 180 | "name": "ipython", 181 | "version": 3 182 | }, 183 | "file_extension": ".py", 184 | "mimetype": "text/x-python", 185 | "name": "python", 186 | "nbconvert_exporter": "python", 187 | "pygments_lexer": "ipython3", 188 | "version": "3.12.3" 189 | } 190 | }, 191 | "nbformat": 4, 192 | "nbformat_minor": 2 193 | } 194 | -------------------------------------------------------------------------------- /docs/ai workflow.md: -------------------------------------------------------------------------------- 1 | # AI Query 2 | 3 | * `Current code. Python 3.12 only, no backwards compatibility. I will post 3 files. Understand and acknowledge only, don't suggest anything.` 4 | * *post classes.py* 5 | * *post functions.py* 6 | * *post combinators.py* 7 | * `Now that I have posted and the code, let's start. Here is what I need help with:` 8 | 9 | 10 | # Required Code Structure 11 | 12 | * The name of the file needs to be on the first line of the file as a comment like `# filename.py` 13 | so it can be easily copyed and pasted into a query. 14 | * Alternatively, we could use a module docstring with the filename in it. This would also be compatible with a hashbang. 15 | ``` 16 | #!/usr/bin/env python3 17 | """ 18 | pyping.py - This file does XYZ... 19 | """ 20 | ``` 21 | * Each file must be less than 10000 characters (about 300 lines) because queries are limited. 22 | * As the AI can figure out the logic of the code, line comments are not needed, and may even be distracting and counterproductive. Comments also take up valuable characters in the query, so they should be avoided. 23 | -------------------------------------------------------------------------------- /docs/docs/Discussion.md: -------------------------------------------------------------------------------- 1 | # Discussion on Fuzzy Logic 2 | - To discuss your thoughts and getting involved in existing discussions visit [github discussion page](https://github.com/amogorkon/fuzzylogic/discussions) 3 | 4 | 19 | -------------------------------------------------------------------------------- /docs/docs/How-To.md: -------------------------------------------------------------------------------- 1 | # FAQ'S 2 | 3 | Q: I'm triying to make a simple rule IF A then B 4 | but if I write 5 | ``` 6 | R1 = Rule({A: B}) 7 | R1 = Rule({temp.hot: motor.fast}) 8 | ``` 9 | Gives me an error -> TypeError: 'Set' object is not iterable 10 | 11 | A: Rules are there to map sets of one domain to a single set of another, so it expects the left side to be an iterable like a tuple.Try `R1 = Rule({(temp.hot, ): motor.fast})` That should do the trick. 12 | 13 | --- 14 | 15 | Q: I'm wondering which papers/books you followed for the program. I just started learning fuzzy logic and can't find a good paper or book. Thanks a lot. 16 | 17 | A: I've learned the basics of fuzzy logic in my CS study at university, many years ago, but I can't give you those resources. 18 | The book I followed mostly, idea and definition-wise is [Fuzzy Logic and Control: Software and Hardware Applications, Vol. 2 by Mohammad Jamshidi, Nader Vadiee and Timothy Ross]( https://books.google.de/books/about/Fuzzy_Logic_and_Control.html?id=fN9SAAAAMAAJ&redir_esc=y ) 19 | 20 | Oh, and there are excellent youtube videos on the subject that also inspired me: 21 | [Lecture series by Prof S Chakraverty](https://www.youtube.com/watch?v=oWqXwCEfY78 ) 22 | and [examples like](https://youtu.be/R4TPFpYXvS0) 23 | 24 | -------------------------------------------------------------------------------- /docs/docs/Showcase.md: -------------------------------------------------------------------------------- 1 | # Fuzzy Logic for Python 3 2 | 3 | The doctests in the modules should give a good idea how to use things by themselves, while here are some examples how to use everything together. 4 | 5 | ## Installation 6 | First things first: To install fuzzylogic, just enter `python -m pip install fuzzylogic` and you should be good to go! 7 | 8 | ## Functions and Sets 9 | Defining a domain with its range and resolution should be trivial since most real world instruments come with those specifications. However, defining the fuzzy sets within those domains is where the fun begins as only a human can tell whether something is "hot" or "not", right? 10 | 11 | Why the distinction? Functions only map values, nothing special there at all - which is good for testing and performance. Sets on the other hand implement logical operations that have special python syntax, which makes it easy to work with but a little more difficult to test and adds some performance overhead. So, sets are for abstraction and easy handling, functions for performance. 12 | 13 | ## Domains 14 | You can use (I do so regularly) fuzzy functions outside any specific fuzzy context. However, if you want to take advantage of the logic of fuzzy sets, plot stuff or defuzzyfy values, you need to use Domains. Domains and Sets are special in a way that they intrically rely on each other. This is enforced by how assignments work. Regular Domain attributes are the sets that were assigned to the domain. Also, if just a function is assigned it is automatically wrapped in a Set. 15 | 16 | 17 | ```python 18 | from matplotlib import pyplot 19 | pyplot.rc("figure", figsize=(10, 10)) 20 | ``` 21 | 22 | 23 | ```python 24 | from fuzzylogic.classes import Domain 25 | from fuzzylogic.functions import R, S, alpha 26 | 27 | T = Domain("test", 0, 30, res=0.1) 28 | ``` 29 | 30 | 31 | ```python 32 | T.up = R(1,10) 33 | T.up.plot() 34 | ``` 35 | 36 | 37 | 38 | ![png](Showcase_files/Showcase_5_0.png) 39 | 40 | 41 | 42 | 43 | ```python 44 | T.down = S(20, 29) 45 | T.down.plot() 46 | ``` 47 | 48 | 49 | 50 | ![png](Showcase_files/Showcase_6_0.png) 51 | 52 | 53 | 54 | 55 | ```python 56 | T.polygon = T.up & T.down 57 | T.polygon.plot() 58 | ``` 59 | 60 | 61 | 62 | ![png](Showcase_files/Showcase_7_0.png) 63 | 64 | 65 | 66 | 67 | ```python 68 | T.inv_polygon = ~T.polygon 69 | T.inv_polygon.plot() 70 | ``` 71 | 72 | 73 | 74 | ![png](Showcase_files/Showcase_8_0.png) 75 | 76 | 77 | 78 | let's show off a few interesting functions ;) 79 | 80 | 81 | ```python 82 | from fuzzylogic.classes import Domain, Set 83 | from fuzzylogic.functions import (sigmoid, gauss, trapezoid, 84 | triangular_sigmoid, rectangular) 85 | 86 | T = Domain("test", 0, 70, res=0.1) 87 | T.sigmoid = sigmoid(1,1,20) 88 | T.sigmoid.plot() 89 | T.gauss = gauss(10, 0.01, c_m=0.9) 90 | T.gauss.plot() 91 | T.trapezoid = trapezoid(25, 30, 35, 40, c_m=0.9) 92 | T.trapezoid.plot() 93 | T.triangular_sigmoid = triangular_sigmoid(40, 70, c=55) 94 | T.triangular_sigmoid.plot() 95 | ``` 96 | 97 | 98 | 99 | ![png](Showcase_files/Showcase_10_0.png) 100 | 101 | 102 | 103 | ## More on Domains 104 | 105 | After specifying the domain and assigning sets, calling a domain with a value returns a dict of memberships of the sets in that domain. 106 | 107 | 108 | ```python 109 | from fuzzylogic.classes import Domain 110 | from fuzzylogic.functions import alpha, triangular 111 | from fuzzylogic.hedges import plus, minus, very 112 | 113 | numbers = Domain("numbers", 0, 20, res=0.1) 114 | 115 | close_to_10 = alpha(floor=0.2, ceiling=0.8, func=triangular(0, 20)) 116 | close_to_5 = triangular(1, 10) 117 | 118 | numbers.foo = minus(close_to_5) 119 | numbers.bar = very(close_to_10) 120 | 121 | numbers.bar.plot() 122 | numbers.foo.plot() 123 | numbers.baz = numbers.foo + numbers.bar 124 | numbers.baz.plot() 125 | 126 | numbers(8) 127 | ``` 128 | 129 | 130 | 131 | 132 | {Set(.f at 0x7f2ba31c34c0>): 0.5443310539518174, 133 | Set(.f at 0x7f2ba31c3550>): 0.6400000000000001, 134 | Set(.f at 0x7f2ba31ba4c0>): 0.8359591794226543} 135 | 136 | 137 | 138 | 139 | 140 | ![png](Showcase_files/Showcase_12_1.png) 141 | 142 | 143 | 144 | 145 | ```python 146 | from fuzzylogic.classes import Domain 147 | from fuzzylogic.functions import bounded_sigmoid 148 | 149 | T = Domain("temperature", 0, 100, res=0.1) 150 | T.cold = bounded_sigmoid(5,15, inverse=True) 151 | T.cold.plot() 152 | T.hot = bounded_sigmoid(20, 40) 153 | T.hot.plot() 154 | T.warm = ~T.hot & ~T.cold 155 | T.warm.plot() 156 | T(10) 157 | ``` 158 | 159 | 160 | 161 | 162 | {Set(.f at 0x7f2ba31baee0>): 0.5, 163 | Set(.f at 0x7f2ba305bee0>): 0.012195121951219511, 164 | Set(.F at 0x7f2ba3045550>): 0.5} 165 | 166 | 167 | 168 | 169 | 170 | ![png](Showcase_files/Showcase_13_1.png) 171 | 172 | 173 | 174 | Many times you end up with sets that never hit 1 like with sigmoids, triangular funcs that hit the border of the domain or after operations with other sets. Then it is often needed to normalize (define max(set) == 1). Note that Set.normalized() returns a set that (unlike other set ops) is already bound to the domain and given the name "normalized\_{set.name}". This can't be circumvented because normalizing is only defined on a given domain. 175 | 176 | 177 | ```python 178 | from fuzzylogic.classes import Domain 179 | from fuzzylogic.functions import alpha, trapezoid 180 | 181 | N = Domain("numbers", 0, 6, res=0.01) 182 | N.two_or_so = alpha(floor=0, ceiling=0.7, func=trapezoid(0, 1.9, 2.1, 4)) 183 | N.two_or_so.plot() 184 | N.x = N.two_or_so.normalized() 185 | N.x.plot() 186 | ``` 187 | 188 | 189 | 190 | ![png](Showcase_files/Showcase_15_0.png) 191 | 192 | 193 | 194 | ## Inference 195 | 196 | After measuring a RL value and mapping it to sets within a domain, it is normally needed to translate the result to another domain that corresponds to some sort of control mechanism. This translation or mapping is called inference and is rooted in the logical conclusion operation A => B, for example: If it rains then the street is wet. 197 | The street may be wet for a number of reasons, but if it rains it will be wet for sure. This **IF A THEN B** can also be written as 198 | ***(A AND B) OR NOT(A AND TRUE)***. This may look straight forward for boolean logic, but since we are not just dealing with True and False, there are a number of ways in fuzzy logic to actually implement this. 199 | 200 | Here is a simple but fully working example with all moving parts, demonstrating the use in the context of an HVAC system. 201 | 202 | It also demonstrates the three different ways to set up complex combinations of rules: you can either define each rule one by one and then combine them via the | operator, or you can put the rules into a list and use sum(..) to combine them into one in a single step, or you can define one big and complex rule right from the start. Which way best suits your needs depends on how complex each rule is and how/where you define them in your code and whether you need to use them in different places in different combinations. 203 | 204 | 205 | ```python 206 | from fuzzylogic.classes import Domain, Set, Rule 207 | from fuzzylogic.hedges import very 208 | from fuzzylogic.functions import R, S 209 | 210 | temp = Domain("Temperature", -80, 80) 211 | hum = Domain("Humidity", 0, 100) 212 | motor = Domain("Speed", 0, 2000) 213 | 214 | temp.cold = S(0,20) 215 | temp.hot = R(15,30) 216 | 217 | hum.dry = S(20,50) 218 | hum.wet = R(40,70) 219 | 220 | motor.fast = R(1000,1500) 221 | motor.slow = ~motor.fast 222 | 223 | R1 = Rule({(temp.hot, hum.dry): motor.fast}) 224 | R2 = Rule({(temp.cold, hum.dry): very(motor.slow)}) 225 | R3 = Rule({(temp.hot, hum.wet): very(motor.fast)}) 226 | R4 = Rule({(temp.cold, hum.wet): motor.slow}) 227 | 228 | rules = Rule({(temp.hot, hum.dry): motor.fast, 229 | (temp.cold, hum.dry): very(motor.slow), 230 | (temp.hot, hum.wet): very(motor.fast), 231 | (temp.cold, hum.wet): motor.slow, 232 | }) 233 | 234 | rules == R1 | R2 | R3 | R4 == sum([R1, R2, R3, R4]) 235 | 236 | values = {hum: 45, temp: 22} 237 | print(R1(values), R2(values), R3(values), R4(values), "=>", rules(values)) 238 | ``` 239 | 240 | 1610.5651371516108 None 1655.6798260836274 None => 1633.122481617619 241 | 242 | 243 | There are a few things to note in this example. Firstly, make sure to pass in the values as a single dictionary at the end, not as parameters. 244 | If a rule has zero weight - in this example a temp of 22 results in cold weighted with S(0,20) as 0 - the Rule returns None, which makes sure this condition is ignored in subsequent calculations. Also you might notice that the result is a value of 1633, which is way more than motor.fast with R(1000,1500) would suggest. However, since the domain motor is defined between 0 and 2000, the center of gravity method used for evaluation takes many of the values between 1500 and 2000 weighted with 1 into account, giving this slightly unexpected result. 245 | 246 | Writing a single Rule or a few this way is explicit and simple to understand, but considering only 2 input variables with 3 Sets each, this already means 9 lines of code, with each element repeated 3 times. Also, most resources like books present these rules as tables or spreadsheets, which can be overly complicated or at least annoying to rewrite as explicit Rules. 247 | To help with such cases, I've added the ability to transform 2D tables into Rules. This functionality uses pandas to import a table written as multi-line string for now, but it would be easy to accept Excel spreadsheets, csv and all the other formats supported by pandas. However, this functionality comes with three potential drawbacks: The content of cells are strings - which has no IDE support (so *typos might slip through*), which are eval()ed - which *can pose a security risk if the table is provided by an untrusted source*. Thirdly, *only rules with 2 input variables can be modelled this way*, unlike 'classic' Rules which allow an unlimited number of input variables. 248 | 249 | Please note, the second argument to this function must be the namespace where all Domains, Sets and hedges are defined that are used within the table. This is due to the fact that the evaluation of those names actually happens in a module where those names normally aren't defined. 250 | 251 | 252 | ```python 253 | table = """ 254 | hum.dry hum.wet 255 | temp.cold very(motor.slow) motor.slow 256 | temp.hot motor.fast very(motor.fast) 257 | """ 258 | from fuzzylogic.classes import rule_from_table 259 | table_rules = rule_from_table(table, globals()) 260 | assert table_rules == rules 261 | ``` 262 | 263 | As you can see, the above table_rules is equal to the rules as constructed one at a time earlier, but much less verbose. No element had to be repeated and it is much easier to read. 264 | 265 | ## Sources 266 | * Fuzzy Logic and Control: Software and Hardware Applications, Volume 2 267 | 268 | By: Mohammad Jamshidi; Nader Vadiee; Timothy J. Ross - University of New Mexico 269 | Publisher: Prentice Hall 270 | Pub. Date: June 07, 1993 271 | 272 | * Computational Intelligence - Fuzzy-Tutorial 273 | 274 | Prof. Dr. habil. A. Grauel 275 | 276 | * http://petro.tanrei.ca/fuzzylogic/fuzzy_negnevistky.html 277 | * http://kik.informatik.fh-dortmund.de/abschlussarbeiten/fuzzyControl/operatoren.html 278 | * [Fundamentals of Fuzzy Logic Control – Fuzzy Sets, Fuzzy Rules and Defuzzifications 279 | Ying Bai and Dali Wang](https://www.researchgate.net/profile/Ying_Bai/publication/225872318_Fundamentals_of_Fuzzy_Logic_Control_-_Fuzzy_Sets_Fuzzy_Rules_and_Defuzzifications/links/0fcfd5057a874858b1000000/Fundamentals-of-Fuzzy-Logic-Control-Fuzzy-Sets-Fuzzy-Rules-and-Defuzzifications.pdf) 280 | 281 | -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_10_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_10_0.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_12_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_12_1.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_13_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_13_1.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_15_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_15_0.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_5_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_5_0.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_6_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_6_0.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_7_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_7_0.png -------------------------------------------------------------------------------- /docs/docs/Showcase_files/Showcase_8_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/docs/docs/Showcase_files/Showcase_8_0.png -------------------------------------------------------------------------------- /docs/docs/classes.md: -------------------------------------------------------------------------------- 1 | Module fuzzylogic.classes 2 | ========================= 3 | Domain, Set and Rule classes for fuzzy logic. 4 | 5 | Primary abstractions for recursive functions and arrays, 6 | adding logical operaitons for easier handling. 7 | 8 | Domain 9 | ------- 10 | 11 | `Domain(name: str, low: float, high: float, res=float | int, sets: dict = None)` 12 | : A domain is a 'measurable' dimension of 'real' values like temperature. 13 | 14 | There must be a lower and upper limit and a resolution (the size of steps) 15 | specified. 16 | 17 | Fuzzysets are defined within one such domain and are only meaningful 18 | while considered within their domain ('apples and bananas'). 19 | To operate with sets across domains, there needs to be a mapping. 20 | 21 | 22 | The sets are accessed as attributes of the domain like 23 | >>> temp = Domain('temperature', 0, 100) 24 | >>> temp.hot = Set(lambda x: 0) 25 | >>> temp.hot(5) 26 | 0 27 | 28 | 29 | 30 | It is possible now to call derived sets without assignment first! 31 | 32 | >>> from .hedges import very 33 | >>> (very(~temp.hot) | ~very(temp.hot))(2) 34 | 1 35 | 36 | You MUST NOT add arbitrary attributes to an *instance* of Domain - you can 37 | however subclass or modify the class itself. If you REALLY have to add attributes, 38 | make sure to "whitelist" it in __slots__ first. 39 | 40 | Use the Domain by calling it with the value in question. This returns a 41 | dictionary with the degrees of membership per set. You MAY override __call__ 42 | in a subclass to enable concurrent evaluation for performance improvement. 43 | 44 | Define a domain. 45 |
46 | click here to see Domain class source code 47 | 48 | ``` 49 | class Domain: 50 | """ 51 | A domain is a 'measurable' dimension of 'real' values like temperature. 52 | 53 | There must be a lower and upper limit and a resolution (the size of steps) 54 | specified. 55 | 56 | Fuzzysets are defined within one such domain and are only meaningful 57 | while considered within their domain ('apples and bananas'). 58 | To operate with sets across domains, there needs to be a mapping. 59 | 60 | The sets are accessed as attributes of the domain like 61 | >>> temp = Domain('temperature', 0, 100) 62 | >>> temp.hot = Set(lambda x: 0) 63 | >>> temp.hot(5) 64 | 0 65 | 66 | It is possible now to call derived sets without assignment first! 67 | >>> from .hedges import very 68 | >>> (very(~temp.hot) | ~very(temp.hot))(2) 69 | 1 70 | 71 | You MUST NOT add arbitrary attributes to an *instance* of Domain - you can 72 | however subclass or modify the class itself. If you REALLY have to add attributes, 73 | make sure to "whitelist" it in __slots__ first. 74 | 75 | Use the Domain by calling it with the value in question. This returns a 76 | dictionary with the degrees of membership per set. You MAY override __call__ 77 | in a subclass to enable concurrent evaluation for performance improvement. 78 | """ 79 | 80 | __slots__ = ["_name", "_low", "_high", "_res", "_sets"] 81 | 82 | def __init__(self, name: str, low: float, high: float, res=float | int, sets: dict = None): 83 | """Define a domain.""" 84 | assert low < high, "higher bound must be greater than lower." 85 | assert res > 0, "resolution can't be negative or zero" 86 | self._name = name 87 | self._high = high 88 | self._low = low 89 | self._res = res 90 | self._sets = {} if sets is None else sets # Name: Set(Function()) 91 | 92 | def __call__(self, X): 93 | """Pass a value to all sets of the domain and return a dict with results.""" 94 | if isinstance(X, np.ndarray): 95 | if any(not (self._low <= x <= self._high) for x in X): 96 | raise FuzzyWarning("Value in array is outside of defined range!") 97 | res = {} 98 | for s in self._sets.values(): 99 | vector = np.vectorize(s.func, otypes=[float]) 100 | res[s] = vector(X) 101 | return res 102 | if not (self._low <= X <= self._high): 103 | warn(f"{X} is outside of domain!") 104 | return {s: s.func(X) for name, s in self._sets.items()} 105 | 106 | def __str__(self): 107 | """Return a string to print().""" 108 | return self._name 109 | 110 | def __repr__(self): 111 | """Return a string so that eval(repr(Domain)) == Domain.""" 112 | return f"Domain('{self._name}', {self._low}, {self._high}, res={self._res}, sets={self._sets})" 113 | 114 | def __eq__(self, other): 115 | """Test equality of two domains.""" 116 | return all( 117 | [ 118 | self._name == other._name, 119 | self._low == other._low, 120 | self._high == other._high, 121 | self._res == other._res, 122 | self._sets == other._sets, 123 | ] 124 | ) 125 | 126 | def __hash__(self): 127 | return id(self) 128 | 129 | def __getattr__(self, name): 130 | """Get the value of an attribute. Is called after __getattribute__ is called with an AttributeError.""" 131 | if name in self._sets: 132 | return self._sets[name] 133 | else: 134 | raise AttributeError(f"{name} is not a set or attribute") 135 | 136 | def __setattr__(self, name, value): 137 | """Define a set within a domain or assign a value to a domain attribute.""" 138 | # It's a domain attr 139 | if name in self.__slots__: 140 | object.__setattr__(self, name, value) 141 | # We've got a fuzzyset 142 | else: 143 | assert str.isidentifier(name), f"{name} must be an identifier." 144 | if not isinstance(value, Set): 145 | # Often useful to just assign a function for simple sets.. 146 | value = Set(value) 147 | # However, we need the abstraction if we want to use Superfuzzysets (derived sets). 148 | self._sets[name] = value 149 | value.domain = self 150 | value.name = name 151 | 152 | def __delattr__(self, name): 153 | """Delete a fuzzy set from the domain.""" 154 | if name in self._sets: 155 | del self._sets[name] 156 | else: 157 | raise FuzzyWarning("Trying to delete a regular attr, this needs extra care.") 158 | 159 | @property 160 | def range(self): 161 | """Return an arange object with the domain's specifics. 162 | 163 | This is used to conveniently iterate over all possible values 164 | for plotting etc. 165 | 166 | High upper bound is INCLUDED unlike range. 167 | """ 168 | if int(self._res) == self._res: 169 | return np.arange(self._low, self._high + self._res, int(self._res)) 170 | else: 171 | return np.linspace( 172 | self._low, self._high, int((self._high - self._low) / self._res) + 1 173 | ) 174 | 175 | def min(self, x): 176 | """Standard way to get the min over all membership funcs. 177 | 178 | It's not just more convenient but also faster than 179 | to calculate all results, construct a dict, unpack the dict 180 | and calculate the min from that. 181 | """ 182 | return min(f(x) for f in self._sets.values()) 183 | 184 | def max(self, x): 185 | """Standard way to get the max over all membership funcs.""" 186 | return max(f(x) for f in self._sets.values()) 187 | ``` 188 |
189 | --- 190 | 191 | ### max 192 | 193 | `max(self, x)` 194 | : Standard way to get the max over all membership funcs. 195 | 196 | --- 197 | 198 | ### min 199 | `min(self, x)` 200 | : Standard way to get the min over all membership funcs. 201 | It's not just more convenient but also faster than 202 | to calculate all results, construct a dict, unpack the dict 203 | and calculate the min from that. 204 | 205 | 206 | --- 207 | 208 | Rule 209 | ------- 210 | 211 | `Rule(conditions, func=None)` 212 | : A collection of bound sets that span a multi-dimensional space of their respective domains. 213 | ``` 214 | class Rule: 215 | """ 216 | A collection of bound sets that span a multi-dimensional space of their respective domains. 217 | """ 218 | 219 | def __init__(self, conditions, func=None): 220 | self.conditions = {frozenset(C): O for C, O, in conditions.items()} 221 | self.func = func 222 | 223 | def __add__(self, other): 224 | assert isinstance(other, Rule) 225 | return Rule({**self.conditions, **other.conditions}) 226 | 227 | def __radd__(self, other): 228 | assert isinstance(other, (Rule, int)) 229 | # we're using sum(..) 230 | if isinstance(other, int): 231 | return self 232 | return Rule({**self.conditions, **other.conditions}) 233 | 234 | def __or__(self, other): 235 | assert isinstance(other, Rule) 236 | return Rule({**self.conditions, **other.conditions}) 237 | 238 | def __eq__(self, other): 239 | return self.conditions == other.conditions 240 | 241 | def __getitem__(self, key): 242 | return self.conditions[frozenset(key)] 243 | 244 | def __call__(self, args: "dict[Domain, float]", method="cog"): 245 | """Calculate the infered value based on different methods. 246 | Default is center of gravity (cog). 247 | """ 248 | assert len(args) == max( 249 | len(c) for c in self.conditions.keys() 250 | ), "Number of values must correspond to the number of domains defined as conditions!" 251 | assert isinstance(args, dict), "Please make sure to pass in the values as a dictionary." 252 | if method == "cog": 253 | assert ( 254 | len({C.domain for C in self.conditions.values()}) == 1 255 | ), "For CoG, all conditions must have the same target domain." 256 | actual_values = {f: f(args[f.domain]) for S in self.conditions.keys() for f in S} 257 | 258 | weights = [] 259 | for K, v in self.conditions.items(): 260 | x = min((actual_values[k] for k in K if k in actual_values), default=0) 261 | if x > 0: 262 | weights.append((v, x)) 263 | 264 | if not weights: 265 | return None 266 | target_domain = list(self.conditions.values())[0].domain 267 | index = sum(v.center_of_gravity * x for v, x in weights) / sum(x for v, x in weights) 268 | return (target_domain._high - target_domain._low) / len( 269 | target_domain.range 270 | ) * index + target_domain._low 271 | ``` 272 | --- 273 | 274 | Set 275 | ------- 276 | 277 | `class Set(func: , *, name=None, domain=None)` 278 | : A fuzzyset defines a 'region' within a domain. 279 | The associated membership function defines 'how much' a given value is 280 | inside this region - how 'true' the value is. 281 | Sets and functions MUST NOT be mixed because functions don't have 282 | the methods of the sets needed for the logic. 283 | 284 | Sets that are returned from one of the operations are 'derived sets' or 285 | 'Superfuzzysets' according to Zadeh. 286 | 287 | Note that most checks are merely assertions that can be optimized away. 288 | DO NOT RELY on these checks and use tests to make sure that only valid calls are made. 289 | This class uses the classical MIN/MAX operators for AND/OR. To use different operators, simply subclass and 290 | replace the __and__ and __or__ functions. However, be careful not to mix the classes logically, 291 | since it might be confusing which operator will be used (left/right binding). 292 | 293 | ### Array 294 | `array(self)` 295 | : Return an array of all values for this set within the given domain. 296 | 297 | ### Concentrated 298 | `concentrated(self)` 299 | : Alternative to hedge "very". 300 | Returns a new set that has a reduced amount of values the set includes and to dampen the 301 | membership of many values. 302 | 303 | ### Dilated 304 | `dilated(self)` 305 | : Expand the set with more values and already included values are enhanced. 306 | 307 | ### Intensified 308 | `intensified(self)` 309 | : Alternative to hedges.Returns a new set where the membership of values are increased that 310 | already strongly belong to the set and dampened the rest. 311 | 312 | ### Multiplied 313 | `multiplied(self, n)` 314 | : Multiply with a constant factor, changing all membership values. 315 | 316 | ### Normalized 317 | `normalized(self)` 318 | : Return a set that is normalized *for this domain* with 1 as max. 319 | 320 | ### Plot 321 | `plot(self)` 322 | : Graph the set in the given domain. 323 | -------------------------------------------------------------------------------- /docs/docs/combinators.md: -------------------------------------------------------------------------------- 1 | Module fuzzylogic.combinators 2 | ============================= 3 | Combine two linguistic terms. 4 | 5 | a and b are functions of two sets of the same domain. 6 | 7 | Since these combinators are used directly in the Set class to implement logic operations, 8 | the most obvious use of this module is when subclassing Set to make use of specific combinators 9 | for special circumstances. 10 | 11 | Most functions also SHOULD support an arbitrary number of arguments so they can be used in 12 | other contexts than just fuzzy sets. HOWEVER, mind that the primary set of arguments always are functors and 13 | there is always only one secondary argument - the value to be evaluated. 14 | 15 | ## Example 16 | 17 | def bounded_sum(*guncs): 18 | funcs = list(guncs) 19 | 20 | def op(x, y): 21 | return x + y - x * y 22 | 23 | def F(z): 24 | return reduce(op, (f(z) for f in funcs)) 25 | 26 | return F 27 | 28 | This function is initialized with any number of membership functions ("guncs") 29 | which are then turned into a fixed list and numba.njit-ed in the future. 30 | 31 | > f = bounded_sum(R(0, 10), R(5, 15), S(0, 10)) 32 | 33 | would return the inner F function and prepare everything for operation in an initialization phase. 34 | Now ready for operational phase, when called with a value like `f(5)`, 35 | the inner function applies all specified functions to this value 36 | and combines these membership-values via the inner op function, reducing it all to a single value in [0,1]. 37 | 38 | Functions 39 | --------- 40 | 41 | 42 | `MAX(*guncs)` 43 | : Classic OR variant. 44 | 45 | 46 | `MIN(*guncs) ‑> collections.abc.Callable` 47 | : Classic AND variant. 48 | 49 | 50 | `bounded_sum(*guncs)` 51 | : OR variant. 52 | 53 | 54 | `einstein_product(*guncs)` 55 | : AND variant. 56 | 57 | 58 | `einstein_sum(*guncs)` 59 | : OR variant. 60 | 61 | 62 | `gamma_op(g)` 63 | : Combine AND with OR by a weighing factor g. 64 | 65 | This is called a 'compensatoric' operator. 66 | 67 | g (gamma-factor) 68 | 0 < g < 1 (g == 0 -> AND; g == 1 -> OR) 69 | 70 | Same problem as with lambda_op, since all combinators only take functions as arguments, so we parametrize this with g in a pre-init step. 71 | 72 | 73 | `hamacher_product(*guncs)` 74 | : AND variant. 75 | 76 | (xy) / (x + y - xy) for x, y != 0 77 | 0 otherwise 78 | 79 | 80 | `hamacher_sum(*guncs)` 81 | : OR variant. 82 | 83 | (x + y - 2xy) / (1 - xy) for x,y != 1 84 | 1 otherwise 85 | 86 | 87 | `lambda_op(l)` 88 | : A 'compensatoric' operator, combining AND with OR by a weighing factor l. 89 | 90 | This complicates matters a little, since all normal combinators only take functions as parameters so we parametrize this with l in a pre-init step. 91 | 92 | 93 | `lukasiewicz_AND(*guncs)` 94 | : AND variant. 95 | 96 | 97 | `lukasiewicz_OR(*guncs)` 98 | : OR variant. 99 | 100 | 101 | `njit(func)` 102 | : 103 | 104 | 105 | `product(*guncs)` 106 | : AND variant. 107 | 108 | 109 | `simple_disjoint_sum(*guncs)` 110 | : Simple fuzzy XOR operation. 111 | Someone fancy a math proof? 112 | 113 | Basic idea: 114 | (A AND ~B) OR (~A AND B) 115 | 116 | >>> xor = simple_disjoint_sum(noop(), noop()) 117 | >>> xor(0) 118 | 0 119 | >>> xor(1) 120 | 0 121 | >>> xor(0.5) 122 | 0.5 123 | >>> xor(0.3) == round(xor(0.7), 2) 124 | True 125 | 126 | Attempt for expansion without proof: 127 | x = 0.5 128 | y = 1 129 | (x and ~y) or (~x and b) 130 | 131 | max(min(0.5, 0), min(0.5, 1)) == 0.5 132 | 133 | ---- 134 | 135 | x = 0 136 | y = 0.5 137 | z = 1 138 | 139 | (A AND ~B AND ~C) OR (B AND ~A AND ~C) OR (C AND ~B AND ~A) 140 | max(min(0,0.5,0), min(0.5,1,0), min(1,0.5,1)) == 0.5 -------------------------------------------------------------------------------- /docs/docs/functions.md: -------------------------------------------------------------------------------- 1 | Module fuzzylogic.functions 2 | =========================== 3 | General-purpose functions that map R -> [0,1]. 4 | 5 | These functions work as closures. 6 | The inner function uses the variables of the outer function. 7 | 8 | These functions work in two steps: prime and call. 9 | In the first step the function is constructed, initialized and 10 | constants pre-evaluated. In the second step the actual value 11 | is passed into the function, using the arguments of the first step. 12 | 13 | Definitions 14 | ----------- 15 | These functions are used to determine the *membership* of a value x in a fuzzy- 16 | set. Thus, the 'height' is the variable 'm' in general. 17 | In a normal set there is at least one m with m == 1. This is the default. 18 | In a non-normal set, the global maximum and minimum is skewed. 19 | The following definitions are for normal sets. 20 | 21 | The intervals with non-zero m are called 'support', short s_m 22 | The intervals with m == 1 are called 'core', short c_m 23 | The intervals with max(m) are called "height" 24 | The intervals m != 1 and m != 0 are called 'boundary'. 25 | The intervals with m == 0 are called 'unsupported', short no_m 26 | 27 | In a fuzzy set with one and only one m == 1, this element is called 'prototype'. 28 | 29 | Functions 30 | --------- 31 | 32 | 33 | `R(low, high)` 34 | : Simple alternative for bounded_linear(). 35 | 36 | THIS FUNCTION ONLY CAN HAVE A POSITIVE SLOPE - 37 | USE THE S() FUNCTION FOR NEGATIVE SLOPE. 38 | 39 | 40 | `S(low, high)` 41 | : Simple alternative for bounded_linear. 42 | 43 | THIS FUNCTION ONLY CAN HAVE A NEGATIVE SLOPE - 44 | USE THE R() FUNCTION FOR POSITIVE SLOPE. 45 | 46 | 47 | `alpha(*, floor: float = 0, ceiling: float = 1, func: collections.abc.Callable, floor_clip: Optional[float] = None, ceiling_clip: Optional[float] = None)` 48 | : Clip a function's values. 49 | 50 | This is used to either cut off the upper or lower part of a graph. 51 | Actually, this is more like a hedge but doesn't make sense for sets. 52 | 53 | 54 | `bounded_exponential(k=0.1, limit=1)` 55 | : Function that goes through the origin and approaches a limit. 56 | k determines the steepness. The function defined for [0, +inf). 57 | Useful for things that can't be below 0 but may not have a limit like temperature 58 | or time, so values are always defined. 59 | f(x)=limit-limit/e^(k*x) 60 | 61 | Again: This function assumes x >= 0, there are no checks for this assumption! 62 | 63 | 64 | `bounded_linear(low: float, high: float, *, c_m: float = 1, no_m: float = 0, inverse=False)` 65 | : Variant of the linear function with gradient being determined by bounds. 66 | 67 | The bounds determine minimum and maximum value-mappings, 68 | but also the gradient. As [0, 1] must be the bounds for y-values, 69 | left and right bounds specify 2 points on the graph, for which the formula 70 | f(x) = y = (y2 - y1) / (x2 - x1) * (x - x1) + y1 = (y2 - y1) / (x2 - x1) * 71 | (x - x2) + y2 72 | 73 | (right_y - left_y) / ((right - left) * (x - self.left) + left_y) 74 | works. 75 | 76 | >>> f = bounded_linear(2, 3) 77 | >>> f(1) 78 | 0.0 79 | >>> f(2) 80 | 0.0 81 | >>> f(2.5) 82 | 0.5 83 | >>> f(3) 84 | 1.0 85 | >>> f(4) 86 | 1.0 87 | 88 | 89 | `bounded_sigmoid(low, high, inverse=False)` 90 | : Calculate a weight based on the sigmoid function. 91 | 92 | Specify the lower limit where f(x) = 0.1 and the 93 | upper with f(x) = 0.9 and calculate the steepness and elasticity 94 | based on these. We don't need the general logistic function as we 95 | operate on [0,1]. 96 | 97 | core idea: 98 | f(x) = 1. / (1. + exp(x * (4. * log(3)) / (low - high)) * 99 | 9 * exp(low * -(4. * log(3)) / (low - high))) 100 | 101 | How I got this? IIRC I was playing around with linear equations and 102 | boundary conditions of sigmoid funcs on wolframalpha.. 103 | 104 | previously factored to: 105 | k = -(4. * log(3)) / (low - high) 106 | o = 9 * exp(low * k) 107 | return 1 / (1 + exp(-k * x) * o) 108 | 109 | vars 110 | ---- 111 | low: x-value with f(x) = 0.1 112 | for x < low: m -> 0 113 | high: x-value with f(x) = 0.9 114 | for x > high: m -> 1 115 | 116 | >>> f = bounded_sigmoid(0, 1) 117 | >>> f(0) 118 | 0.1 119 | >>> round(f(1), 2) 120 | 0.9 121 | >>> round(f(100000), 2) 122 | 1.0 123 | >>> round(f(-100000), 2) 124 | 0.0 125 | 126 | 127 | `constant(c: float) ‑> collections.abc.Callable` 128 | : Return always the same value, no matter the input. 129 | 130 | Useful for testing. 131 | >>> f = constant(1) 132 | >>> f(0) 133 | 1 134 | 135 | 136 | `gauss(c, b, *, c_m=1)` 137 | : Defined by ae^(-b(x-x0)^2), a gaussian distribution. 138 | 139 | Basically a triangular sigmoid function, it comes close to human perception. 140 | 141 | vars 142 | ---- 143 | c_m (a) 144 | defines the maximum y-value of the graph 145 | b 146 | defines the steepness 147 | c (x0) 148 | defines the symmetry center/peak of the graph 149 | 150 | 151 | `inv(g: collections.abc.Callable) ‑> collections.abc.Callable` 152 | : Invert the given function within the unit-interval. 153 | 154 | For sets, the ~ operator uses this. It is equivalent to the TRUTH value of FALSE. 155 | 156 | 157 | `linear(m: float = 0, b: float = 0) ‑> ` 158 | : A textbook linear function with y-axis section and gradient. 159 | 160 | f(x) = m*x + b 161 | BUT CLIPPED. 162 | 163 | >>> f = linear(1, -1) 164 | >>> f(-2) # should be -3 but clipped 165 | 0 166 | >>> f(0) # should be -1 but clipped 167 | 0 168 | >>> f(1) 169 | 0 170 | >>> f(1.5) 171 | 0.5 172 | >>> f(2) 173 | 1 174 | >>> f(3) # should be 2 but clipped 175 | 1 176 | 177 | 178 | `moderate(func)` 179 | : Map [0,1] -> [0,1] with bias towards 0.5. 180 | 181 | For instance this is needed to dampen extremes. 182 | 183 | 184 | `njit(func)` 185 | : 186 | 187 | 188 | `noop() ‑> collections.abc.Callable` 189 | : Do nothing and return the value as is. 190 | 191 | Useful for testing. 192 | 193 | 194 | `normalize(height, func)` 195 | : Map [0,1] to [0,1] so that max(array) == 1. 196 | 197 | 198 | `rectangular(low: float, high: float, *, c_m: float = 1, no_m: float = 0) ‑> ` 199 | : Basic rectangular function that returns the core_y for the core else 0. 200 | 201 | ______ 202 | | | 203 | ____| |___ 204 | 205 | 206 | `sigmoid(L, k, x0)` 207 | : Special logistic function. 208 | 209 | http://en.wikipedia.org/wiki/Logistic_function 210 | 211 | f(x) = L / (1 + e^(-k*(x-x0))) 212 | with 213 | x0 = x-value of the midpoint 214 | L = the curve's maximum value 215 | k = steepness 216 | 217 | 218 | `simple_sigmoid(k=0.229756)` 219 | : Sigmoid variant with only one parameter (steepness). 220 | 221 | The midpoint is 0. 222 | The slope is positive for positive k and negative k. 223 | f(x) is within [0,1] for any real k and x. 224 | >>> f = simple_sigmoid() 225 | >>> round(f(-1000), 2) 226 | 0.0 227 | >>> f(0) 228 | 0.5 229 | >>> round(f(1000), 2) 230 | 1.0 231 | >>> round(f(-20), 2) 232 | 0.01 233 | >>> round(f(20), 2) 234 | 0.99 235 | 236 | 237 | `singleton(p, *, no_m=0, c_m=1)` 238 | : A single spike. 239 | 240 | >>> f = singleton(2) 241 | >>> f(1) 242 | 0 243 | >>> f(2) 244 | 1 245 | 246 | 247 | `trapezoid(low, c_low, c_high, high, *, c_m=1, no_m=0)` 248 | : Combination of rectangular and triangular, for convenience. 249 | 250 | ____ 251 | / \ 252 | ____/ \___ 253 | 254 | 255 | `triangular(low, high, *, c=None, c_m=1, no_m=0)` 256 | : Basic triangular norm as combination of two linear functions. 257 | 258 | /\ 259 | ____/ \___ 260 | 261 | 262 | `triangular_sigmoid(low, high, c=None)` 263 | : Version of triangular using sigmoids instead of linear. 264 | 265 | THIS FUNCTION PEAKS AT 0.9 266 | 267 | >>> g = triangular_sigmoid(2, 4) 268 | >>> g(2) 269 | 0.1 270 | >>> round(g(3), 2) 271 | 0.9 -------------------------------------------------------------------------------- /docs/docs/hedges.md: -------------------------------------------------------------------------------- 1 | Module fuzzylogic.hedges 2 | ======================== 3 | Lingual hedges modify curves of membership values. 4 | 5 | These should work with Sets and functions. 6 | 7 | Functions 8 | --------- 9 | 10 | 11 | `minus(g)` 12 | : Increase membership support so that more values hit the top. 13 | 14 | 15 | `plus(g)` 16 | : Sharpen memberships like 'very' but not as strongly. 17 | 18 | 19 | `very(g)` 20 | : Sharpen memberships so that only the values close 1 stay at the top. -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | # Fuzzy Logic for Python 3 2 | 3 | [![license](https://img.shields.io/github/license/amogorkon/fuzzylogic)](https://github.com/amogorkon/fuzzylogic/blob/master/LICENSE) 4 | [![stars](https://img.shields.io/github/stars/amogorkon/fuzzylogic?style=plastic)](https://github.com/amogorkon/fuzzylogic/stargazers) 5 | [![forks](https://img.shields.io/github/forks/amogorkon/fuzzylogic?style=plastic)](https://github.com/amogorkon/fuzzylogic/network/members) 6 | [![Downloads](https://pepy.tech/badge/fuzzylogic)](https://pepy.tech/project/fuzzylogic) 7 | 8 | 9 | This is the fourth time I rebuilt this library from scratch to find the sweet spot between ease of use (beautiful is better than ugly!), testability (simple is better than complex!) and potential for performance optimization (practicality beats purity!). 10 | 11 | ### Why a new library? 12 | The first time I was confronted with fuzzy logic, I fell in love with the concept, but after reading books and checking out libraries etc. I found it frustrating how most people make fuzzy logic appear complicated, hard to handle and incorporate in code. 13 | Sure, there are frameworks that allow modelling of functions via GUI, but that's not a solution for a coder, right? Then there's a ton of mathematical research and other cruft that no normal person has time and patience to work through before trying to explore and applying things. Coming from this direction, there are also a number of script-ish (DSL) language frameworks that try to make the IF THEN ELSE pattern work (which I also tried in python, but gave it up because it just looks ugly). 14 | And yes, it's also possible to implement the whole thing completely in a functional style, but you really don't want to work with a recursive structure of 7+ steps by hand, trying not to miss a (..) along the way. 15 | Finally, most education on the subject emphasize sets and membership functions, but fail to mention the importance of the domain (or "universe of discourse"). It's easy to miss this point if you get lost with set operations and membership values, which are actually not that difficult once you can *play* and *explore* how these things look and work! 16 | 17 | ### The Idea 18 | So, the idea is to have three parts that work together: domains, sets and rules. Each of these classes wrap additional logic around basic building blocks - Set gives logical operations to simple functions, Domain gives additional logic to numpy arrays and groups Sets together while Rule combines different Domains. You start modelling your system by defining your domain of interest. Then you think about where your interesting points are in that domain and look for a function that might do what you want. In general, fuzzy.functions map any value to [0,1], that's all. Simply wrap the function in a Set and assign it to the domain in question. Once assigned, you can plot that set and see if it actually looks how you imagined. Now that you have one or more sets, you also can start to combine them with set operations &, |, ~, etc. It's fairly straight forward. 19 | Finally, use the Rules to map input domain to output domain to actually control stuff. 20 | ### Warning: Magic 21 | To make it possible to write fuzzy logic in the most pythonic and simplest way imaginable, it was necessary to employ some magic tricks that normally are discouraged, but at least there's no black magic involved (aka meta-programming etc.), so things are easy to debug if there is a problem. Most notably: 22 | * all functions are recursive closures (which makes it kinda hard to serialize things, if you really want to do that) 23 | * The main classes use a lot of dunder methods to implement their logic, which can be a bit daunting at first glance 24 | * Domain and Set uses an assignment trick to make it possible to instantiate Set() without passing domain and name over and over (yet still be explicit, just not the way one would normally expect). This also allows to call sets as Domain.attributes, which also normally shouldn't be possible (since they are technically not attributes). However, this allows interesting things like dangling sets (sets without domains) that can be freely combined with other sets to avoid cluttering of domain-namespaces and just have the resulting set assigned to a domain to work with. 25 | 26 | # Installation 27 | Just enter 28 | `python -m pip install fuzzylogic` 29 | in a commandline prompt and you should be good to go! 30 | 31 | It's even more fun to experiment with it in [Jupyter Lab](https://jupyter.org) :-) 32 | 33 | # Documentation 34 | Thanks to [Atul Kushwaha](https://github.com/coderatul), we now have an amazing [documentation](https://fuzzylogic.readthedocs.io/en/latest/) including our [Showcase](https://github.com/amogorkon/fuzzylogic/blob/master/docs/Showcase.ipynb) - check it out! 35 | 36 | # Office Hours 37 | You can also contact me one-on-one! Check my [office hours](https://calendly.com/amogorkon/officehours) to set up a meeting :-) 38 | 39 | -- Anselm Kiefner 40 | -------------------------------------------------------------------------------- /docs/docs/rules.md: -------------------------------------------------------------------------------- 1 | Module fuzzylogic.rules 2 | ======================= 3 | Functions to evaluate, infer and defuzzify. 4 | 5 | Functions 6 | --------- 7 | 8 | 9 | `rescale(out_min, out_max, *, in_min=0, in_max=1)` 10 | : Scale from one domain to another. 11 | 12 | Tests only cover scaling from [0,1] (with default in_min, in_max!) 13 | to R. 14 | 15 | For arbitrary R -> R additional testing is required, 16 | but it should work in general out of the box. 17 | 18 | Originally used the algo from SO 19 | (OUT_max - OUT_min)*(x - IN_min) / (IN_max - IN_min) + OUT_min 20 | but there are too many edge cases thanks to over/underflows. 21 | Current factorized algo was proposed as equivalent by wolframalpha, 22 | which seems more stable. 23 | 24 | 25 | `round_partial(value, res)` 26 | : Round any value to any arbitrary precision. 27 | 28 | >>> round_partial(0.405, 0.02) 29 | 0.4 30 | >>> round_partial(0.412, 0.02) 31 | 0.42 32 | >>> round_partial(1.38, 0.25) 33 | 1.5 34 | >>> round_partial(1.12, 0.25) 35 | 1.0 36 | >>> round_partial(9.24, 0.25) 37 | 9.25 38 | >>> round_partial(7.76, 0.25) 39 | 7.75 40 | >>> round_partial(987654321, 100) 41 | 987654300 42 | >>> round_partial(3.14, 0) 43 | 3.14 44 | 45 | 46 | `weighted_sum(*, weights: dict, target_d: fuzzylogic.classes.Domain) ‑> float` 47 | : Used for weighted decision trees and such. 48 | 49 | Parametrize with dict of factorname -> weight and domain of results. 50 | Call with a dict of factorname -> [0, 1] 51 | 52 | There SHOULD be the same number of items (with the same names!) 53 | of weights and factors, but it doesn't have to be - however 54 | set(factors.names) <= set(weights.names) - in other words: 55 | there MUST be at least as many items in weights as factors. -------------------------------------------------------------------------------- /docs/docs/truth.md: -------------------------------------------------------------------------------- 1 | Module fuzzylogic.truth 2 | ======================= 3 | Functions that transform a given membership value to a truth value. 4 | 5 | How this can be useful? Beats me. Found it somewhere on the internet, 6 | never needed it. 7 | 8 | Functions 9 | --------- 10 | 11 | 12 | `fairly_false(m)` 13 | : Part of a circle in quadrant I. 14 | 15 | 16 | `fairly_true(m)` 17 | : Part of a circle in quadrant II. 18 | 19 | 20 | `false(m)` 21 | : The opposite of TRUE. 22 | 23 | 24 | `true(m)` 25 | : The membership-value is its own truth-value. 26 | 27 | 28 | `very_false(m)` 29 | : Part of a circle in quadrant III. 30 | 31 | 32 | `very_true(m)` 33 | : Part of a circle in quadrant IV. -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Fuzzy Logic 2 | nav: 3 | - Home: index.md 4 | - Tutorial: Showcase.md 5 | - FAQ's: How-To.md 6 | - Discussion: Discussion.md 7 | - API Reference: 8 | - classes: classes.md 9 | - combinators: combinators.md 10 | - functions: functions.md 11 | - hedges: hedges.md 12 | - rules: rules.md 13 | - truth : truth.md 14 | 15 | theme: readthedocs 16 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.3 2 | colorama==0.4.6 3 | ghp-import==2.1.0 4 | Jinja2==3.1.6 5 | Markdown==3.3.7 6 | MarkupSafe==2.1.3 7 | mergedeep==1.3.4 8 | mkdocs==1.4.3 9 | packaging==23.1 10 | python-dateutil==2.8.2 11 | PyYAML==6.0 12 | pyyaml_env_tag==0.1 13 | six==1.16.0 14 | watchdog==3.0.0 15 | -------------------------------------------------------------------------------- /fuzzylogic.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | ], 7 | "settings": { 8 | "python.testing.pytestEnabled": true 9 | } 10 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 3 | select = ["E", "F"] 4 | ignore = [] 5 | 6 | # Allow autofix for all enabled rules (when `--fix`) is provided. 7 | fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] 8 | unfixable = [] 9 | 10 | # Exclude a variety of commonly ignored directories. 11 | exclude = [ 12 | ".bzr", 13 | ".direnv", 14 | ".eggs", 15 | ".git", 16 | ".git-rewrite", 17 | ".hg", 18 | ".mypy_cache", 19 | ".nox", 20 | ".pants.d", 21 | ".pytype", 22 | ".ruff_cache", 23 | ".svn", 24 | ".tox", 25 | ".venv", 26 | "__pypackages__", 27 | "_build", 28 | "buck-out", 29 | "build", 30 | "dist", 31 | "node_modules", 32 | "venv", 33 | "src/ui", 34 | "box", 35 | ] 36 | 37 | # Same as Black. 38 | line-length = 110 39 | 40 | # Allow unused variables when underscore-prefixed. 41 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 42 | 43 | # Assume Python 3.12 44 | target-version = "py312" 45 | 46 | [tool.ruff.mccabe] 47 | # Unlike Flake8, default to a complexity level of 10. 48 | max-complexity = 10 -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ignore-glob=.tests/* --doctest-modules 3 | norecursedirs = .box build 4 | testpaths = tests src 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | hypothesis 3 | pandas 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = fuzzylogic 3 | version = attr: src.fuzzylogic.__version__ 4 | description = Fuzzy Logic for Python 3 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | author = Anselm Kiefner 8 | author_email = fuzzylogic@anselm.kiefner.de 9 | license = MIT 10 | url = https://github.com/amogorkon/fuzzylogic 11 | classifiers = 12 | Development Status :: 5 - Production/Stable 13 | Intended Audience :: Developers 14 | Intended Audience :: Education 15 | Intended Audience :: Manufacturing 16 | Intended Audience :: Science/Research 17 | Natural Language :: English 18 | Operating System :: OS Independent 19 | Programming Language :: Python :: 3 :: Only 20 | Topic :: Scientific/Engineering :: Artificial Intelligence 21 | Topic :: Scientific/Engineering :: Mathematics 22 | Topic :: Scientific/Engineering :: Information Analysis 23 | keywords = fuzzy logic 24 | 25 | [options] 26 | packages = find: 27 | package_dir = 28 | = src 29 | zip_safe = False 30 | python_requires = >=3.12 31 | 32 | [options.packages.find] 33 | where = src 34 | 35 | [options.extras_require] 36 | plotting = 37 | matplotlib 38 | numba = 39 | numba 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "." 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | "python.testing.unittestArgs": [ 8 | "-v", 9 | "-s", 10 | ".", 11 | "-p", 12 | "test_*.py" 13 | ], 14 | "python.analysis.typeCheckingMode": "basic" 15 | } -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-Fuzzylogic/fuzzylogic/8ad56e980474ddf58ef02f7e01510d9e0b3b6a45/src/__init__.py -------------------------------------------------------------------------------- /src/fuzzylogic/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = (1, 5, 0) 2 | -------------------------------------------------------------------------------- /src/fuzzylogic/classes.py: -------------------------------------------------------------------------------- 1 | """ 2 | classes.py - Domain, Set and Rule classes for fuzzy logic. 3 | 4 | Primary abstractions for recursive functions and arrays, 5 | adding logical operaitons for easier handling. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from random import randint 11 | from typing import Any, Iterable, overload 12 | 13 | from numpy.typing import NDArray 14 | 15 | try: 16 | import matplotlib.pyplot as plt 17 | except ImportError: 18 | plt = None 19 | 20 | 21 | import numpy as np 22 | 23 | from fuzzylogic import defuzz 24 | 25 | from .combinators import MAX, MIN, bounded_sum, product, simple_disjoint_sum 26 | from .functions import Membership, inv, normalize 27 | 28 | type Array = ( 29 | NDArray[np.float16] 30 | | NDArray[np.float32] 31 | | NDArray[np.float64] 32 | | NDArray[np.float128] 33 | | NDArray[np.float256] 34 | ) 35 | 36 | 37 | class FuzzyWarning(UserWarning): 38 | """Extra Exception so that user code can filter exceptions specific to this lib.""" 39 | 40 | pass 41 | 42 | 43 | NO_DOMAIN_TO_COMPARE = "Domain can't work with no domain." 44 | CANT_COMPARE_DOMAINS = "Can't work with different domains." 45 | NO_DOMAIN = "No domain defined." 46 | 47 | 48 | class Domain: 49 | """ 50 | A domain is a 'measurable' dimension of 'real' values like temperature. 51 | 52 | There must be a lower and upper limit and a resolution (the size of steps) 53 | specified. 54 | 55 | Fuzzysets are defined within one such domain and are only meaningful 56 | while considered within their domain ('apples and bananas'). 57 | To operate with sets across domains, there needs to be a mapping. 58 | 59 | The sets are accessed as attributes of the domain like 60 | >>> temp = Domain('temperature', 0, 100) 61 | >>> temp.hot = Set(lambda x: 0) 62 | >>> temp.hot(5) 63 | 0 64 | 65 | It is possible now to call derived sets without assignment first! 66 | >>> from .hedges import very 67 | >>> (very(~temp.hot) | ~very(temp.hot))(2) 68 | 1.0 69 | 70 | You MUST NOT add arbitrary attributes to an *instance* of Domain - you can 71 | however subclass or modify the class itself. If you REALLY have to add attributes, 72 | make sure to "whitelist" it in __slots__ first. 73 | 74 | Use the Domain by calling it with the value in question. This returns a 75 | dictionary with the degrees of membership per set. You MAY override __call__ 76 | in a subclass to enable concurrent evaluation for performance improvement. 77 | """ 78 | 79 | __slots__ = ["_name", "_low", "_high", "_res", "_sets"] 80 | 81 | def __init__( 82 | self, 83 | name: str, 84 | low: float, 85 | high: float, 86 | res: float = 1, 87 | sets: dict[str, Set] | None = None, 88 | ) -> None: 89 | """Define a domain.""" 90 | assert low < high, "higher bound must be greater than lower." 91 | assert res > 0, "resolution can't be negative or zero" 92 | assert isinstance(name, str), "Domain Name must be a string." 93 | assert str.isidentifier(name), "Domain Name must be a valid identifier." 94 | self._name = name 95 | self._high = high 96 | self._low = low 97 | self._res = res 98 | self._sets = {} if sets is None else sets # Name: Set(Function()) 99 | 100 | def __call__(self, x: float) -> dict[Set, float]: 101 | """Pass a value to all sets of the domain and return a dict with results.""" 102 | if not (self._low <= x <= self._high): 103 | raise FuzzyWarning(f"{x} is outside of domain!") 104 | return {self._sets[name]: s.func(x) for name, s in self._sets.items()} 105 | 106 | def __len__(self) -> int: 107 | """Return the size of the domain, as the actual number of possible values, calculated internally.""" 108 | return len(self.range) 109 | 110 | def __str__(self) -> str: 111 | """Return a string to print().""" 112 | return self._name 113 | 114 | def __repr__(self) -> str: 115 | """Return a string so that eval(repr(Domain)) == Domain.""" 116 | return f"Domain('{self._name}', {self._low}, {self._high}, res={self._res}, sets={self._sets})" 117 | 118 | def __eq__(self, other: object) -> bool: 119 | """Test equality of two domains.""" 120 | if not isinstance(other, Domain): 121 | return False 122 | return all([ 123 | self._name == other._name, 124 | self._low == other._low, 125 | self._high == other._high, 126 | self._res == other._res, 127 | self._sets == other._sets, 128 | ]) 129 | 130 | def __hash__(self) -> int: 131 | return id(self) 132 | 133 | def __getattr__(self, name: str) -> Set: 134 | """Get the value of an attribute. Called after __getattribute__ is called with an AttributeError.""" 135 | if name in self._sets: 136 | return self._sets[name] 137 | else: 138 | raise AttributeError(f"{name} is not a set or attribute") 139 | 140 | def __setattr__(self, name: str, value: Set | Membership) -> None: 141 | """Define a set within a domain or assign a value to a domain attribute.""" 142 | # It's a domain attr 143 | if name in self.__slots__: 144 | object.__setattr__(self, name, value) 145 | # We've got a fuzzyset 146 | else: 147 | assert str.isidentifier(name), f"{name} must be an identifier." 148 | if not isinstance(value, Set): 149 | # Often useful to just assign a function for simple sets.. 150 | value = Set(value) 151 | # However, we need the abstraction if we want to use Superfuzzysets (derived sets). 152 | self._sets[name] = value 153 | value.domain = self 154 | value.name = name 155 | value.array() # force the array to be calculated for caching 156 | 157 | def __delattr__(self, name: str) -> None: 158 | """Delete a fuzzy set from the domain.""" 159 | if name in self._sets: 160 | del self._sets[name] 161 | else: 162 | raise FuzzyWarning("Trying to delete a regular attr, this needs extra care.") 163 | 164 | @property 165 | def range(self) -> Array: 166 | """Return an arange object with the domain's specifics. 167 | 168 | This is used to conveniently iterate over all possible values 169 | for plotting etc. 170 | 171 | High upper bound is INCLUDED unlike range. 172 | """ 173 | if int(self._res) == self._res: 174 | return np.arange(self._low, self._high + self._res, int(self._res)) 175 | else: 176 | return np.linspace(self._low, self._high, int((self._high - self._low) / self._res) + 1) 177 | 178 | def min(self, x: float) -> float: 179 | """Standard way to get the min over all membership funcs. 180 | 181 | It's not just more convenient but also faster than 182 | to calculate all results, construct a dict, unpack the dict 183 | and calculate the min from that. 184 | """ 185 | return min((f(x) for f in self._sets.values()), default=0) 186 | 187 | def max(self, x: float) -> float: 188 | """Standard way to get the max over all membership funcs.""" 189 | return max((f(x) for f in self._sets.values()), default=0) 190 | 191 | 192 | class Set: 193 | """ 194 | A fuzzyset defines a 'region' within a domain. 195 | 196 | The associated membership function defines 'how much' a given value is 197 | inside this region - how 'true' the value is. 198 | 199 | Sets and functions MUST NOT be mixed because functions don't have 200 | the methods of the sets needed for the logic. 201 | 202 | Sets that are returned from one of the operations are 'derived sets' or 203 | 'Superfuzzysets' according to Zadeh. 204 | 205 | Note that most checks are merely assertions that can be optimized away. 206 | DO NOT RELY on these checks and use tests to make sure that only valid calls are made. 207 | 208 | This class uses the classical MIN/MAX operators for AND/OR. To use different operators, simply subclass & 209 | replace the __and__ and __or__ functions. However, be careful not to mix the classes logically, 210 | since it might be confusing which operator will be used (left/right binding). 211 | 212 | """ 213 | 214 | name = None # these are set on assignment to the domain! DO NOT MODIFY 215 | domain = None 216 | 217 | def __init__( 218 | self, 219 | func: Membership, 220 | *, 221 | name: str | None = None, 222 | domain: Domain | None = None, 223 | ): 224 | self.func: Membership = func 225 | self.domain: Domain | None = domain 226 | self.name: str | None = name 227 | 228 | self._center_of_gravity: float | None = None 229 | self._cached_array: np.ndarray | None = None 230 | 231 | @overload 232 | def __call__(self, x: float, /) -> float: ... 233 | 234 | @overload 235 | def __call__(self, x: NDArray[np.float16], /) -> NDArray[np.float16]: ... 236 | 237 | @overload 238 | def __call__(self, x: NDArray[np.float32], /) -> NDArray[np.float32]: ... 239 | 240 | @overload 241 | def __call__(self, x: NDArray[np.float64], /) -> NDArray[np.float64]: ... 242 | 243 | @overload 244 | def __call__(self, x: NDArray[np.float128], /) -> NDArray[np.float128]: ... 245 | 246 | @overload 247 | def __call__(self, x: NDArray[np.float256], /) -> NDArray[np.float256]: ... 248 | 249 | def __call__( 250 | self, 251 | x: float | Array, 252 | ) -> float | Array: 253 | if isinstance(x, np.ndarray): 254 | return np.vectorize(self.func)(x) 255 | else: 256 | return self.func(x) 257 | 258 | def __invert__(self) -> Set: 259 | """Return a new set with 1 - function.""" 260 | assert self.domain is not None, NO_DOMAIN 261 | return Set(inv(self.func), domain=self.domain) 262 | 263 | def __neg__(self) -> Set: 264 | """Synonyme for invert.""" 265 | assert self.domain is not None, NO_DOMAIN 266 | return Set(inv(self.func), domain=self.domain) 267 | 268 | def __and__(self, other: Set) -> Set: 269 | """Return a new set with modified function.""" 270 | assert self.domain is not None and other.domain is not None, NO_DOMAIN_TO_COMPARE 271 | assert self.domain == other.domain 272 | return Set(MIN(self.func, other.func), domain=self.domain) 273 | 274 | def __or__(self, other: Set) -> Set: 275 | """Return a new set with modified function.""" 276 | assert self.domain == other.domain 277 | return Set(MAX(self.func, other.func), domain=self.domain) 278 | 279 | def __mul__(self, other: Set) -> Set: 280 | """Return a new set with modified function.""" 281 | assert self.domain == other.domain 282 | return Set(product(self.func, other.func), domain=self.domain) 283 | 284 | def __add__(self, other: Set) -> Set: 285 | """Return a new set with modified function.""" 286 | assert self.domain == other.domain 287 | return Set(bounded_sum(self.func, other.func), domain=self.domain) 288 | 289 | def __xor__(self, other: Set) -> Set: 290 | """Return a new set with modified function.""" 291 | assert self.domain == other.domain 292 | return Set(simple_disjoint_sum(self.func, other.func), domain=self.domain) 293 | 294 | def __pow__(self, power: int) -> Set: 295 | """Return a new set with modified function.""" 296 | 297 | # FYI: pow is used with hedges 298 | def f(x: float): 299 | return pow(self.func(x), power) 300 | 301 | return Set(f, domain=self.domain) 302 | 303 | def __eq__(self, other: object) -> bool: 304 | """A set is equal with another if both return the same values over the same range.""" 305 | if self.domain is None or not isinstance(other, Set) or other.domain is None: 306 | # It would require complete bytecode analysis to check whether both Sets 307 | # represent the same recursive functions - 308 | # additionally, there are infinitely many mathematically equivalent 309 | # functions that don't have the same bytecode... 310 | raise FuzzyWarning("Impossible to determine.") 311 | 312 | # however, if domains ARE assigned (whether or not it's the same domain), 313 | # we simply can check if they map to the same values 314 | return np.array_equal(self.array(), other.array()) 315 | 316 | def __le__(self, other: Set) -> bool: 317 | """If this <= other, it means this is a subset of the other.""" 318 | assert self.domain is not None and other.domain is not None, NO_DOMAIN_TO_COMPARE 319 | assert self.domain == other.domain, CANT_COMPARE_DOMAINS 320 | return all(np.less_equal(self.array(), other.array())) 321 | 322 | def __lt__(self, other: Set) -> bool: 323 | """If this < other, it means this is a proper subset of the other.""" 324 | assert self.domain is not None and other.domain is not None, NO_DOMAIN_TO_COMPARE 325 | assert self.domain == other.domain, CANT_COMPARE_DOMAINS 326 | return all(np.less(self.array(), other.array())) 327 | 328 | def __ge__(self, other: Set) -> bool: 329 | """If this >= other, it means this is a superset of the other.""" 330 | assert self.domain is not None and other.domain is not None, NO_DOMAIN_TO_COMPARE 331 | assert self.domain == other.domain, CANT_COMPARE_DOMAINS 332 | return all(np.greater_equal(self.array(), other.array())) 333 | 334 | def __gt__(self, other: Set) -> bool: 335 | """If this > other, it means this is a proper superset of the other.""" 336 | assert self.domain is not None and other.domain is not None, NO_DOMAIN_TO_COMPARE 337 | assert self.domain == other.domain, CANT_COMPARE_DOMAINS 338 | return all(np.greater(self.array(), other.array())) 339 | 340 | def __len__(self) -> int: 341 | """Number of membership values in the set, defined by bounds and resolution of domain.""" 342 | assert self.domain is not None, NO_DOMAIN 343 | return len(self.array()) 344 | 345 | @property 346 | def cardinality(self) -> float: 347 | """The sum of all values in the set.""" 348 | assert self.domain is not None, NO_DOMAIN 349 | return sum(self.array()) 350 | 351 | @property 352 | def relative_cardinality(self) -> float: 353 | """Relative cardinality is the sum of all membership values by float of all values.""" 354 | assert self.domain is not None, NO_DOMAIN 355 | assert len(self) > 0, "The domain has no element." # only possible with step=inf, but still.. 356 | return self.cardinality / len(self) 357 | 358 | def concentrated(self) -> Set: 359 | """ 360 | Alternative to hedge "very". 361 | 362 | Returns a new set that has a reduced amount of values the set includes and to dampen the 363 | membership of many values. 364 | """ 365 | return Set(lambda x: self.func(x) ** 2, domain=self.domain) 366 | 367 | def intensified(self) -> Set: 368 | """ 369 | Alternative to hedges. 370 | 371 | Returns a new set where the membership of values are increased that 372 | already strongly belong to the set and dampened the rest. 373 | """ 374 | 375 | def f(x: float) -> float: 376 | return 2 * self.func(x) ** 2 if x < 0.5 else 1 - 2 * (1 - self.func(x) ** 2) 377 | 378 | return Set(f, domain=self.domain) 379 | 380 | def dilated(self) -> Set: 381 | """Expand the set with more values and already included values are enhanced.""" 382 | return Set(lambda x: self.func(x) ** 1.0 / 2.0, domain=self.domain) 383 | 384 | def multiplied(self, n: float) -> Set: 385 | """Multiply with a constant factor, changing all membership values.""" 386 | return Set(lambda x: self.func(x) * n, domain=self.domain) 387 | 388 | def plot(self) -> None: 389 | """Graph the set in the given domain.""" 390 | assert self.domain is not None, NO_DOMAIN 391 | if not plt: 392 | raise ImportError( 393 | "matplotlib not available. Please re-install with 'pip install fuzzylogic[plotting]'" 394 | ) 395 | R = self.domain.range # e.g., generated via np.linspace(...) 396 | 397 | cog_val = self.center_of_gravity() 398 | 399 | diffs = np.diff(R) 400 | tol_value = diffs.min() / 100 if len(diffs) > 0 else 1e-6 401 | 402 | if all(abs(x - cog_val) >= tol_value for x in R): 403 | R = sorted(set(R).union({cog_val})) 404 | 405 | V = [self.func(x) for x in R] 406 | plot_color = "#{:06x}".format(randint(0, 0xFFFFFF)) 407 | plt.plot(R, V, label=str(self), color=plot_color, lw=2) 408 | plt.axvline(cog_val, color=plot_color, linestyle="--", linewidth=1.5, label=f"CoG = {cog_val:.2f}") 409 | plt.plot(cog_val, self.func(cog_val), "o", color=plot_color, markersize=8) 410 | 411 | plt.title("Fuzzy Set Membership Function") 412 | plt.xlabel("Domain Value") 413 | plt.ylabel("Membership Degree") 414 | plt.legend() 415 | plt.grid(True) 416 | 417 | def array(self) -> Array: 418 | """Return an array of all values for this set within the given domain.""" 419 | assert self.domain is not None, NO_DOMAIN 420 | if self._cached_array is None: 421 | self._cached_array = np.fromiter((self.func(x) for x in self.domain.range), float) 422 | return self._cached_array 423 | 424 | def range(self) -> Array: 425 | """Return the range of the domain.""" 426 | assert self.domain is not None, NO_DOMAIN 427 | return self.domain.range 428 | 429 | def center_of_gravity(self) -> float: 430 | """Return the center of gravity for this distribution, within the given domain.""" 431 | if self._center_of_gravity is not None: 432 | return self._center_of_gravity 433 | assert self.domain is not None, NO_DOMAIN 434 | weights = self.array() 435 | if sum(weights) == 0: 436 | return 0 437 | cog = float(np.average(self.domain.range, weights=weights)) 438 | self._center_of_gravity = cog 439 | return cog 440 | 441 | def __repr__(self) -> str: 442 | """ 443 | Return a string representation of the Set that reconstructs the set with eval(). 444 | """ 445 | if self.domain is not None: 446 | return f"{self.domain._name}.{self.name}" # type: ignore 447 | return f"Set(({self.func.__qualname__})" 448 | 449 | def __str__(self) -> str: 450 | """Return a string for print().""" 451 | if self.domain is not None: 452 | return f"{self.domain._name}.{self.name}" # type: ignore 453 | return f"Set({self.func.__name__})" 454 | 455 | def normalized(self) -> Set: 456 | """Return a set that is normalized *for this domain* with 1 as max.""" 457 | assert self.domain is not None, NO_DOMAIN 458 | return Set(normalize(max(self.array()), self.func), domain=self.domain) 459 | 460 | def __hash__(self) -> int: 461 | return id(self) 462 | 463 | 464 | class SingletonSet(Set): 465 | def __init__(self, c: float, no_m: float = 0, c_m: float = 1, domain: Domain | None = None): 466 | super().__init__(self._singleton_fn(c, no_m, c_m), domain=domain) 467 | self.c = c 468 | self.no_m = no_m 469 | self.c_m = c_m 470 | self.domain = domain 471 | 472 | self._cached_array: np.ndarray | None = None 473 | 474 | @staticmethod 475 | def _singleton_fn(c: float, no_m: float = 0, c_m: float = 1) -> Membership: 476 | return lambda x: c_m if x == c else no_m 477 | 478 | def center_of_gravity(self) -> float: 479 | """Directly return singleton position""" 480 | return self.c 481 | 482 | def plot(self) -> None: 483 | """Graph the singleton set in the given domain, 484 | ensuring that the singleton's coordinate is included. 485 | """ 486 | assert self.domain is not None, "NO_DOMAIN" 487 | if not plt: 488 | raise ImportError( 489 | "matplotlib not available. Please re-install with 'pip install fuzzylogic[plotting]'" 490 | ) 491 | 492 | R = self.domain.range 493 | if self.c not in R: 494 | R = sorted(set(R).union({self.c})) 495 | V = [self.func(x) for x in R] 496 | plt.plot(R, V, label=f"Singleton {self.c}") 497 | plt.title("Singleton Membership Function") 498 | plt.xlabel("Domain Value") 499 | plt.ylabel("Membership") 500 | plt.legend() 501 | plt.grid(True) 502 | plt.show() 503 | 504 | 505 | class Rule: 506 | """ 507 | Collection of bound sets spanning a multi-dimensional space of their domains, mapping to a target domain. 508 | 509 | """ 510 | 511 | type T = Rule 512 | 513 | def __init__( 514 | self, 515 | *args: Rule | dict[Iterable[Set] | Set, Set], 516 | ) -> None: 517 | """Define a rule with conditions and a target set.""" 518 | self.conditions: dict[frozenset[Set], Set] = {} 519 | for arg in args: 520 | if isinstance(arg, Rule): 521 | self.conditions |= arg.conditions 522 | elif isinstance(arg, dict): 523 | for if_sets, then_set in arg.items(): 524 | if isinstance(if_sets, Set): 525 | if_sets = (if_sets,) 526 | self.conditions[frozenset(if_sets)] = then_set # type: ignore 527 | else: 528 | raise TypeError(f"Expected any number of Rule or dict[Set, Set], got {type(arg).__name__}.") 529 | 530 | def __add__(self, other: Rule) -> Rule: 531 | return Rule({**self.conditions, **other.conditions}) 532 | 533 | def __radd__(self, other: Rule | int) -> Rule: 534 | if isinstance(other, int): # as sum(...) does implicitely 0 + ... 535 | return self 536 | return Rule({**self.conditions, **other.conditions}) 537 | 538 | def __or__(self, other: Rule) -> Rule: 539 | return Rule({**self.conditions, **other.conditions}) 540 | 541 | def __eq__(self, other: object) -> bool: 542 | if not isinstance(other, Rule): 543 | return False 544 | return self.conditions == other.conditions 545 | 546 | def __getitem__(self, key: Iterable[Set]) -> Set: 547 | return self.conditions[frozenset(key)] 548 | 549 | def __call__(self, values: dict[Domain, float], method=defuzz.cog) -> float | None: 550 | """ 551 | Calculate the inferred crisp value based on the fuzzy rules. 552 | values: dict[Domain, float] - the input values for the fuzzy sets 553 | method: defuzzification method to use (default: center of gravity) from fuzzylogic.defuzz 554 | Returns the defuzzified value. 555 | """ 556 | assert isinstance(values, dict), "Please pass a dict[Domain, float|int] as values." 557 | assert values, "No condition rules defined!" 558 | 559 | # Extract common target domain and build list of (then_set, firing_strength) 560 | sample_then_set = next(iter(self.conditions.values())) 561 | target_domain = getattr(sample_then_set, "domain", None) 562 | assert target_domain, "Target domain must be defined." 563 | 564 | target_weights: list[tuple[Set, float]] = [] 565 | for if_sets, then_set in self.conditions.items(): 566 | assert then_set.domain == target_domain, "All target sets must be in the same Domain." 567 | degrees = [] 568 | for s in if_sets: 569 | assert s.domain is not None, "Domain must be defined for all fuzzy sets." 570 | degrees.append(s(values[s.domain])) 571 | firing_strength = min(degrees, default=0) 572 | if firing_strength > 0: 573 | target_weights.append((then_set, firing_strength)) 574 | if not target_weights: 575 | return None 576 | 577 | # For center-of-gravity / centroid: 578 | if method == defuzz.cog: 579 | return defuzz.cog(target_weights) 580 | 581 | # For methods that rely on an aggregated membership function: 582 | points = list(target_domain.range) 583 | n = len(points) 584 | step = ( 585 | (target_domain._high - target_domain._low) / (n - 1) 586 | if n > 1 587 | else (target_domain._high - target_domain._low) 588 | ) 589 | 590 | def aggregated_membership(x: float) -> float: 591 | # For each rule, limit its inferred output by its firing strength and then take the max 592 | return max(min(weight, then_set(x)) for then_set, weight in target_weights) 593 | 594 | match method: 595 | case defuzz.bisector: 596 | return defuzz.bisector(aggregated_membership, points, step) 597 | case defuzz.mom: 598 | return defuzz.mom(aggregated_membership, points) 599 | case defuzz.som: 600 | return defuzz.som(aggregated_membership, points) 601 | case defuzz.lom: 602 | return defuzz.lom(aggregated_membership, points) 603 | case _: 604 | raise ValueError("Invalid defuzzification method specified.") 605 | 606 | 607 | def rule_from_table(table: str, references: dict[str, float]) -> Rule: 608 | """Turn a (2D) string table into a Rule of fuzzy sets. 609 | 610 | ATTENTION: This will eval() all strings in the table. 611 | This can pose a potential security risk if the table originates from an untrusted source. 612 | 613 | Using a table will considerably reduce the amount of required text to describe all rules, 614 | but there are two critical drawbacks: Tables are limited to 2 input variables (2D) and they are strings, 615 | with no IDE support. It is strongly recommended to check the Rule output for consistency. 616 | For example, a trailing "." will result in a SyntaxError when eval()ed. 617 | """ 618 | import io 619 | from itertools import product 620 | 621 | import pandas as pd 622 | 623 | df = pd.read_table(io.StringIO(table), sep=r"\s+") # type: ignore 624 | 625 | D: dict[tuple[Any, Any], Any] = { 626 | ( 627 | eval(df.index[x].strip(), references), # type: ignore 628 | eval(df.columns[y].strip(), references), # type: ignore 629 | ): eval(df.iloc[x, y], references) # type: ignore 630 | for x, y in product(range(len(df.index)), range(len(df.columns))) # type: ignore 631 | } 632 | return Rule(D) # type: ignore 633 | 634 | 635 | if __name__ == "__main__": 636 | import doctest 637 | 638 | doctest.testmod() 639 | -------------------------------------------------------------------------------- /src/fuzzylogic/combinators.py: -------------------------------------------------------------------------------- 1 | """ 2 | combinators.py - Combine two linguistic terms. 3 | 4 | a and b are functions of two sets of the same domain. 5 | 6 | Since these combinators are used directly in the Set class to implement logic operations, 7 | the most obvious use of this module is when subclassing Set to make use of specific combinators 8 | for special circumstances. 9 | 10 | Most functions also SHOULD support an arbitrary number of arguments so they can be used in 11 | other contexts than just fuzzy sets. HOWEVER, mind that the primary set of arguments always are functors and 12 | there is always only one secondary argument - the value to be evaluated. 13 | 14 | ## Example 15 | 16 | def bounded_sum(*guncs): 17 | funcs = list(guncs) 18 | 19 | def op(x, y): 20 | return x + y - x * y 21 | 22 | def F(z): 23 | return reduce(op, (f(z) for f in funcs)) 24 | 25 | return F 26 | 27 | This function is initialized with any number of membership functions ("guncs") 28 | which are then turned into a fixed list and numba.njit-ed in the future. 29 | 30 | > f = bounded_sum(R(0, 10), R(5, 15), S(0, 10)) 31 | 32 | would return the inner F function and prepare everything for operation in an initialization phase. 33 | Now ready for operational phase, when called with a value like `f(5)`, 34 | the inner function applies all specified functions to this value 35 | and combines these membership-values via the inner op function, reducing it all to a single value in [0,1]. 36 | """ 37 | 38 | from collections.abc import Callable 39 | from functools import reduce 40 | 41 | from numpy import multiply 42 | 43 | type Membership = Callable[[float], float] 44 | 45 | try: 46 | raise ImportError 47 | # from numba import njit # still not ready for prime time :( 48 | except ImportError: 49 | 50 | def njit(func: Membership) -> Membership: 51 | return func 52 | 53 | 54 | def MIN(*guncs: Membership) -> Membership: 55 | """Classic AND variant.""" 56 | funcs = list(guncs) 57 | 58 | def F(z: float) -> float: 59 | return min(f(z) for f in funcs) 60 | 61 | return F 62 | 63 | 64 | def MAX(*guncs: Membership) -> Membership: 65 | """Classic OR variant.""" 66 | funcs = list(guncs) 67 | 68 | def F(z: float) -> float: 69 | return max((f(z) for f in funcs), default=1) 70 | 71 | return F 72 | 73 | 74 | def product(*guncs: Membership) -> Membership: 75 | """AND variant.""" 76 | funcs = list(guncs) 77 | epsilon = 1e-10 # Small value to prevent underflow 78 | 79 | def F(z: float) -> float: 80 | return reduce(multiply, (max(f(z), epsilon) for f in funcs)) 81 | 82 | return F 83 | 84 | 85 | def bounded_sum(*guncs: Membership) -> Membership: 86 | """OR variant.""" 87 | funcs = list(guncs) 88 | 89 | def op(x: float, y: float) -> float: 90 | return x + y - x * y 91 | 92 | def F(z: float) -> float: 93 | return reduce(op, (f(z) for f in funcs)) 94 | 95 | return F 96 | 97 | 98 | def lukasiewicz_AND(*guncs: Membership) -> Membership: 99 | """AND variant.""" 100 | funcs = list(guncs) 101 | 102 | def op(x: float, y: float) -> float: 103 | return min(1, x + y) 104 | 105 | def F(z: float) -> float: 106 | return reduce(op, (f(z) for f in funcs)) 107 | 108 | return F 109 | 110 | 111 | def lukasiewicz_OR(*guncs: Membership) -> Membership: 112 | """OR variant.""" 113 | 114 | funcs = list(guncs) 115 | 116 | def op(x: float, y: float) -> float: 117 | return max(0, x + y - 1) 118 | 119 | def F(z: float) -> float: 120 | return reduce(op, (f(z) for f in funcs)) 121 | 122 | return F 123 | 124 | 125 | def einstein_product(*guncs: Membership) -> Membership: 126 | """AND variant.""" 127 | funcs = list(guncs) 128 | 129 | def op(x: float, y: float) -> float: 130 | return (x * y) / (2 - (x + y - x * y)) 131 | 132 | def F(z: float) -> float: 133 | return reduce(op, (f(z) for f in funcs)) 134 | 135 | return F 136 | 137 | 138 | def einstein_sum(*guncs: Membership) -> Membership: 139 | """OR variant.""" 140 | funcs = list(guncs) 141 | 142 | def op(x: float, y: float) -> float: 143 | return (x + y) / (1 + x * y) 144 | 145 | def F(z: float) -> float: 146 | return reduce(op, (f(z) for f in funcs)) 147 | 148 | return F 149 | 150 | 151 | def hamacher_product(*guncs: Membership) -> Membership: 152 | """AND variant. 153 | 154 | (xy) / (x + y - xy) for x, y != 0 155 | 0 otherwise 156 | """ 157 | funcs = list(guncs) 158 | 159 | def op(x: float, y: float) -> float: 160 | return (x * y) / (x + y - x * y) if x != 0 and y != 0 else 0 161 | 162 | def F(z: float) -> float: 163 | return reduce(op, (f(z) for f in funcs)) 164 | 165 | return F 166 | 167 | 168 | def hamacher_sum(*guncs: Membership) -> Membership: 169 | """OR variant. 170 | 171 | (x + y - 2xy) / (1 - xy) for x,y != 1 172 | 1 otherwise 173 | """ 174 | funcs = list(guncs) 175 | 176 | def op(x: float, y: float) -> float: 177 | return (x + y - 2 * x * y) / (1 - x * y) if x != 1 or y != 1 else 1 178 | 179 | def F(z: float) -> float: 180 | return reduce(op, (f(z) for f in funcs)) 181 | 182 | return F 183 | 184 | 185 | def lambda_op(h: float) -> Callable[..., Membership]: 186 | """A 'compensatoric' operator, combining AND with OR by a weighing factor l. 187 | 188 | This complicates matters a little, since all normal combinators only take functions 189 | as parameters so we parametrize this with l in a pre-init step. 190 | """ 191 | assert 0 <= h <= 1, breakpoint() 192 | 193 | # sourcery skip: use-function-docstrings 194 | def E(*guncs: Membership) -> Membership: 195 | funcs = list(guncs) 196 | 197 | def op(x: float, y: float) -> float: 198 | return h * (x * y) + (1 - h) * (x + y - x * y) 199 | 200 | def F(z: float) -> float: 201 | return reduce(op, (f(z) for f in funcs)) 202 | 203 | return F 204 | 205 | return E 206 | 207 | 208 | def gamma_op(g: float) -> Callable[..., Membership]: 209 | """Combine AND with OR by a weighing factor g. 210 | 211 | This is called a 'compensatoric' operator. 212 | 213 | g (gamma-factor) 214 | 0 < g < 1 (g == 0 -> AND; g == 1 -> OR) 215 | 216 | Same problem as with lambda_op, since all combinators only take functions as arguments, 217 | so we parametrize this with g in a pre-init step. 218 | """ 219 | assert 0 <= g <= 1 220 | 221 | def E(*guncs: Membership) -> Membership: 222 | funcs = list(guncs) 223 | 224 | def op(x: float, y: float) -> float: 225 | return (x * y) ** (1 - g) * ((1 - x) * (1 - y)) ** g 226 | 227 | def F(z: float) -> float: 228 | return reduce(op, (f(z) for f in funcs)) 229 | 230 | return F 231 | 232 | return E 233 | 234 | 235 | def simple_disjoint_sum(*funcs: Membership) -> Membership: # sourcery skip: unwrap-iterable-construction 236 | """Simple fuzzy XOR operation. 237 | Someone fancy a math proof? 238 | 239 | Basic idea: 240 | (A AND ~B) OR (~A AND B) 241 | 242 | >>> from .functions import noop 243 | >>> xor = simple_disjoint_sum(noop(), noop()) 244 | >>> xor(0) 245 | 0 246 | >>> xor(1) 247 | 0 248 | >>> xor(0.5) 249 | 0.5 250 | >>> xor(0.3) == round(xor(0.7), 2) 251 | True 252 | 253 | Attempt for expansion without proof: 254 | x = 0.5 255 | y = 1 256 | (x and ~y) or (~x and b) 257 | 258 | max(min(0.5, 0), min(0.5, 1)) == 0.5 259 | 260 | ---- 261 | 262 | x = 0 263 | y = 0.5 264 | z = 1 265 | 266 | (A AND ~B AND ~C) OR (B AND ~A AND ~C) OR (C AND ~B AND ~A) 267 | max(min(0,0.5,0), min(0.5,1,0), min(1,0.5,1)) == 0.5 268 | """ 269 | 270 | def F(z: float) -> float: 271 | # Reminder how it works for 2 args 272 | # x, y = a(z), b(z) 273 | # return max(min(x, 1-y), min(1-x, y)) 274 | 275 | M: set[float] = { 276 | f(z) for f in funcs 277 | } # a set of all membership values over all given functions to be iterated over 278 | # we need to go over each value in the set, calc min(x, inverse(rest)), from that calc max 279 | # for x in args: 280 | # print(x, [1-y for y in args-set([x])]) 281 | 282 | # FYI: this works because M-set([x]) returns a new set without x, which we use to construct a new set 283 | # with inverted values - however, if M only has one value, 284 | # which is the case if all given values are equal - we have to handle an empty generator expression, 285 | # which the "or (1-x,)" does. 286 | # Lastly, the *(...) is needed because min only takes one single iterator, so we need to unzip. 287 | return max(min((x, *({1 - y for y in M - set([x])} or (1 - x,)))) for x in M) 288 | 289 | return F 290 | -------------------------------------------------------------------------------- /src/fuzzylogic/defuzz.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from .classes import Membership, Set 7 | 8 | 9 | def cog(target_weights: list[tuple[Set, float]]) -> float: 10 | """ 11 | Defuzzify using the center-of-gravity (or centroid) method. 12 | target_weights: list of tuples (then_set, weight) 13 | 14 | The COG is defined by the formula: 15 | 16 | COG = (∑ μᵢ × xᵢ) / (∑ μᵢ) 17 | 18 | where: 19 | • μᵢ is the membership value for the iᵗʰ element, 20 | • xᵢ is the corresponding value for the iᵗʰ element in the output domain. 21 | 22 | """ 23 | 24 | sum_weights = sum(weight for _, weight in target_weights) 25 | if sum_weights == 0: 26 | raise ValueError("Total weight is zero. Cannot compute center-of-gravity.") 27 | sum_weighted_cogs = sum(then_set.center_of_gravity() * weight for then_set, weight in target_weights) 28 | return sum_weighted_cogs / sum_weights 29 | 30 | 31 | def bisector( 32 | aggregated_membership: Membership, 33 | points: list[float], 34 | step: float, 35 | ) -> float: 36 | """ 37 | Defuzzify via the bisector method. 38 | aggregated_membership: function mapping crisp value x -> membership degree (typically in [0,1]) 39 | points: discretized points in the target domain 40 | step: spacing between points 41 | """ 42 | total_area = sum(aggregated_membership(x) * step for x in points) 43 | half_area = total_area / 2.0 44 | cumulative = 0.0 45 | for x in points: 46 | cumulative += aggregated_membership(x) * step 47 | if cumulative >= half_area: 48 | return x 49 | return points[-1] 50 | 51 | 52 | def mom(aggregated_membership: Membership, points: list[float]) -> float | None: 53 | """ 54 | Mean of Maxima (MOM): average the x-values where the aggregated membership is maximal. 55 | """ 56 | max_points = _get_max_points(aggregated_membership, points) 57 | return sum(max_points) / len(max_points) if max_points else None 58 | 59 | 60 | def som(aggregated_membership: Membership, points: list[float]) -> float | None: 61 | """ 62 | Smallest of Maxima: return the smallest x-value at which the aggregated membership is maximal. 63 | """ 64 | return min(_get_max_points(aggregated_membership, points), default=None) 65 | 66 | 67 | def lom(aggregated_membership: Membership, points: list[float]) -> float | None: 68 | """ 69 | Largest of Maxima: return the largest x-value at which the aggregated membership is maximal. 70 | """ 71 | return max(_get_max_points(aggregated_membership, points), default=None) 72 | 73 | 74 | def _get_max_points(aggregated_membership: Membership, points: list[float]) -> list[float]: 75 | values_points = [(x, aggregated_membership(x)) for x in points] 76 | max_value = max(y for (_, y) in values_points) 77 | tol = 1e-6 # tolerance for floating point comparisons 78 | return [x for (x, y) in values_points if abs(y - max_value) < tol] 79 | -------------------------------------------------------------------------------- /src/fuzzylogic/estimate.py: -------------------------------------------------------------------------------- 1 | """Guesstimate the membership functions and their parameters of a fuzzy logic system. 2 | 3 | How this works: 4 | 1. We normalize the target array to a very small size, in the range [0, 1]. 5 | 2. We guess which functions match well based on the normalized array, 6 | only caring about the shape of the function, not the actual values. 7 | 3. We take the best matching functions and start guessing the parameters applying evolutionary algorithms. 8 | 4. Using the best matching functions with their parameters, we get some preliminary results. 9 | 5. We use the preliminary results to construct an array of the same size as the input array, 10 | but with the membership function applied. The difference of the two arrays is the new target. 11 | 6. Start the process again with the new target. Repeat until there is no difference between the two arrays. 12 | 7. The final result is the combination of those functions with their parameters. 13 | """ 14 | 15 | import contextlib 16 | import inspect 17 | import sys 18 | from collections.abc import Callable 19 | from itertools import permutations 20 | from random import choice, randint 21 | from statistics import median 22 | from typing import Any 23 | 24 | import numpy as np 25 | 26 | from .classes import Array 27 | from .functions import ( 28 | Membership, 29 | R, 30 | S, 31 | constant, 32 | gauss, 33 | rectangular, 34 | sigmoid, 35 | singleton, 36 | step, 37 | trapezoid, 38 | triangular, 39 | ) 40 | 41 | type MembershipSetup = Callable[[Any], Membership] 42 | 43 | np.seterr(all="raise") 44 | functions = [step, rectangular] 45 | 46 | argument1_functions = [singleton, constant] 47 | argument2_functions = [R, S, gauss] 48 | argument3_functions = [triangular, sigmoid] 49 | argument4_functions = [trapezoid] 50 | 51 | 52 | def normalize(target: Array, output_length: int = 16) -> Array: 53 | """Normalize and interpolate a numpy array. 54 | 55 | Return an array of output_length and normalized values. 56 | """ 57 | min_val = float(np.min(target)) 58 | max_val = float(np.max(target)) 59 | if min_val == max_val: 60 | return np.ones(output_length) 61 | normalized_array = (target - min_val) / (max_val - min_val) 62 | normalized_array = np.interp( 63 | np.linspace(0, 1, output_length), np.linspace(0, 1, len(normalized_array)), normalized_array 64 | ) 65 | return normalized_array 66 | 67 | 68 | def guess_function(target: Array) -> MembershipSetup: 69 | normalized = normalize(target) 70 | return constant if np.all(normalized == 1) else singleton 71 | 72 | 73 | def fitness(func: Membership, target: Array, certainty: int | None = None) -> float: 74 | """Compute the difference between the array and the function evaluated at the parameters. 75 | 76 | if the error is 0, we have a perfect match: fitness -> 1 77 | if the error approaches infinity, we have a bad match: fitness -> 0 78 | """ 79 | test = np.fromiter([func(x) for x in np.arange(*target.shape)], float) 80 | result = 1 / (np.sum(np.abs((test - target))) + 1) 81 | return result if certainty is None else round(result, certainty) 82 | 83 | 84 | def seed_population(func: MembershipSetup, target: Array) -> dict[tuple[float, ...], float]: 85 | # create a random population of parameters 86 | params = [p for p in inspect.signature(func).parameters.values() if p.kind == p.POSITIONAL_OR_KEYWORD] 87 | seed_population: dict[tuple[float, ...], float] = {} 88 | seed_numbers: list[float] = [ 89 | sys.float_info.min, 90 | sys.float_info.max, 91 | 0, 92 | 1, 93 | -1, 94 | 0.5, 95 | -0.5, 96 | min(target), 97 | max(target), 98 | float(np.argmax(target)), 99 | ] 100 | # seed population 101 | for combination in permutations(seed_numbers, len(params)): 102 | with contextlib.suppress(Exception): 103 | seed_population[combination] = fitness(func(*combination), target) 104 | assert seed_population, "Failed to seed population - wtf?" 105 | return seed_population 106 | 107 | 108 | def reproduce(parent1: tuple[float, ...], parent2: tuple[float, ...]) -> tuple[float, ...]: 109 | child: list[float] = [] 110 | for p1, p2 in zip(parent1, parent2): 111 | # mix the parts of the floats by randomness within the range of the parents 112 | # adding a random jitter should avoid issues when p1 == p2 113 | a1, a2 = np.frexp(p1) 114 | b1, b2 = np.frexp(p2) 115 | a1 = float(a1) 116 | b1 = float(b1) 117 | a2 = float(a2) 118 | b2 = float(b2) 119 | a1 += randint(-1, 1) 120 | a2 += randint(-1, 1) 121 | b1 += randint(-1, 1) 122 | b2 += randint(-1, 1) 123 | child.append(float((a1 + b1) / 2) * 2 ** np.random.uniform(a2, b2)) 124 | return tuple(child) 125 | 126 | 127 | def guess_parameters( 128 | func: MembershipSetup, target: Array, precision: int | None = None, certainty: int | None = None 129 | ) -> tuple[float, ...]: 130 | """Find the best fitting parameters for a function, targeting an array. 131 | 132 | Args: 133 | func (MembershipSetup): A possibly matching membership function. 134 | target (Array): The target array to fit the function to. 135 | precision (int | None): The precision of the parameters. 136 | certainty (int | None): The certainty of the fitness score. 137 | 138 | Returns: 139 | tuple[float, ...]: The best fitting parameters for the function. 140 | """ 141 | 142 | def best() -> tuple[float, ...]: 143 | return sorted(population.items(), key=lambda x: x[1])[0][0] 144 | 145 | seed_pop = seed_population(func, target) 146 | population = seed_pop.copy() 147 | print(seed_pop) 148 | # iterate until convergence or max iterations 149 | pressure = 0 150 | pop_size = 100 151 | last_pop = {} 152 | for generation in range(12): 153 | # sort the population by fitness 154 | pop: list[tuple[tuple[float, ...], float]] = sorted( 155 | population.items(), key=lambda x: x[1], reverse=True 156 | )[:pop_size] 157 | if not pop: 158 | population = last_pop 159 | return best() 160 | print(f"Best so far:: {func.__name__}(*{pop[0][0]}) with {pop[0][1]:.10f}") 161 | # maybe the seed population already has a perfect match? 162 | if pop[0][1] == 1: 163 | print("Lucky!") 164 | return best() 165 | # the next generation 166 | new_population: dict[tuple[float, ...], float] = {} 167 | killed = 0 168 | for parent1 in pop: 169 | while True: 170 | with contextlib.suppress(Exception): 171 | # select another parent and try to reproduce - try until it works once 172 | # at least one viable child is guaranteed (parent1 == parent2) 173 | parent2 = choice(pop) 174 | child = reproduce(parent1[0], parent2[0]) 175 | new_population[child] = (fit := fitness(func(*child), target)) 176 | # check for convergence 177 | if fit == 1: 178 | print("Lucky!") 179 | return child 180 | # kill the worst 181 | if fit <= pressure: 182 | del new_population[child] 183 | killed += 1 184 | if killed % 1000 == 0: 185 | print("xxx") 186 | if killed > 10000: 187 | break 188 | else: 189 | if len(new_population) % 1000 == 0: 190 | print("...") 191 | break 192 | print( 193 | f"Generation {generation}: {killed} killed; pop size {len(population)}; pressure {pressure:.10f}" 194 | ) 195 | if last_pop == new_population: 196 | break 197 | last_pop = population 198 | population = new_population 199 | # Under Pressure! 200 | if len(population) == 1: 201 | print("Only a single survivor!") 202 | break 203 | if killed > 1000: 204 | pop_size += 1000 205 | pressure **= 0.999 206 | population |= seed_pop 207 | else: 208 | pressure: float = median([x[1] for x in population.items()]) 209 | return best() 210 | 211 | 212 | def shave(target: Array, components: dict[Membership, tuple[float, ...]]) -> Array: 213 | """Remove the membership functions from the target array.""" 214 | result: Array = np.zeros_like(target, dtype=float) 215 | for func, params in components.items(): 216 | f = func(*params) 217 | result += np.fromiter((f(x) for x in np.arange(*target.shape)), dtype=float) # type: ignore 218 | return np.asarray(target - result, dtype=target.dtype) # type: ignore 219 | -------------------------------------------------------------------------------- /src/fuzzylogic/functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | functions.py - General-purpose functions that map R -> [0,1]. 3 | 4 | These functions work as closures. 5 | The inner function uses the variables of the outer function. 6 | 7 | These functions work in two steps: prime and call. 8 | In the first step the function is constructed, initialized and 9 | constants pre-evaluated. In the second step the actual value 10 | is passed into the function, using the arguments of the first step. 11 | 12 | Definitions 13 | ----------- 14 | These functions are used to determine the *membership* of a value x in a fuzzy- 15 | set. Thus, the 'height' is the variable 'm' in general. 16 | In a normal set there is at least one m with m == 1. This is the default. 17 | In a non-normal set, the global maximum and minimum is skewed. 18 | The following definitions are for normal sets. 19 | 20 | The intervals with non-zero m are called 'support', short s_m 21 | The intervals with m == 1 are called 'core', short c_m 22 | The intervals with max(m) are called "height" 23 | The intervals m != 1 and m != 0 are called 'boundary'. 24 | The intervals with m == 0 are called 'unsupported', short no_m 25 | 26 | In a fuzzy set with one and only one m == 1, this element is called 'prototype'. 27 | """ 28 | 29 | from __future__ import annotations 30 | 31 | from collections.abc import Callable 32 | from math import exp, isinf, isnan, log 33 | from typing import TYPE_CHECKING, Any 34 | 35 | if TYPE_CHECKING: 36 | from .classes import SingletonSet 37 | 38 | type Membership = Callable[[float], float] 39 | 40 | 41 | try: 42 | from numba import njit as njit # ready for prime time? 43 | 44 | raise ImportError 45 | 46 | except ImportError: 47 | 48 | def njit(func: Membership) -> Membership: 49 | return func 50 | 51 | 52 | LOW_HIGH = "low must be less than high" 53 | 54 | ##################### 55 | # SPECIAL FUNCTIONS # 56 | ##################### 57 | 58 | 59 | def inv(g: Membership) -> Membership: 60 | """Invert the given function within the unit-interval. 61 | 62 | For sets, the ~ operator uses this. It is equivalent to the TRUTH value of FALSE. 63 | """ 64 | 65 | def f(x: float) -> float: 66 | return float(1 - g(x)) 67 | 68 | return f 69 | 70 | 71 | def noop() -> Membership: 72 | """Do nothing and return the value as is. 73 | 74 | Useful for testing. 75 | """ 76 | 77 | def f(x: float) -> float: 78 | return x 79 | 80 | return f 81 | 82 | 83 | def constant(c: float) -> Membership: 84 | """Return always the same value, no matter the input. 85 | 86 | Useful for testing. 87 | >>> f = constant(1) 88 | >>> f(0) 89 | 1 90 | """ 91 | 92 | def f(_: float) -> float: 93 | return c 94 | 95 | return f 96 | 97 | 98 | def alpha( 99 | *, 100 | floor: float = 0, 101 | ceiling: float = 1, 102 | func: Membership, 103 | floor_clip: float | None = None, 104 | ceiling_clip: float | None = None, 105 | ) -> Membership: 106 | """Clip a function's values. 107 | 108 | This is used to either cut off the upper or lower part of a graph. 109 | Actually, this is more like a hedge but doesn't make sense for sets. 110 | """ 111 | assert floor <= ceiling, breakpoint() 112 | assert 0 <= floor, breakpoint() 113 | assert ceiling <= 1, breakpoint() 114 | 115 | floor_clip = floor if floor_clip is None else floor_clip 116 | ceiling_clip = ceiling if ceiling_clip is None else ceiling_clip 117 | 118 | def f(x: float) -> float: 119 | m = func(x) 120 | if m >= ceiling: 121 | return ceiling_clip 122 | elif m <= floor: 123 | return floor_clip 124 | else: 125 | return m 126 | 127 | return f 128 | 129 | 130 | def normalize(height: float, func: Callable[[float], float]) -> Callable[[float], float]: 131 | """Map [0,1] to [0,1] so that max(array) == 1.""" 132 | assert 0 < height <= 1 133 | 134 | def f(x: float) -> float: 135 | return func(x) / height 136 | 137 | return f 138 | 139 | 140 | def moderate(func: Callable[[float], float]) -> Callable[[float], float]: 141 | """Map [0,1] -> [0,1] with bias towards 0.5. 142 | 143 | For instance this is needed to dampen extremes. 144 | """ 145 | 146 | def f(x: float) -> float: 147 | return 1 / 2 + 4 * (func(x) - 1 / 2) ** 3 148 | 149 | return f 150 | 151 | 152 | ######################## 153 | # MEMBERSHIP FUNCTIONS # 154 | ######################## 155 | 156 | 157 | def singleton(c: float, no_m: float = 0, c_m: float = 1) -> SingletonSet: 158 | """A singleton function. 159 | 160 | This is unusually tricky because the CoG sums up all values and divides by the number of values, which 161 | may result in 0 due to rounding errors. 162 | Additionally and more significantly, a singleton well within domain range but not within 163 | its resolution will never be found and considered. Thus, singletons need special treatment. 164 | 165 | We solve this issue by returning a special subclass (which must be imported here due to circular import), 166 | which overrides the normal CoG implementation, but still works with the rest of the code. 167 | """ 168 | from .classes import SingletonSet 169 | 170 | return SingletonSet(c, no_m=no_m, c_m=c_m) 171 | 172 | 173 | def linear(m: float = 0, b: float = 0) -> Membership: 174 | """A textbook linear function with y-axis section and gradient. 175 | 176 | f(x) = m*x + b 177 | BUT CLIPPED. 178 | 179 | >>> f = linear(1, -1) 180 | >>> f(-2) # should be -3 but clipped 181 | 0 182 | >>> f(0) # should be -1 but clipped 183 | 0 184 | >>> f(1) 185 | 0 186 | >>> f(1.5) 187 | 0.5 188 | >>> f(2) 189 | 1 190 | >>> f(3) # should be 2 but clipped 191 | 1 192 | """ 193 | 194 | def f(x: float) -> float: 195 | y = m * x + b 196 | if y <= 0: 197 | return 0 198 | elif y >= 1: 199 | return 1 200 | else: 201 | return y 202 | 203 | return f 204 | 205 | 206 | def step(limit: float, /, *, left: float = 0, right: float = 1, at_lmt: float | None = None) -> Membership: 207 | """A step function. 208 | 209 | Coming from left, the function returns the *left* argument. 210 | At the limit, it returns *at_lmt* or the average of left and right. 211 | After the limit, it returns the *right* argument. 212 | >>> f = step(2) 213 | >>> f(1) 214 | 0 215 | >>> f(2) 216 | 0.5 217 | >>> f(3) 218 | 1 219 | """ 220 | assert 0 <= left <= 1 and 0 <= right <= 1 221 | 222 | def f(x: float) -> float: 223 | if x < limit: 224 | return left 225 | elif x > limit: 226 | return right 227 | else: 228 | return at_lmt if at_lmt is not None else (left + right) / 2 229 | 230 | return f 231 | 232 | 233 | def bounded_linear( 234 | low: float, high: float, *, c_m: float = 1, no_m: float = 0, inverse: bool = False 235 | ) -> Membership: 236 | """Variant of the linear function with gradient being determined by bounds. 237 | 238 | The bounds determine minimum and maximum value-mappings, 239 | but also the gradient. As [0, 1] must be the bounds for y-values, 240 | left and right bounds specify 2 points on the graph, for which the formula 241 | f(x) = y = (y2 - y1) / (x2 - x1) * (x - x1) + y1 = (y2 - y1) / (x2 - x1) * 242 | (x - x2) + y2 243 | 244 | (right_y - left_y) / ((right - left) * (x - self.left) + left_y) 245 | works. 246 | 247 | >>> f = bounded_linear(2, 3) 248 | >>> f(1) 249 | 0.0 250 | >>> f(2) 251 | 0.0 252 | >>> f(2.5) 253 | 0.5 254 | >>> f(3) 255 | 1.0 256 | >>> f(4) 257 | 1.0 258 | """ 259 | assert low < high, LOW_HIGH 260 | assert c_m > no_m, "core_m must be greater than unsupported_m" 261 | 262 | if inverse: 263 | c_m, no_m = no_m, c_m 264 | 265 | gradient = (c_m - no_m) / (high - low) 266 | 267 | # special cases found by hypothesis 268 | 269 | def g_0(_: Any) -> float: 270 | return (c_m + no_m) / 2 271 | 272 | if gradient == 0: 273 | return g_0 274 | 275 | def g_inf(x: float) -> float: 276 | asymptode = (high + low) / 2 277 | if x < asymptode: 278 | return no_m 279 | elif x > asymptode: 280 | return c_m 281 | else: 282 | return (c_m + no_m) / 2 283 | 284 | if isinf(gradient): 285 | return g_inf 286 | 287 | def f(x: float) -> float: 288 | y = gradient * (x - low) + no_m 289 | if y < 0: 290 | return 0.0 291 | return 1.0 if y > 1 else y 292 | 293 | return f 294 | 295 | 296 | def R(low: float, high: float) -> Membership: 297 | """Simple alternative for bounded_linear(). 298 | 299 | THIS FUNCTION ONLY CAN HAVE A POSITIVE SLOPE - 300 | USE THE S() FUNCTION FOR NEGATIVE SLOPE. 301 | """ 302 | assert low < high, f"{low} < {high} is not true." 303 | 304 | def f(x: float) -> float: 305 | if x < low or isinf(high - low): 306 | return 0 307 | elif low <= x <= high: 308 | return (x - low) / (high - low) 309 | else: 310 | return 1 311 | 312 | return f 313 | 314 | 315 | def S(low: float, high: float) -> Membership: 316 | """Simple alternative for bounded_linear. 317 | 318 | THIS FUNCTION ONLY CAN HAVE A NEGATIVE SLOPE - 319 | USE THE R() FUNCTION FOR POSITIVE SLOPE. 320 | """ 321 | assert low < high, f"{low} must be less than {high}." 322 | 323 | def f(x: float) -> float: 324 | if x <= low: 325 | return 1 326 | elif low < x < high: 327 | # factorized to avoid nan 328 | return high / (high - low) - x / (high - low) 329 | else: 330 | return 0 331 | 332 | return f 333 | 334 | 335 | def rectangular(low: float, high: float, *, c_m: float = 1, no_m: float = 0) -> Membership: 336 | """Basic rectangular function that returns the core_y for the core else 0. 337 | 338 | ______ 339 | | | 340 | ____| |___ 341 | """ 342 | assert low < high, f"{low}, {high}" 343 | 344 | def f(x: float) -> float: 345 | return no_m if x < low or high < x else c_m 346 | 347 | return f 348 | 349 | 350 | def triangular( 351 | low: float, high: float, *, c: float | None = None, c_m: float = 1, no_m: float = 0 352 | ) -> Membership: 353 | r"""Basic triangular norm as combination of two linear functions. 354 | 355 | /\ 356 | ____/ \___ 357 | 358 | """ 359 | assert low < high, "low must be less than high." 360 | assert no_m < c_m 361 | 362 | c = c if c is not None else (low + high) / 2.0 363 | assert low < c < high, "peak must be inbetween" 364 | 365 | left_slope = bounded_linear(low, c, no_m=0, c_m=c_m) 366 | right_slope = inv(bounded_linear(c, high, no_m=0, c_m=c_m)) 367 | 368 | def f(x: float) -> float: 369 | return left_slope(x) if x <= c else right_slope(x) 370 | 371 | return f 372 | 373 | 374 | def trapezoid( 375 | low: float, c_low: float, c_high: float, high: float, *, c_m: float = 1, no_m: float = 0 376 | ) -> Membership: 377 | r"""Combination of rectangular and triangular, for convenience. 378 | 379 | ____ 380 | / \ 381 | ____/ \___ 382 | 383 | """ 384 | assert low < c_low <= c_high < high 385 | assert 0 <= no_m < c_m <= 1 386 | 387 | left_slope = bounded_linear(low, c_low, c_m=c_m, no_m=no_m) 388 | right_slope = bounded_linear(c_high, high, c_m=c_m, no_m=no_m, inverse=True) 389 | 390 | def f(x: float) -> float: 391 | if x < low or high < x: 392 | return no_m 393 | elif x < c_low: 394 | return left_slope(x) 395 | elif x > c_high: 396 | return right_slope(x) 397 | else: 398 | return c_m 399 | 400 | return f 401 | 402 | 403 | def sigmoid(L: float, k: float, x0: float = 0) -> Membership: 404 | """Special logistic function. 405 | 406 | http://en.wikipedia.org/wiki/Logistic_function 407 | 408 | f(x) = L / (1 + e^(-k*(x-x0))) 409 | with 410 | x0 = x-value of the midpoint 411 | L = the curve's maximum value 412 | k = steepness 413 | """ 414 | # need to be really careful here, otherwise we end up in nanland 415 | assert 0 < L <= 1, "L invalid." 416 | 417 | def f(x: float) -> float: 418 | if isnan(k * x): 419 | # e^(0*inf) == 1 420 | o = 1.0 421 | else: 422 | try: 423 | o = exp(-k * (x - x0)) 424 | except OverflowError: 425 | o = float("inf") 426 | return L / (1 + o) 427 | 428 | return f 429 | 430 | 431 | def bounded_sigmoid(low: float, high: float, inverse: bool = False) -> Membership: 432 | """ 433 | Calculate a weight based on the sigmoid function. 434 | 435 | Specify the lower limit where f(x) = 0.1 and the 436 | upper with f(x) = 0.9 and calculate the steepness and elasticity 437 | based on these. We don't need the general logistic function as we 438 | operate on [0,1]. 439 | 440 | core idea: 441 | f(x) = 1. / (1. + exp(x * (4. * log(3)) / (low - high)) * 442 | 9 * exp(low * -(4. * log(3)) / (low - high))) 443 | 444 | How I got this? IIRC I was playing around with linear equations and 445 | boundary conditions of sigmoid funcs on wolframalpha.. 446 | 447 | previously factored to: 448 | k = -(4. * log(3)) / (low - high) 449 | o = 9 * exp(low * k) 450 | return 1 / (1 + exp(-k * x) * o) 451 | 452 | vars 453 | ---- 454 | low: x-value with f(x) = 0.1 455 | for x < low: m -> 0 456 | high: x-value with f(x) = 0.9 457 | for x > high: m -> 1 458 | 459 | >>> f = bounded_sigmoid(0, 1) 460 | >>> f(0) 461 | 0.1 462 | >>> round(f(1), 2) 463 | 0.9 464 | >>> round(f(100000), 2) 465 | 1.0 466 | >>> round(f(-100000), 2) 467 | 0.0 468 | """ 469 | assert low < high, LOW_HIGH 470 | 471 | if inverse: 472 | low, high = high, low 473 | 474 | k = (4.0 * log(3)) / (low - high) 475 | try: 476 | # if high - low underflows to 0.. 477 | if isinf(k): 478 | p = 0.0 479 | # just in case k -> 0 and low -> inf 480 | elif isnan(-k * low): 481 | p = 1.0 482 | else: 483 | p = exp(-k * low) 484 | except OverflowError: 485 | p = float("inf") 486 | 487 | def f(x: float) -> float: 488 | try: 489 | # e^(0*inf) = 1 for both -inf and +inf 490 | q = 1.0 if (isinf(k) and x == 0) or (k == 0 and isinf(x)) else exp(x * k) 491 | except OverflowError: 492 | q = float("inf") 493 | 494 | # e^(inf)*e^(-inf) == 1 495 | r = p * q 496 | if isnan(r): 497 | r = 1 498 | return 1 / (1 + 9 * r) 499 | 500 | return f 501 | 502 | 503 | def bounded_exponential(k: float = 0.1, limit: float = 1) -> Membership: 504 | """Function that goes through the origin and approaches a limit. 505 | k determines the steepness. The function defined for [0, +inf). 506 | Useful for things that can't be below 0 but may not have a limit like temperature 507 | or time, so values are always defined. 508 | f(x)=limit-limit/e^(k*x) 509 | 510 | Again: This function assumes x >= 0, there are no checks for this assumption! 511 | """ 512 | assert limit > 0 513 | assert k > 0 514 | 515 | def f(x: float) -> float: 516 | try: 517 | return limit - limit / exp(k * x) 518 | except OverflowError: 519 | return limit 520 | 521 | return f 522 | 523 | 524 | def simple_sigmoid(k: float = 0.229756) -> Membership: 525 | """Sigmoid variant with only one parameter (steepness). 526 | 527 | The midpoint is 0. 528 | The slope is positive for positive k and negative k. 529 | f(x) is within [0,1] for any real k and x. 530 | >>> f = simple_sigmoid() 531 | >>> round(f(-1000), 2) 532 | 0.0 533 | >>> f(0) 534 | 0.5 535 | >>> round(f(1000), 2) 536 | 1.0 537 | >>> round(f(-20), 2) 538 | 0.01 539 | >>> round(f(20), 2) 540 | 0.99 541 | """ 542 | 543 | def f(x: float) -> float: 544 | if isinf(x) and k == 0: 545 | return 1 / 2 546 | try: 547 | return 1 / (1 + exp(x * -k)) 548 | except OverflowError: 549 | return 0.0 550 | 551 | return f 552 | 553 | 554 | def triangular_sigmoid(low: float, high: float, c: float | None = None) -> Membership: 555 | """Version of triangular using sigmoids instead of linear. 556 | 557 | THIS FUNCTION PEAKS AT 0.9 558 | 559 | >>> g = triangular_sigmoid(2, 4) 560 | >>> g(2) 561 | 0.1 562 | >>> round(g(3), 2) 563 | 0.9 564 | """ 565 | assert low < high, LOW_HIGH 566 | c = c if c is not None else (low + high) / 2.0 567 | assert low < c < high, "c must be inbetween" 568 | 569 | left_slope = bounded_sigmoid(low, c) 570 | right_slope = inv(bounded_sigmoid(c, high)) 571 | 572 | def f(x: float) -> float: 573 | return left_slope(x) if x <= c else right_slope(x) 574 | 575 | return f 576 | 577 | 578 | def gauss(c: float, b: float, *, c_m: float = 1) -> Membership: 579 | """Defined by ae^(-b(x-x0)^2), a gaussian distribution. 580 | 581 | Basically a triangular sigmoid function, it comes close to human perception. 582 | 583 | vars 584 | ---- 585 | c_m (a) 586 | defines the maximum y-value of the graph 587 | b 588 | defines the steepness 589 | c (x0) 590 | defines the symmetry center/peak of the graph 591 | """ 592 | assert 0 < c_m <= 1 593 | assert 0 < b, "b must be greater than 0" 594 | 595 | def f(x: float) -> float: 596 | try: 597 | o = (x - c) ** 2 598 | except OverflowError: 599 | return 0 600 | return c_m * exp(-b * o) 601 | 602 | return f 603 | 604 | 605 | if __name__ == "__main__": 606 | import doctest 607 | 608 | doctest.testmod() 609 | -------------------------------------------------------------------------------- /src/fuzzylogic/hedges.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lingual hedges modify curves of membership values. 3 | 4 | These should work with Sets and functions. 5 | """ 6 | 7 | from typing import overload 8 | 9 | from .classes import Set 10 | from .functions import Membership 11 | 12 | 13 | @overload 14 | def very(G: Set) -> Set: ... 15 | @overload 16 | def very(G: Membership) -> Membership: ... 17 | def very(G: Set | Membership) -> Set | Membership: 18 | """Sharpen memberships so that only the values close 1 stay at the top.""" 19 | if isinstance(G, Set): 20 | 21 | def s_f(g: Membership) -> Membership: 22 | def f(x: float) -> float: 23 | return g(x) ** 2 24 | 25 | return f 26 | 27 | return Set(s_f(G.func), domain=G.domain, name=f"very_{G.name}") 28 | else: 29 | 30 | def f(x: float) -> float: 31 | return G(x) ** 2 32 | 33 | return f 34 | 35 | 36 | @overload 37 | def plus(G: Set) -> Set: ... 38 | @overload 39 | def plus(G: Membership) -> Membership: ... 40 | def plus(G: Set | Membership) -> Set | Membership: 41 | """Sharpen memberships like 'very' but not as strongly.""" 42 | if isinstance(G, Set): 43 | 44 | def s_f(g: Membership) -> Membership: 45 | def f(x: float): 46 | return g(x) ** 1.25 47 | 48 | return f 49 | 50 | return Set(s_f(G.func), domain=G.domain, name=f"plus_{G.name}") 51 | else: 52 | 53 | def f(x: float) -> float: 54 | return G(x) ** 1.25 55 | 56 | return f 57 | 58 | 59 | @overload 60 | def minus(G: Set) -> Set: ... 61 | @overload 62 | def minus(G: Membership) -> Membership: ... 63 | def minus(G: Set | Membership) -> Set | Membership: 64 | """Increase membership support so that more values hit the top.""" 65 | if isinstance(G, Set): 66 | 67 | def s_f(g: Membership) -> Membership: 68 | def f(x: float) -> float: 69 | return g(x) ** 0.75 70 | 71 | return f 72 | 73 | return Set(s_f(G.func), domain=G.domain, name=f"minus_{G.name}") 74 | else: 75 | 76 | def f(x: float) -> float: 77 | return G(x) ** 0.75 78 | 79 | return f 80 | -------------------------------------------------------------------------------- /src/fuzzylogic/neural_network.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import numpy as np 4 | 5 | from .classes import Array 6 | from .functions import R, S, constant, gauss, rectangular, sigmoid, singleton, step, trapezoid, triangular 7 | 8 | functions = [step, rectangular] 9 | 10 | argument1_functions = [singleton, constant] 11 | argument2_functions = [R, S, gauss] 12 | argument3_functions = [triangular, sigmoid] 13 | argument4_functions = [trapezoid] 14 | 15 | 16 | def generate_examples() -> dict[str, list[Array]]: 17 | examples: dict[str, list[Array]] = defaultdict(lambda: []) 18 | examples["constant"] = [np.ones(16)] 19 | for x in range(16): 20 | A = np.zeros(16) 21 | A[x] = 1 22 | examples["singleton"].append(A) 23 | 24 | for x in range(1, 16): 25 | func = R(0, x) 26 | examples["R"].append(func(np.linspace(0, 1, 16))) # type: ignore 27 | return examples 28 | -------------------------------------------------------------------------------- /src/fuzzylogic/tools.py: -------------------------------------------------------------------------------- 1 | """Functions to evaluate, infer and defuzzify.""" 2 | 3 | from collections.abc import Callable 4 | from math import isinf 5 | 6 | from .classes import Domain 7 | 8 | 9 | def round_partial(value: float, res: float) -> float: 10 | """ 11 | Round any value to any arbitrary precision. 12 | 13 | >>> round_partial(0.405, 0.02) 14 | 0.4 15 | >>> round_partial(0.412, 0.02) 16 | 0.42 17 | >>> round_partial(1.38, 0.25) 18 | 1.5 19 | >>> round_partial(1.12, 0.25) 20 | 1.0 21 | >>> round_partial(9.24, 0.25) 22 | 9.25 23 | >>> round_partial(7.76, 0.25) 24 | 7.75 25 | >>> round_partial(987654321, 100) 26 | 987654300 27 | >>> round_partial(3.14, 0) 28 | 3.14 29 | """ 30 | # backed up by wolframalpha 31 | return value if res == 0 or isinf(res) else round(value / res) * res 32 | 33 | 34 | def rescale( 35 | out_min: float, out_max: float, *, in_min: float = 0, in_max: float = 1 36 | ) -> Callable[[float], float]: 37 | """Scale from one domain to another. 38 | 39 | Tests only cover scaling from [0,1] (with default in_min, in_max!) 40 | to R. 41 | 42 | For arbitrary R -> R additional testing is required, 43 | but it should work in general out of the box. 44 | 45 | Originally used the algo from SO 46 | (OUT_max - OUT_min)*(x - IN_min) / (IN_max - IN_min) + OUT_min 47 | but there are too many edge cases thanks to over/underflows. 48 | Current factorized algo was proposed as equivalent by wolframalpha, 49 | which seems more stable. 50 | """ 51 | assert in_min < in_max 52 | 53 | # for easier handling of the formula 54 | a = out_min 55 | b = out_max 56 | c = in_min 57 | d = in_max 58 | m = d - c 59 | n = a * d 60 | o = b * c 61 | 62 | def f(x: float) -> float: 63 | return (n - a * x - o + b * x) / m 64 | 65 | return f 66 | 67 | 68 | def weighted_sum(*, weights: dict[str, float], target_d: Domain) -> Callable[[dict[str, float]], float]: 69 | """Used for weighted decision trees and such. 70 | 71 | Parametrize with dict of factorname -> weight and domain of results. 72 | Call with a dict of factorname -> [0, 1] 73 | 74 | There SHOULD be the same number of items (with the same names!) 75 | of weights and factors, but it doesn't have to be - however 76 | set(factors.names) <= set(weights.names) - in other words: 77 | there MUST be at least as many items in weights as factors. 78 | """ 79 | assert sum(weights.values()) == 1, breakpoint() 80 | 81 | rsc = rescale(target_d._low, target_d._high) # type: ignore 82 | 83 | def f(memberships: dict[str, float]) -> float: 84 | result = sum(r * weights[n] for n, r in memberships.items()) 85 | return round_partial(rsc(result), target_d._res) # type: ignore 86 | 87 | return f 88 | -------------------------------------------------------------------------------- /src/fuzzylogic/truth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions that transform a given membership value to a truth value. 3 | 4 | How this can be useful? Beats me. Found it somewhere on the internet, 5 | never needed it. 6 | """ 7 | 8 | from math import sqrt 9 | 10 | 11 | def true(m: float) -> float: 12 | """The membership-value is its own truth-value.""" 13 | return m 14 | 15 | 16 | def false(m: float) -> float: 17 | """The opposite of TRUE.""" 18 | return 1 - m 19 | 20 | 21 | def fairly_false(m: float) -> float: 22 | """Part of a circle in quadrant I.""" 23 | return sqrt(1 - m**2) 24 | 25 | 26 | def fairly_true(m: float) -> float: 27 | """Part of a circle in quadrant II.""" 28 | return sqrt(1 - (1 - m) ** 2) 29 | 30 | 31 | def very_false(m: float) -> float: 32 | """Part of a circle in quadrant III.""" 33 | return -sqrt(1 - (1 - m) ** 2) 34 | 35 | 36 | def very_true(m: float) -> float: 37 | """Part of a circle in quadrant IV.""" 38 | return -sqrt(1 - m**2) 39 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | """WIP tests.""" 2 | 3 | import inspect 4 | 5 | # =========================================== 6 | 7 | 8 | # =========================================== 9 | 10 | for name, func in globals().copy().items(): 11 | if name.startswith("test_"): 12 | print(f" ↓↓↓↓↓↓↓ {name} ↓↓↓↓↓↓") 13 | print(inspect.getsource(func)) 14 | func() 15 | print(f"↑↑↑↑↑↑ {name} ↑↑↑↑↑↑") 16 | print() 17 | -------------------------------------------------------------------------------- /tests/test_caro.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from fuzzylogic.classes import Domain, Rule, rule_from_table 4 | from fuzzylogic.functions import R, S, trapezoid 5 | 6 | temp = Domain("Temperatur", -30, 100, res=0.0001) # ,res=0.1) 7 | temp.kalt = S(-10, 30) 8 | temp.heiß = R(30, 70) 9 | temp.mittel = ~temp.heiß & ~temp.kalt 10 | 11 | 12 | tan = Domain("tandelta", 0, 1.3, res=0.0001) # ,res=0.1) 13 | tan.klein = S(0.1, 0.5) 14 | tan.groß = R(0.5, 0.9) 15 | tan.mittel = ~tan.groß & ~tan.klein 16 | 17 | gef = Domain("Gefahrenbewertung", -0.5, 1.5, res=0.0001) # ,res=0.1) 18 | gef.klein = trapezoid(-0.5, 0, 0, 0.5) 19 | gef.groß = trapezoid(0.5, 1, 1, 1.5) 20 | gef.mittel = trapezoid(0, 0.5, 0.5, 1) 21 | 22 | R1 = Rule({(temp.kalt, tan.klein): gef.klein}) 23 | R2 = Rule({(temp.mittel, tan.klein): gef.klein}) 24 | R3 = Rule({(temp.heiß, tan.klein): gef.klein}) 25 | R4 = Rule({(temp.kalt, tan.mittel): gef.klein}) 26 | R5 = Rule({(temp.mittel, tan.mittel): gef.mittel}) 27 | R6 = Rule({(temp.heiß, tan.mittel): gef.groß}) 28 | R7 = Rule({(temp.kalt, tan.groß): gef.mittel}) 29 | R8 = Rule({(temp.mittel, tan.groß): gef.groß}) 30 | R9 = Rule({(temp.heiß, tan.groß): gef.groß}) 31 | 32 | 33 | rules = R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 34 | 35 | table = """ 36 | tan.klein tan.mittel tan.groß 37 | temp.kalt gef.klein gef.klein gef.mittel 38 | temp.mittel gef.klein gef.mittel gef.groß 39 | temp.heiß gef.klein gef.groß gef.groß 40 | """ 41 | 42 | 43 | table_rules = rule_from_table(table, globals()) 44 | 45 | assert table_rules == rules 46 | 47 | value = {temp: 20, tan: 0.55} 48 | result = rules(value) 49 | assert isinstance(result, float) 50 | assert np.isclose(result, 0.45, atol=0.0001) 51 | 52 | """ 53 | For the input {temp: 20, tan: 0.55}: 54 | * temp: 20 activates: 55 | temp.mittel (membership = 0.75) 56 | temp.kalt (membership = 0.25) 57 | 58 | * tan: 0.55 activates: 59 | tan.mittel (membership = 0.875) 60 | tan.groß (membership = 0.125) 61 | 62 | This triggers four rules: 63 | 64 | * R4: (temp.kalt, tan.mittel) → gef.klein (firing strength = min(0.25, 0.875) = 0.25) 65 | * R5: (temp.mittel, tan.mittel) → gef.mittel (firing strength = 0.75) 66 | * R7: (temp.kalt, tan.groß) → gef.mittel (firing strength = min(0.25, 0.125) = 0.125) 67 | * R8: (temp.mittel, tan.groß) → gef.groß (firing strength = 0.125) 68 | 69 | 70 | |------------------|------------------|------------------| 71 | | Rule | Consequent | Weight | Consequent CoG | 72 | |------------------|------------------|------------------| 73 | |R4|gef.klein| 0.25| 0.0 (midpoint of [-0.5, 0.5]) 74 | |R5|gef.mittel| 0.75| 0.5 (midpoint of [0, 1]) 75 | |R7|gef.mittel| 0.125| 0.5 76 | |R8|gef.groß| 0.125| 1.0 (midpoint of [0.5, 1.5]) 77 | 78 | COG = (0.25 * 0.0 + 0.75 * 0.5 + 0.125 * 0.5 + 0.125 * 1.0) / (0.25 + 0.75 + 0.125 + 0.125) 79 | = 0.5625/1.25 80 | = 0.45 81 | """ 82 | -------------------------------------------------------------------------------- /tests/test_defuzz.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | import timeit 5 | 6 | import numpy as np 7 | import pytest 8 | from hypothesis import assume, given 9 | from hypothesis import strategies as st 10 | 11 | from fuzzylogic.defuzz import ( 12 | _get_max_points, 13 | bisector, 14 | cog, 15 | lom, 16 | mom, 17 | som, 18 | ) 19 | from fuzzylogic.functions import Membership 20 | 21 | # --------------------------------------------------------------------------- 22 | # Core Testing Infrastructure 23 | # --------------------------------------------------------------------------- 24 | 25 | 26 | class DummyDomain: 27 | """Mock domain for testing fuzzy operations""" 28 | 29 | def __init__(self, low: float, high: float, n_points: int = 101): 30 | assert low < high, "Invalid domain bounds" 31 | self._low = low 32 | self._high = high 33 | self._n_points = n_points 34 | 35 | @property 36 | def range(self) -> list[float]: 37 | return np.linspace(self._low, self._high, self._n_points).tolist() 38 | 39 | 40 | class DummySet: 41 | """Mock fuzzy set with configurable properties""" 42 | 43 | def __init__(self, cog_value: float, membership_func: Membership | None = None): 44 | self._cog = cog_value 45 | self.membership_func = membership_func or (lambda x: 1.0) 46 | self.domain = None 47 | 48 | def center_of_gravity(self) -> float: 49 | return self._cog 50 | 51 | def __call__(self, x: float) -> float: 52 | return self.membership_func(x) 53 | 54 | 55 | # --------------------------------------------------------------------------- 56 | # Property-Based Tests 57 | # --------------------------------------------------------------------------- 58 | 59 | 60 | @given( 61 | cogs=st.lists(st.floats(min_value=-1e3, max_value=1e3), min_size=1, max_size=10), 62 | weights=st.lists(st.floats(min_value=0.1, max_value=1e3), min_size=1, max_size=10), 63 | domain=st.tuples(st.floats(min_value=-1e3), st.floats(min_value=-1e3)).filter(lambda x: x[0] < x[1]), 64 | ) 65 | def test_cog_weighted_average_property(cogs: list[float], weights: list[float], domain: tuple[float, float]): 66 | """Verify COG is proper weighted average of centroids""" 67 | assume(len(cogs) == len(weights)) 68 | low, high = domain 69 | domain_obj = DummyDomain(low, high) 70 | 71 | sets = [DummySet(cog) for cog in cogs] 72 | for s in sets: 73 | s.domain = domain_obj 74 | 75 | target_weights = list(zip(sets, weights)) 76 | result = cog(target_weights) 77 | 78 | total_weight = sum(weights) 79 | expected = sum(c * w for c, w in zip(cogs, weights)) / total_weight 80 | assert math.isclose(result, expected, rel_tol=1e-5, abs_tol=1e-5) 81 | 82 | 83 | @given( 84 | peak=st.floats(allow_nan=False, allow_infinity=False), 85 | width=st.floats(min_value=0.1, max_value=100), 86 | domain=st.tuples(st.floats(), st.floats()).filter(lambda x: x[0] < x[1]), 87 | ) 88 | def test_bisector_triangular_property(peak: float, width: float, domain: tuple[float, float]): 89 | """Test bisector with generated triangular functions""" 90 | low, high = domain 91 | a = peak - width / 2 92 | b = peak 93 | c = peak + width / 2 94 | assume(low <= a < c <= high) 95 | 96 | domain_obj = DummyDomain(low, high) 97 | points = domain_obj.range 98 | step = (high - low) / (len(points) - 1) 99 | 100 | from fuzzylogic import functions 101 | 102 | f = functions.triangular(a, c, c=b) 103 | 104 | result = bisector(f, points, step) 105 | assert math.isclose(result, peak, rel_tol=0.01), f"Expected {peak}, got {result}" 106 | 107 | 108 | # --------------------------------------------------------------------------- 109 | # Edge Cases 110 | # --------------------------------------------------------------------------- 111 | 112 | 113 | @pytest.mark.parametrize("dtype, tol", [(np.float32, 1e-6), (np.float64, 1e-12), (np.longdouble, 1e-15)]) 114 | def test_cog_precision(dtype, tol): 115 | """Test numerical precision across data types""" 116 | domain = DummyDomain(0, 1, 1001) 117 | exact_val = dtype(0.5) 118 | fuzzy_set = DummySet(float(exact_val)) 119 | fuzzy_set.domain = domain 120 | 121 | result = cog([(fuzzy_set, 1.0)]) 122 | assert abs(result - exact_val) < tol 123 | 124 | 125 | # --------------------------------------------------------------------------- 126 | # Performance 127 | # --------------------------------------------------------------------------- 128 | 129 | 130 | def test_cog_linear_scaling(): 131 | """Verify O(n) time complexity""" 132 | sizes = [100, 1000, 10000] 133 | times = [] 134 | 135 | # sourcery skip: no-loop-in-tests 136 | for _ in sizes: 137 | sets = [DummySet(0.5) for _ in range(10)] 138 | weights = [(s, 1.0) for s in sets] 139 | 140 | t = timeit.timeit(lambda: cog(weights), number=10) 141 | times.append(t) 142 | 143 | # Check linear correlation 144 | log_sizes = np.log(sizes) 145 | log_times = np.log(times) 146 | corr = np.corrcoef(log_sizes, log_times)[0, 1] 147 | assert corr > 0.95, f"Unexpected complexity (corr={corr:.2f})" 148 | 149 | 150 | # --------------------------------------------------------------------------- 151 | # Core Functionality 152 | # --------------------------------------------------------------------------- 153 | 154 | 155 | def test_mom_constant_membership(): 156 | """Test MOM with uniform maximum""" 157 | domain = DummyDomain(0, 10) 158 | points = domain.range 159 | result = mom(lambda _: 1.0, points) 160 | expected = (0 + 10) / 2 161 | assert math.isclose(result, expected) 162 | 163 | 164 | def test_som_lom_plateau(): 165 | """Test SOM/LOM with plateaued maximum""" 166 | domain = DummyDomain(0, 10) 167 | points = domain.range 168 | agg_mf = lambda x: 1.0 if 3 <= x <= 7 else 0.0 169 | 170 | assert math.isclose(som(agg_mf, points), 3.0) 171 | assert math.isclose(lom(agg_mf, points), 7.0) 172 | 173 | 174 | def test_get_max_points(): 175 | """Test maximum point detection""" 176 | points = [0, 1, 2, 3, 4] 177 | agg_mf = lambda x: 1.0 if x == 2 else 0.5 178 | assert _get_max_points(agg_mf, points) == [2] 179 | 180 | 181 | if __name__ == "__main__": 182 | pytest.main([__file__, "-v", "--hypothesis-show-statistics"]) 183 | -------------------------------------------------------------------------------- /tests/test_functionality.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functional tests of the fuzzylogic library. 3 | """ 4 | 5 | import unittest 6 | 7 | from numpy import array_equal 8 | from pytest import fixture 9 | 10 | from fuzzylogic.classes import Domain, Set 11 | from fuzzylogic.functions import R, S, bounded_linear 12 | from fuzzylogic.tools import weighted_sum 13 | 14 | 15 | @fixture 16 | def temp() -> Domain: 17 | d = Domain("temperature", -100, 100, res=0.1) # in Celsius 18 | d.cold = S(0, 15) # sic 19 | d.hot = Set(R(10, 30)) # sic 20 | d.warm = ~d.cold & ~d.hot 21 | return d 22 | 23 | 24 | @fixture 25 | def simple() -> Domain: 26 | d = Domain("simple", 0, 10) 27 | d.low = S(0, 1) 28 | d.high = R(8, 10) 29 | return d 30 | 31 | 32 | def test_array(simple: Domain) -> None: 33 | assert array_equal(simple.low.array(), [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 34 | assert array_equal(simple.high.array(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 1.0]) 35 | assert len(simple.low.array()) == 11 # unlike arrays and lists, upper boundary is INCLUDED 36 | 37 | 38 | def test_value(temp: Domain) -> None: 39 | assert temp(6) == {temp.cold: 0.6, temp.hot: 0, temp.warm: 0.4} 40 | 41 | 42 | def test_rating() -> None: 43 | """Tom is surveying restaurants. 44 | He doesn't need fancy logic but rather uses a simple approach 45 | with weights. 46 | He went into a small, dirty bar that served some 47 | really good drink and food that wasn't nicely arranged but still 48 | yummmy. He rates the different factors on a scale from 1 to 10, 49 | uses a bounded_linear function to normalize over [0,1] and 50 | passes both the weights (how much each aspect should weigh in total) 51 | and the domain as parameters into weighted_sum. 52 | However, he can't just use Domain(value) because that would return 53 | a dict of memberships, instead he uses Domain.min(value) which 54 | returns the minimum of all memberships no matter how many sets 55 | there are. He creates a dict of membership values corresponding to 56 | the weights and passes that into the parametrized weighted_sum func 57 | as argument to get the final rating for this restaurant. 58 | """ 59 | R = Domain("rating", 1, 10, res=0.1) 60 | R.norm = bounded_linear(1, 10) 61 | weights = {"beverage": 0.3, "atmosphere": 0.2, "looks": 0.2, "taste": 0.3} 62 | w_func = weighted_sum(weights=weights, target_d=R) 63 | 64 | ratings: dict[str, float] = { 65 | "beverage": R.min(9), 66 | "atmosphere": R.min(5), 67 | "looks": R.min(4), 68 | "taste": R.min(8), 69 | } 70 | assert w_func(ratings) == 6.9 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /tests/test_singleton.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | 5 | import numpy as np 6 | import pytest 7 | from hypothesis import given 8 | from hypothesis import strategies as st 9 | 10 | from fuzzylogic.classes import Domain 11 | from fuzzylogic.functions import singleton 12 | 13 | # --------------------------------------------------------------------------- 14 | # Basic Unit Tests 15 | # --------------------------------------------------------------------------- 16 | 17 | 18 | def test_singleton_membership(): 19 | """Test that a singleton returns 1.0 exactly at its specified location and 0 elsewhere.""" 20 | s = singleton(500) 21 | # Exact hit yields 1.0 22 | assert s(500) == 1.0 23 | # Any other value yields 0.0 24 | assert s(499.999) == 0.0 25 | assert s(500.1) == 0.0 26 | 27 | 28 | def test_singleton_center_of_gravity(): 29 | """Test that the center_of_gravity always returns the singleton’s location.""" 30 | for c in [0, 250, 500, 750, 1000]: 31 | s = singleton(c) 32 | assert s.center_of_gravity() == c, f"Expected COG {c}, got {s.center_of_gravity()}" 33 | 34 | 35 | # --------------------------------------------------------------------------- 36 | # Domain Integration Tests 37 | # --------------------------------------------------------------------------- 38 | 39 | 40 | def test_singleton_with_domain(): 41 | """ 42 | Test that a SingletonSet assigned to a domain yields the expected membership 43 | array, containing a spike at the correct position. 44 | """ 45 | D = Domain("D", 0, 1000) 46 | s = singleton(500) 47 | s.domain = D 48 | 49 | arr = s.array() 50 | points = D.range 51 | 52 | assert 500 in points, "Domain should contain 500 exactly." 53 | idx = points[500] 54 | 55 | np.testing.assert_almost_equal(arr[idx], 1.0) 56 | np.testing.assert_almost_equal(arr.sum(), 1.0) 57 | 58 | 59 | @given(c=st.integers(min_value=0, max_value=1000)) 60 | def test_singleton_property_center(c: int): 61 | """ 62 | Property-based test: For any integer c in [0, 1000], a singleton defined at c 63 | (and assigned to an appropriately discretized Domain) has a center-of-gravity equal to c. 64 | """ 65 | D = Domain("D", 0, 1000) 66 | s = singleton(c) 67 | s.domain = D 68 | assert s.center_of_gravity() == c 69 | 70 | 71 | # --------------------------------------------------------------------------- 72 | # Fuzzy Operation Integration Tests 73 | # --------------------------------------------------------------------------- 74 | 75 | 76 | def test_singleton_union(): 77 | """ 78 | Test that the fuzzy union (OR) of two disjoint singleton sets creates a fuzzy set 79 | containing two spikes – one at each singleton location. 80 | """ 81 | D = Domain("D", 0, 1000) 82 | s1 = singleton(500) 83 | s2 = singleton(600) 84 | s1.domain = D 85 | s2.domain = D 86 | 87 | union_set = s1 | s2 88 | union_set.domain = D 89 | 90 | arr = union_set.array() 91 | points = D.range 92 | 93 | assert 500 in points and 600 in points 94 | idx_500 = points[500] 95 | idx_600 = points[600] 96 | np.testing.assert_almost_equal(arr[idx_500], 1.0) 97 | np.testing.assert_almost_equal(arr[idx_600], 1.0) 98 | 99 | np.testing.assert_almost_equal(arr.sum(), 2.0) 100 | 101 | 102 | # --------------------------------------------------------------------------- 103 | # Differential / Regression Testing with Defuzzification 104 | # --------------------------------------------------------------------------- 105 | 106 | 107 | def test_singleton_defuzzification(): 108 | """ 109 | Test that when a singleton is used in defuzzification (via center_of_gravity), 110 | the exact spike value is returned regardless of discrete sampling issues. 111 | """ 112 | s = singleton(500.1) 113 | assert math.isclose(s.center_of_gravity(), 500.1, rel_tol=1e-9) 114 | 115 | 116 | # --------------------------------------------------------------------------- 117 | # Performance 118 | # --------------------------------------------------------------------------- 119 | 120 | 121 | def test_singleton_performance(): 122 | """ 123 | A basic performance test to ensure that evaluating a singleton over a large domain 124 | remains efficient. 125 | """ 126 | D = Domain("D", 0, 1000, res=0.0001) # A large domain 127 | D.s = singleton(500) 128 | time_taken = pytest.importorskip("timeit").timeit(lambda: D.s.array(), number=10) 129 | assert time_taken < 1, "Performance slowed down unexpectedly." 130 | 131 | 132 | if __name__ == "__main__": 133 | pytest.main([__file__, "-v"]) 134 | -------------------------------------------------------------------------------- /tests/test_units.py: -------------------------------------------------------------------------------- 1 | from math import isclose 2 | from typing import cast 3 | from unittest import TestCase 4 | 5 | import numpy as np 6 | from hypothesis import HealthCheck, assume, given, settings 7 | from hypothesis import strategies as st 8 | 9 | from fuzzylogic import combinators as combi 10 | from fuzzylogic import functions as fun 11 | from fuzzylogic import hedges, truth 12 | from fuzzylogic import tools as ru 13 | from fuzzylogic.classes import Domain, Set 14 | 15 | version = (0, 1, 1, 4) 16 | 17 | # Common settings for all tests 18 | common_settings = settings(deadline=None, suppress_health_check=cast(list[HealthCheck], list(HealthCheck))) 19 | 20 | 21 | class TestFunctions(TestCase): 22 | @common_settings 23 | @given(st.floats(allow_nan=False)) 24 | def test_noop(self, x: float) -> None: 25 | f = fun.noop() 26 | assert f(x) == x 27 | 28 | @common_settings 29 | @given(st.floats(allow_nan=False, allow_infinity=False)) 30 | def test_inv(self, x: float) -> None: 31 | assume(0 <= x <= 1) 32 | f = fun.inv(fun.noop()) 33 | assert isclose(f(f(x)), x, abs_tol=1e-16) 34 | 35 | @common_settings 36 | @given(st.floats(allow_nan=False, allow_infinity=False), st.floats(allow_nan=False, allow_infinity=False)) 37 | def test_constant(self, x: float, c: float) -> None: 38 | f = fun.constant(c) 39 | assert f(x) == c 40 | 41 | @common_settings 42 | @given( 43 | st.floats(allow_nan=False), st.floats(min_value=0, max_value=1), st.floats(min_value=0, max_value=1) 44 | ) 45 | def test_alpha(self, x: float, lower: float, upper: float) -> None: 46 | assume(lower < upper) 47 | f = fun.alpha(floor=lower, ceiling=upper, func=fun.noop()) 48 | if x <= lower: 49 | assert f(x) == lower 50 | elif x >= upper: 51 | assert f(x) == upper 52 | else: 53 | assert f(x) == x 54 | 55 | @common_settings 56 | @given( 57 | st.floats(allow_nan=False), 58 | st.floats(min_value=0, max_value=1), 59 | st.floats(min_value=0, max_value=1), 60 | st.floats(min_value=0, max_value=1) | st.none(), 61 | st.floats(min_value=0, max_value=1) | st.none(), 62 | ) 63 | def test_alpha_2( 64 | self, x: float, floor: float, ceil: float, floor_clip: float | None, ceil_clip: float | None 65 | ) -> None: 66 | assume(floor < ceil) 67 | if floor_clip is not None and ceil_clip is not None: 68 | assume(floor_clip < ceil_clip) 69 | f = fun.alpha( 70 | floor=floor, ceiling=ceil, func=fun.noop(), floor_clip=floor_clip, ceiling_clip=ceil_clip 71 | ) 72 | assert 0 <= f(x) <= 1 73 | 74 | @common_settings 75 | @given(st.floats(allow_nan=False), st.floats(min_value=0, max_value=1)) 76 | def test_normalize(self, x: float, height: float) -> None: 77 | assume(height > 0) 78 | f = fun.normalize(height, fun.alpha(ceiling=height, func=fun.R(0, 100))) 79 | assert 0 <= f(x) <= 1 80 | 81 | @common_settings 82 | @given(st.floats(min_value=0, max_value=1)) 83 | def test_moderate(self, x: float) -> None: 84 | f = fun.moderate(fun.noop()) 85 | assert 0 <= f(x) <= 1 86 | 87 | @common_settings 88 | @given(st.floats(), st.floats(), st.floats(min_value=0, max_value=1), st.floats(min_value=0, max_value=1)) 89 | def test_singleton(self, x: float, c: float, no_m: float, c_m: float) -> None: 90 | assume(0 <= no_m < c_m <= 1) 91 | f = fun.singleton(c, no_m=no_m, c_m=c_m) 92 | assert f(x) == (c_m if x == c else no_m) 93 | 94 | @common_settings 95 | @given( 96 | st.floats(allow_nan=False, allow_infinity=False), 97 | st.floats(allow_nan=False, allow_infinity=False), 98 | st.floats(allow_nan=False, allow_infinity=False), 99 | ) 100 | def test_linear(self, x: float, m: float, b: float) -> None: 101 | f = fun.linear(m, b) 102 | assert 0 <= f(x) <= 1 103 | 104 | @common_settings 105 | @given( 106 | st.floats(allow_nan=False), 107 | st.floats(allow_nan=False, allow_infinity=False), 108 | st.floats(allow_nan=False, allow_infinity=False), 109 | st.floats(min_value=0, max_value=1), 110 | st.floats(min_value=0, max_value=1), 111 | ) 112 | def test_bounded_linear(self, x: float, low: float, high: float, c_m: float, no_m: float) -> None: 113 | assume(low < high) 114 | assume(c_m > no_m) 115 | f = fun.bounded_linear(low, high, c_m=c_m, no_m=no_m) 116 | assert 0 <= f(x) <= 1 117 | 118 | @common_settings 119 | @given( 120 | st.floats(allow_nan=False, allow_infinity=False), 121 | st.floats(allow_nan=False, allow_infinity=False), 122 | st.floats(allow_nan=False, allow_infinity=False), 123 | ) 124 | def test_R(self, x: float, low: float, high: float) -> None: 125 | assume(low < high) 126 | f = fun.R(low, high) 127 | assert 0 <= f(x) <= 1 128 | 129 | @common_settings 130 | @given( 131 | st.floats(allow_nan=False, allow_infinity=False), 132 | st.floats(allow_nan=False, allow_infinity=False), 133 | st.floats(allow_nan=False, allow_infinity=False), 134 | ) 135 | def test_S(self, x: float, low: float, high: float) -> None: 136 | assume(low < high) 137 | f = fun.S(low, high) 138 | assert 0 <= f(x) <= 1 139 | 140 | @common_settings 141 | @given( 142 | st.floats(allow_nan=False), 143 | st.floats(allow_nan=False, allow_infinity=False), 144 | st.floats(allow_nan=False, allow_infinity=False), 145 | st.floats(min_value=0, max_value=1), 146 | st.floats(min_value=0, max_value=1), 147 | ) 148 | def test_rectangular(self, x: float, low: float, high: float, c_m: float, no_m: float) -> None: 149 | assume(low < high) 150 | f = fun.rectangular(low, high, c_m=c_m, no_m=no_m) 151 | assert 0 <= f(x) <= 1 152 | 153 | @common_settings 154 | @given( 155 | st.floats(allow_nan=False), 156 | st.floats(allow_nan=False, allow_infinity=False), 157 | st.floats(allow_nan=False, allow_infinity=False), 158 | st.floats(allow_nan=False, allow_infinity=False), 159 | st.floats(min_value=0, max_value=1), 160 | st.floats(min_value=0, max_value=1), 161 | ) 162 | def test_triangular(self, x: float, low: float, high: float, c: float, c_m: float, no_m: float) -> None: 163 | assume(low < c < high) 164 | assume(no_m < c_m) 165 | f = fun.triangular(low, high, c=c, c_m=c_m, no_m=no_m) 166 | assert 0 <= f(x) <= 1 167 | 168 | @common_settings 169 | @given( 170 | st.floats(allow_nan=False), 171 | st.floats(allow_nan=False, allow_infinity=False), 172 | st.floats(allow_nan=False, allow_infinity=False), 173 | st.floats(allow_nan=False, allow_infinity=False), 174 | st.floats(allow_nan=False, allow_infinity=False), 175 | st.floats(min_value=0, max_value=1), 176 | st.floats(min_value=0, max_value=1), 177 | ) 178 | def test_trapezoid( 179 | self, x: float, low: float, c_low: float, c_high: float, high: float, c_m: float, no_m: float 180 | ) -> None: 181 | assume(low < c_low <= c_high < high) 182 | assume(no_m < c_m) 183 | f = fun.trapezoid(low, c_low, c_high, high, c_m=c_m, no_m=no_m) 184 | assert 0 <= f(x) <= 1 185 | 186 | @common_settings 187 | @given( 188 | st.floats(allow_nan=False), 189 | st.floats(min_value=0, max_value=1), 190 | st.floats(allow_nan=False, allow_infinity=False), 191 | st.floats(min_value=0, max_value=1), 192 | ) 193 | def test_sigmoid(self, x: float, L: float, k: float, x0: float) -> None: 194 | assume(0 < L <= 1) 195 | f = fun.sigmoid(L, k, x0) 196 | assert 0 <= f(x) <= 1 197 | 198 | @common_settings 199 | @given( 200 | st.floats(allow_nan=False), 201 | st.floats(allow_nan=False, allow_infinity=False), 202 | st.floats(allow_nan=False, allow_infinity=False), 203 | ) 204 | def test_bounded_sigmoid(self, x: float, low: float, high: float) -> None: 205 | assume(low < high) 206 | f = fun.bounded_sigmoid(low, high) 207 | assert 0 <= f(x) <= 1 208 | 209 | @common_settings 210 | @given(st.floats(allow_nan=False), st.floats(allow_nan=False, allow_infinity=False)) 211 | def test_simple_sigmoid(self, x: float, k: float) -> None: 212 | f = fun.simple_sigmoid(k) 213 | assert 0 <= f(x) <= 1 214 | 215 | @common_settings 216 | @given( 217 | st.floats(allow_nan=False), 218 | st.floats(allow_nan=False, allow_infinity=False), 219 | st.floats(allow_nan=False, allow_infinity=False), 220 | st.floats(allow_nan=False, allow_infinity=False), 221 | ) 222 | def test_triangular_sigmoid(self, x: float, low: float, high: float, c: float) -> None: 223 | assume(low < c < high) 224 | f = fun.triangular(low, high, c=c) 225 | assert 0 <= f(x) <= 1 226 | 227 | @common_settings 228 | @given( 229 | st.floats(allow_nan=False), 230 | st.floats(allow_nan=False, allow_infinity=False), 231 | st.floats(allow_nan=False, allow_infinity=False), 232 | st.floats(min_value=0, max_value=1), 233 | ) 234 | def test_gauss(self, x: float, b: float, c: float, c_m: float) -> None: 235 | assume(b > 0) 236 | assume(c_m > 0) 237 | f = fun.gauss(c, b, c_m=c_m) 238 | assert 0 <= f(x) <= 1 239 | 240 | @common_settings 241 | @given( 242 | st.floats(allow_nan=False, min_value=0, allow_infinity=False), 243 | st.floats(allow_nan=False, min_value=0, allow_infinity=False), 244 | st.floats(allow_nan=False, min_value=0), 245 | ) 246 | def test_bounded_exponential(self, k: float, limit: float, x: float) -> None: 247 | assume(k != 0) 248 | assume(limit != 0) 249 | f = fun.bounded_exponential(k, limit) 250 | assert 0 <= f(x) <= limit 251 | 252 | 253 | class TestHedges(TestCase): 254 | @common_settings 255 | @given(st.floats(min_value=0, max_value=1)) 256 | def test_very(self, x: float) -> None: 257 | s = Set(fun.noop()) 258 | f = hedges.very(s) 259 | assert 0 <= f(x) <= 1 260 | 261 | @common_settings 262 | @given(st.floats(min_value=0, max_value=1)) 263 | def test_minus(self, x: float) -> None: 264 | s = Set(fun.noop()) 265 | f = hedges.minus(s) 266 | assert 0 <= f(x) <= 1 267 | 268 | @common_settings 269 | @given(st.floats(min_value=0, max_value=1)) 270 | def test_plus(self, x: float) -> None: 271 | s = Set(fun.noop()) 272 | f = hedges.plus(s) 273 | assert 0 <= f(x) <= 1 274 | 275 | 276 | class TestCombinators(TestCase): 277 | @common_settings 278 | @given(st.floats(min_value=0, max_value=1)) 279 | def test_MIN(self, x: float) -> None: 280 | a = fun.noop() 281 | b = fun.noop() 282 | f = combi.MIN(a, b) 283 | assert 0 <= f(x) <= 1 284 | 285 | @common_settings 286 | @given(st.floats(min_value=0, max_value=1)) 287 | def test_MAX(self, x: float) -> None: 288 | a = fun.noop() 289 | b = fun.noop() 290 | f = combi.MAX(a, b) 291 | assert 0 <= f(x) <= 1 292 | 293 | @common_settings 294 | @given(st.floats(min_value=0, max_value=1)) 295 | def test_product(self, x: float) -> None: 296 | a = fun.noop() 297 | b = fun.noop() 298 | f = combi.product(a, b) 299 | assert 0 <= f(x) <= 1 300 | 301 | @common_settings 302 | @given(st.floats(min_value=0, max_value=1)) 303 | def test_bounded_sum(self, x: float) -> None: 304 | a = fun.noop() 305 | b = fun.noop() 306 | f = combi.bounded_sum(a, b) 307 | assert 0 <= f(x) <= 1 308 | 309 | @common_settings 310 | @given(st.floats(min_value=0, max_value=1)) 311 | def test_lukasiewicz_AND(self, x: float) -> None: 312 | a = fun.noop() 313 | b = fun.noop() 314 | f = combi.lukasiewicz_AND(a, b) 315 | assert 0 <= f(x) <= 1 316 | 317 | @common_settings 318 | @given(st.floats(min_value=0, max_value=1)) 319 | def test_lukasiewicz_OR(self, x: float) -> None: 320 | a = fun.noop() 321 | b = fun.noop() 322 | f = combi.lukasiewicz_OR(a, b) 323 | assert 0 <= f(x) <= 1 324 | 325 | @common_settings 326 | @given(st.floats(min_value=0, max_value=1)) 327 | def test_einstein_product(self, x: float) -> None: 328 | a = fun.noop() 329 | b = fun.noop() 330 | f = combi.einstein_product(a, b) 331 | assert 0 <= f(x) <= 1 332 | 333 | @common_settings 334 | @given(st.floats(min_value=0, max_value=1)) 335 | def test_einstein_sum(self, x: float) -> None: 336 | a = fun.noop() 337 | b = fun.noop() 338 | f = combi.einstein_sum(a, b) 339 | assert 0 <= f(x) <= 1 340 | 341 | @common_settings 342 | @given(st.floats(min_value=0, max_value=1)) 343 | def test_hamacher_product(self, x: float) -> None: 344 | a = fun.noop() 345 | b = fun.noop() 346 | f = combi.hamacher_product(a, b) 347 | assert 0 <= f(x) <= 1 348 | 349 | @common_settings 350 | @given(st.floats(min_value=0, max_value=1)) 351 | def test_hamacher_sum(self, x: float) -> None: 352 | a = fun.noop() 353 | b = fun.noop() 354 | f = combi.hamacher_sum(a, b) 355 | assert 0 <= f(x) <= 1 356 | 357 | @common_settings 358 | @given(st.floats(min_value=0, max_value=1), st.floats(min_value=0, max_value=1)) 359 | def test_lambda_op(self, x: float, lambda_: float) -> None: 360 | a = fun.noop() 361 | b = fun.noop() 362 | g = combi.lambda_op(lambda_) 363 | f = g(a, b) 364 | assert 0 <= f(x) <= 1 365 | 366 | @common_settings 367 | @given(st.floats(min_value=0, max_value=1), st.floats(min_value=0, max_value=1)) 368 | def test_gamma_op(self, x: float, g: float) -> None: 369 | a = fun.noop() 370 | b = fun.noop() 371 | G = combi.gamma_op(g) 372 | f = G(a, b) 373 | assert 0 <= f(x) <= 1 374 | 375 | @common_settings 376 | @given(st.floats(min_value=0, max_value=1)) 377 | def test_simple_disjoint_sum(self, x: float) -> None: 378 | a = fun.noop() 379 | b = fun.noop() 380 | f = combi.simple_disjoint_sum(a, b) 381 | assert 0 <= f(x) <= 1 382 | 383 | 384 | class TestDomain(TestCase): 385 | def test_basics(self) -> None: 386 | D = Domain("d", 0, 10) 387 | assert D._name == "d" # type: ignore 388 | assert D._low == 0 # type: ignore 389 | assert D._high == 10 # type: ignore 390 | assert D._res == 1 # type: ignore 391 | x = Set(lambda x: 1) 392 | D.s = x 393 | assert D.s == x 394 | assert D._sets == {"s": x} # type: ignore 395 | R = D(3) 396 | assert R == {D.s: 1} 397 | # repr is hard - need to repr sets first :/ 398 | # D = eval(repr(d)) 399 | # assert d == D 400 | 401 | 402 | class TestSet(TestCase): 403 | @common_settings 404 | @given( 405 | st.floats(allow_nan=False, allow_infinity=False), 406 | st.floats(allow_nan=False, allow_infinity=False), 407 | st.floats(min_value=0.0001, max_value=1), 408 | ) 409 | def test_eq(self, low: float, high: float, res: float) -> None: 410 | """This also tests Set.array(). 411 | This test can massively slow down hypothesis with even 412 | reasonably large/small values. 413 | """ 414 | assume(low < high) 415 | # to avoid MemoryError and runs that take forever.. 416 | assume(high - low <= 10) 417 | D1 = Domain("a", low, high, res=res) 418 | D1.s1 = fun.bounded_linear(low, high) 419 | D2 = Domain("b", low, high, res=res) 420 | D2.s2 = Set(fun.bounded_linear(low, high)) 421 | assert D1.s1 == D2.s2 422 | 423 | def test_normalized(self) -> None: 424 | D = Domain("d", 0, 10, res=0.1) 425 | D.s = Set(fun.bounded_linear(3, 12)) 426 | D.x = D.s.normalized() 427 | D.y = D.x.normalized() 428 | assert D.x == D.y 429 | 430 | def test_sub_super_set(self) -> None: 431 | D = Domain("d", 0, 10, res=0.1) 432 | D.s = Set(fun.bounded_linear(3, 12)) 433 | D.x = D.s.normalized() 434 | assert D.x >= D.s 435 | assert D.s <= D.x 436 | 437 | def test_complement(self) -> None: 438 | D = Domain("d", 0, 10, res=0.1) 439 | D.s1 = Set(fun.bounded_linear(3, 12)) 440 | D.s2 = ~~D.s1 441 | assert all(np.isclose(D.s1.array(), D.s2.array())) 442 | 443 | 444 | class TestRules(TestCase): 445 | @common_settings 446 | @given( 447 | st.floats(min_value=0, max_value=1), 448 | st.floats(allow_infinity=False, allow_nan=False), 449 | st.floats(allow_infinity=False, allow_nan=False), 450 | st.floats(min_value=0, max_value=1), 451 | st.floats(min_value=0, max_value=1), 452 | ) 453 | def test_rescale(self, x: float, out_min: float, out_max: float, in_min: float, in_max: float) -> None: 454 | assume(in_min < in_max) 455 | assume(in_min <= x <= in_max) 456 | assume(out_min < out_max) 457 | f = ru.rescale(out_min, out_max) 458 | assert out_min <= f(x) <= out_max 459 | 460 | @given(st.floats(allow_nan=False), st.floats(allow_nan=False)) 461 | def round_partial(self, x: float, res: float) -> None: 462 | assert isclose(x, ru.round_partial(x, res)) 463 | 464 | 465 | class TestTruth(TestCase): 466 | @common_settings 467 | @given(st.floats(min_value=0, max_value=1)) 468 | def test_true_and_false(self, m: float) -> None: 469 | assert truth.true(m) + truth.false(m) == 1 470 | 471 | @common_settings 472 | @given(st.floats(min_value=0, max_value=1)) 473 | def test_very_false_and_fairly_true(self, m: float) -> None: 474 | assert truth.very_false(m) + truth.fairly_true(m) == 0 475 | 476 | @common_settings 477 | @given(st.floats(min_value=0, max_value=1)) 478 | def test_fairly_false_and_very_true(self, m: float) -> None: 479 | assert truth.fairly_false(m) + truth.very_true(m) == 0 480 | -------------------------------------------------------------------------------- /tests/thebariumoxide_test.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | 3 | from fuzzylogic.classes import Domain, Rule 4 | from fuzzylogic.functions import R, S, trapezoid, triangular 5 | 6 | left = Domain("left_obstacle_sensor", 0, 100, res=0.1) 7 | right = Domain("right_obstacle_sensor", 0, 100, res=0.1) 8 | theta = Domain("θ", -50.5, 50.5, res=0.1) 9 | 10 | left.veryStrong = S(0, 25) 11 | left.strong = trapezoid(0, 25, 25, 50) 12 | left.medium = trapezoid(25, 50, 50, 75) 13 | left.weak = trapezoid(50, 75, 75, 100) 14 | left.veryWeak = R(75, 100) 15 | 16 | right.veryStrong = S(0, 25) 17 | right.strong = trapezoid(0, 25, 25, 50) 18 | right.medium = trapezoid(25, 50, 50, 75) 19 | right.weak = trapezoid(50, 75, 75, 100) 20 | right.veryWeak = R(75, 100) 21 | 22 | theta.mediumRight = triangular(-50.5, -49.5) 23 | theta.smallRight = triangular(-25.5, -24.5) 24 | theta.noTurn = triangular(-0.5, 0.5) 25 | theta.smallLeft = triangular(24.5, 25.5) 26 | theta.mediumLeft = triangular(49.5, 50.5) 27 | 28 | rules = Rule({ 29 | (left.veryStrong, right.veryStrong): theta.noTurn, 30 | (left.veryStrong, right.strong): theta.smallRight, 31 | (left.veryStrong, right.medium): theta.smallRight, 32 | (left.veryStrong, right.weak): theta.mediumRight, 33 | (left.veryStrong, right.veryWeak): theta.mediumRight, 34 | (left.strong, right.veryStrong): theta.smallLeft, 35 | (left.strong, right.strong): theta.noTurn, 36 | (left.strong, right.veryWeak): theta.mediumRight, 37 | (left.strong, right.weak): theta.mediumRight, 38 | (left.strong, right.medium): theta.smallRight, 39 | (left.medium, right.veryStrong): theta.smallLeft, 40 | (left.medium, right.strong): theta.smallLeft, 41 | (left.medium, right.medium): theta.noTurn, 42 | (left.medium, right.weak): theta.smallRight, 43 | (left.medium, right.veryWeak): theta.smallRight, 44 | (left.weak, right.veryStrong): theta.mediumLeft, 45 | (left.weak, right.strong): theta.mediumLeft, 46 | (left.weak, right.medium): theta.smallLeft, 47 | (left.weak, right.weak): theta.noTurn, 48 | (left.weak, right.veryWeak): theta.smallRight, 49 | (left.veryWeak, right.veryStrong): theta.mediumLeft, 50 | (left.veryWeak, right.strong): theta.mediumLeft, 51 | (left.veryWeak, right.medium): theta.smallLeft, 52 | (left.veryWeak, right.weak): theta.smallLeft, 53 | (left.veryWeak, right.veryWeak): theta.noTurn, 54 | }) 55 | 56 | 57 | def fuzzyObjectAvoidance(leftDist, rightDist): 58 | values = {left: leftDist, right: rightDist} 59 | return rules(values) 60 | 61 | 62 | def main(): 63 | theta.mediumRight.plot() 64 | theta.smallRight.plot() 65 | theta.noTurn.plot() 66 | theta.smallLeft.plot() 67 | theta.mediumLeft.plot() 68 | 69 | plt.show() 70 | 71 | print(fuzzyObjectAvoidance(100, 100)) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | --------------------------------------------------------------------------------