Skip to content

Commit d13876e

Browse files
committed
release build
1 parent b429a2e commit d13876e

File tree

14 files changed

+1295
-0
lines changed

14 files changed

+1295
-0
lines changed

build/lib/fuzzylogic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""File to indicate that this is a package."""

build/lib/fuzzylogic/classes.py

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
2+
"""
3+
Domain and Set classes for fuzzy logic.
4+
5+
Primary abstractions for recursive functions for better handling.
6+
"""
7+
8+
import matplotlib.pyplot as plt
9+
from numpy import arange, fromiter, array_equal, less_equal, greater_equal, less, greater
10+
import numpy as np
11+
from logging import warn
12+
import pickle
13+
14+
from .functions import inv, normalize
15+
from .combinators import MAX, MIN, product, bounded_sum, simple_disjoint_sum
16+
17+
class FuzzyWarning(UserWarning):
18+
"""Extra Exception so that user code can filter exceptions specific to this lib."""
19+
pass
20+
21+
22+
class Domain:
23+
"""
24+
A domain is a 'measurable' dimension of 'real' values like temperature.
25+
26+
There must be a lower and upper limit and a resolution (the size of steps)
27+
specified.
28+
29+
Fuzzysets are defined within one such domain and are only meaningful
30+
while considered within their domain ('apples and bananas').
31+
To operate with sets across domains, there needs to be a mapping.
32+
33+
The sets are accessed as attributes of the domain like
34+
>>> temp = Domain('temperature', 0, 100)
35+
>>> temp.hot = Set(lambda x: 0)
36+
>>> temp.hot(5)
37+
0
38+
39+
DO NOT call a derived set without assignment first as it WILL
40+
confuse the recursion and seriously mess up.
41+
NOT: ~temp.hot(2) or ~(temp.hot.)(2)
42+
BUT:
43+
>>> not_hot = ~temp.hot
44+
>>> not_hot(2)
45+
1
46+
47+
You MUST NOT add arbitrary attributes to an *instance* of Domain - you can
48+
however subclass or modify the class itself. If you REALLY have to add attributes,
49+
make sure to "whitelist" it in __slots__ first.
50+
51+
Use the Domain by calling it with the value in question. This returns a
52+
dictionary with the degrees of membership per set. You MAY override __call__
53+
in a subclass to enable concurrent evaluation for performance improvement.
54+
>>> temp.cold = not_hot
55+
>>> temp(3) == {"hot": 0, "cold": 1}
56+
True
57+
"""
58+
59+
__slots__ = ['_name', '_low', '_high', '_res', '_sets']
60+
61+
def __init__(self, name, low, high, *, res=1, sets:dict=None):
62+
"""Define a domain."""
63+
assert low < high, "higher bound must be greater than lower."
64+
assert res > 0, "resolution can't be negative or zero"
65+
self._name = name
66+
self._high = high
67+
self._low = low
68+
self._res = res
69+
self._sets = {} if sets is None else sets # Name: Set(Function())
70+
71+
72+
def __call__(self, x):
73+
"""Pass a value to all sets of the domain and return a dict with results."""
74+
if not(self._low <= x <= self._high):
75+
warn(f"{x} is outside of domain!")
76+
memberships = {name: s.func(x) for name, s in self._sets.items()}
77+
return memberships
78+
79+
def __str__(self):
80+
"""Return a string to print()."""
81+
return self._name
82+
83+
def __repr__(self):
84+
"""Return a string so that eval(repr(Domain)) == Domain."""
85+
return f"Domain('{self._name}', {self._low}, {self._high}, res={self._res}, sets={self._sets})"
86+
87+
def __eq__(self, other):
88+
"""Test equality of two domains."""
89+
return all([self._name == other._name,
90+
self._low == other._low,
91+
self._high == other._high,
92+
self._res == other._res,
93+
self._sets == other._sets])
94+
95+
def __getattr__(self, name):
96+
"""Get the value of an attribute. Is called after __getattribute__ is called with an AttributeError."""
97+
if name in self._sets:
98+
return self._sets[name]
99+
else:
100+
raise AttributeError(f"{name} is not a set or attribute")
101+
102+
def __setattr__(self, name, value):
103+
"""Define a set within a domain or assign a value to a domain attribute."""
104+
# It's a domain attr
105+
if name in self.__slots__:
106+
object.__setattr__(self, name, value)
107+
# We've got a fuzzyset
108+
else:
109+
assert str.isidentifier(name), f"{name} must be an identifier."
110+
if not isinstance(value, Set):
111+
# Often useful to just assign a function for simple sets..
112+
value = Set(value)
113+
# However, we need the abstraction if we want to use Superfuzzysets (derived sets).
114+
self._sets[name] = value
115+
value.domain = self
116+
value.name = name
117+
118+
def __delattr__(self, name):
119+
"""Delete a fuzzy set from the domain."""
120+
if name in self._sets:
121+
del self._sets[name]
122+
else:
123+
raise FuzzyWarning("Trying to delete a regular attr, this needs extra care.")
124+
125+
def range(self):
126+
"""Return an arange object with the domain's specifics.
127+
128+
This is used to conveniently iterate over all possible values
129+
for plotting etc.
130+
131+
High upper bound is INCLUDED unlike range.
132+
"""
133+
return arange(self._low, self._high + self._res, self._res)
134+
135+
def min(self, x):
136+
"""Standard way to get the min over all membership funcs.
137+
138+
It's not just more convenient but also faster than
139+
to calculate all results, construct a dict, unpack the dict
140+
and calculate the min from that.
141+
"""
142+
return min(f(x) for f in self._sets.values())
143+
144+
def max(self, x):
145+
"""Standard way to get the max over all membership funcs."""
146+
return max(f(x) for f in self._sets.values())
147+
148+
class Set:
149+
"""
150+
A fuzzyset defines a 'region' within a domain.
151+
152+
The associated membership function defines 'how much' a given value is
153+
inside this region - how 'true' the value is.
154+
155+
Sets and functions MUST NOT be mixed because functions don't have
156+
the methods of the sets needed for the logic.
157+
158+
Sets that are returned from one of the operations are 'derived sets' or
159+
'Superfuzzysets' according to Zadeh.
160+
161+
Note that most checks are merely assertions that can be optimized away.
162+
DO NOT RELY on these checks and use tests to make sure that only valid calls are made.
163+
164+
This class uses the classical MIN/MAX operators for AND/OR. To use different operators, simply subclass and
165+
replace the __and__ and __or__ functions. However, be careful not to mix the classes logically,
166+
since it might be confusing which operator will be used (left/right binding).
167+
168+
"""
169+
name = None # these are set on assignment to the domain! DO NOT MODIFY
170+
domain = None
171+
172+
173+
def __init__(self, func:callable, *, name=None, domain=None):
174+
"""Initialize the set."""
175+
assert callable(func) or isinstance(func, str)
176+
# if func is a str, we've got a pickled function via repr
177+
if isinstance(func, str):
178+
try:
179+
func = pickle.loads(func)
180+
except:
181+
FuzzyWarning("Can't load pickled function %s" % func)
182+
self.func = func
183+
184+
if name is not None and domain is not None:
185+
setattr(domain, name, self)
186+
187+
if bool(name)^bool(domain):
188+
raise FuzzyWarning("Name or domain is provided, but not both!")
189+
190+
def __call__(self, x):
191+
"""Call the function of the set (which can be of any arbitrary complexity)."""
192+
return self.func(x)
193+
194+
def __invert__(self):
195+
"""Return a new set with modified function."""
196+
return Set(inv(self.func))
197+
198+
def __neg__(self):
199+
return Set(inv(self.func))
200+
201+
def __and__(self, other):
202+
"""Return a new set with modified function."""
203+
return Set(MIN(self.func, other.func))
204+
205+
def __or__(self, other):
206+
"""Return a new set with modified function."""
207+
return Set(MAX(self.func, other.func))
208+
209+
def __mul__(self, other):
210+
"""Return a new set with modified function."""
211+
return Set(product(self.func, other.func))
212+
213+
def __add__(self, other):
214+
"""Return a new set with modified function."""
215+
return Set(bounded_sum(self.func, other.func))
216+
217+
def __xor__(self, other):
218+
"""Return a new set with modified function."""
219+
return Set(simple_disjoint_sum(self.func, other.func))
220+
221+
def __pow__(self, power):
222+
"""Return a new set with modified function."""
223+
#FYI: pow is used with hedges
224+
return Set(lambda x: pow(self.func(x), power))
225+
226+
def __eq__(self, other):
227+
"""A set is equal with another if both return the same values over the same range."""
228+
if self.domain is None or other.domain is None:
229+
# It would require complete AST analysis to check whether both Sets
230+
# represent the same recursive functions -
231+
# additionally, there are infinitely many mathematically equivalent
232+
# functions that don't have the same bytecode...
233+
raise FuzzyWarning("Impossible to determine.")
234+
else:
235+
# however, if domains ARE assigned (whether or not it's the same domain),
236+
# we simply can check if they map to the same values
237+
return array_equal(self.array(), other.array())
238+
239+
def __le__(self, other):
240+
"""If this <= other, it means this is a subset of the other."""
241+
if self.domain is None or other.domain is None:
242+
raise FuzzyWarning("Can't compare without Domains.")
243+
return all(less_equal(self.array(), other.array()))
244+
245+
def __lt__(self, other):
246+
"""If this < other, it means this is a proper subset of the other."""
247+
if self.domain is None or other.domain is None:
248+
raise FuzzyWarning("Can't compare without Domains.")
249+
return all(less(self.array(), other.array()))
250+
251+
def __ge__(self, other):
252+
"""If this >= other, it means this is a superset of the other."""
253+
if self.domain is None or other.domain is None:
254+
raise FuzzyWarning("Can't compare without Domains.")
255+
return all(greater_equal(self.array(), other.array()))
256+
257+
def __gt__(self, other):
258+
"""If this > other, it means this is a proper superset of the other."""
259+
if self.domain is None or other.domain is None:
260+
raise FuzzyWarning("Can't compare without Domains.")
261+
return all(greater(self.array(), other.array()))
262+
263+
def __len__(self):
264+
"""Number of membership values in the set, defined by bounds and resolution of domain."""
265+
if self.domain is None:
266+
raise FuzzyWarning("No domain.")
267+
return len(self.array())
268+
269+
def cardinality(self):
270+
"""The sum of all values in the set."""
271+
if self.domain is None:
272+
raise FuzzyWarning("No domain.")
273+
return sum(self.array())
274+
275+
def relative_cardinality(self):
276+
"""Relative cardinality is the sum of all membership values by number of all values."""
277+
if self.domain is None:
278+
raise FuzzyWarning("No domain.")
279+
if len(self) == 0:
280+
# this is highly unlikely and only possible with res=inf but still..
281+
raise FuzzyWarning("The domain has no element.")
282+
return self.cardinality() / len(self)
283+
284+
def concentrated(self):
285+
"""
286+
Alternative to hedge "very".
287+
288+
Returns an new set that has a reduced amount of values the set includes and to dampen the
289+
membership of many values.
290+
"""
291+
return Set(lambda x: self.func(x) ** 2)
292+
293+
def intensified(self):
294+
"""
295+
Alternative to using hedges.
296+
297+
Returns a new set where the membership of values are increased that
298+
already strongly belong to the set and dampened the rest.
299+
"""
300+
def f(x):
301+
if x < 0.5:
302+
return 2 * self.func(x)**2
303+
else:
304+
return 1 - 2(1 - self.func(x)**2)
305+
return Set(f)
306+
307+
def dilated(self):
308+
"""Expand the set with more values and already included values are enhanced.
309+
"""
310+
return Set(lambda x: self.func(x) ** 1./2.)
311+
312+
def multiplied(self, n):
313+
"""Multiply with a constant factor, changing all membership values."""
314+
return Set(lambda x: self.func(x) * n)
315+
316+
def plot(self):
317+
"""Graph the set in the given domain."""
318+
if self.domain is None:
319+
raise FuzzyWarning("No domain assigned, cannot plot.")
320+
R = self.domain.range()
321+
V = [self.func(x) for x in R]
322+
plt.plot(R, V)
323+
324+
def array(self):
325+
"""Return an array of all values for this set within the given domain."""
326+
if self.domain is None:
327+
raise FuzzyWarning("No domain assigned.")
328+
return fromiter((self.func(x) for x in self.domain.range()), float)
329+
330+
def __repr__(self):
331+
"""
332+
Return a string representation of the Set that reconstructs the set with eval().
333+
334+
*******
335+
Current implementation does NOT work correctly.
336+
337+
This is harder than expected since all functions are (recursive!) closures which
338+
can't simply be pickled. If this functionality really is needed, all functions
339+
would have to be peppered with closure-returning overhead such as
340+
341+
def create_closure_and_function(*args):
342+
func = None
343+
def create_function_closure():
344+
return func
345+
346+
closure = create_function_closure.__closure__
347+
func = types.FunctionType(*args[:-1] + [closure])
348+
return func
349+
"""
350+
return f"Set({self.func})"
351+
352+
def __str__(self):
353+
"""Return a string for print()."""
354+
if self.name is None and self.domain is None:
355+
return f"dangling Set({self.func})"
356+
else:
357+
return f"{self.domain._name}.{self.name}"
358+
359+
def normalized(self):
360+
"""Return a set that is normalized *for this domain* with 1 as max."""
361+
if self.domain is None:
362+
raise FuzzyWarning("Can't normalize without domain.")
363+
return Set(normalize(max(self.array()), self.func))
364+
365+
if __name__ == "__main__":
366+
import doctest
367+
doctest.testmod()

0 commit comments

Comments
 (0)