├── .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 | [](https://github.com/amogorkon/fuzzylogic/blob/master/LICENSE)
4 | [](https://github.com/amogorkon/fuzzylogic/stargazers)
5 | [](https://github.com/amogorkon/fuzzylogic/network/members)
6 | [](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 | 
39 |
40 |
41 |
42 |
43 | ```python
44 | T.down = S(20, 29)
45 | T.down.plot()
46 | ```
47 |
48 |
49 |
50 | 
51 |
52 |
53 |
54 |
55 | ```python
56 | T.polygon = T.up & T.down
57 | T.polygon.plot()
58 | ```
59 |
60 |
61 |
62 | 
63 |
64 |
65 |
66 |
67 | ```python
68 | T.inv_polygon = ~T.polygon
69 | T.inv_polygon.plot()
70 | ```
71 |
72 |
73 |
74 | 
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 | 
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 | 
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 | 
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 | 
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 | [](https://github.com/amogorkon/fuzzylogic/blob/master/LICENSE)
4 | [](https://github.com/amogorkon/fuzzylogic/stargazers)
5 | [](https://github.com/amogorkon/fuzzylogic/network/members)
6 | [](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 |
--------------------------------------------------------------------------------