Skip to content

Commit 1dd044f

Browse files
committed
refactored classes and fixed domains.
1 parent aae5873 commit 1dd044f

File tree

5 files changed

+204
-103
lines changed

5 files changed

+204
-103
lines changed

Showcase.ipynb

Lines changed: 102 additions & 23 deletions
Large diffs are not rendered by default.

fuzzy/classes.py

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11

22
import matplotlib.pyplot as plt
33
from numpy import arange, fromiter
4+
from logging import warn
45

56
from fuzzy.functions import inv
67
from fuzzy.combinators import MAX, MIN, product, bounded_sum
78

9+
class FuzzyWarning(UserWarning):
10+
pass
811

912

1013
class Domain:
@@ -20,137 +23,159 @@ class Domain:
2023
2124
The sets are accessed as attributes of the domain like
2225
>>> temp = Domain('temperature', 0, 100)
23-
>>> temp.hot = Set(temp, lambda x: 0) # functions.constant
26+
>>> temp.hot = Set(lambda x: 0)
2427
>>> temp.hot(5)
2528
0
2629
2730
DO NOT call a derived set without assignment first as it WILL
2831
confuse the recursion and seriously mess up.
32+
NOT: ~temp.hot(2) or ~(temp.hot.)(2) but:
2933
>>> not_hot = ~temp.hot
3034
>>> not_hot(2)
3135
1
3236
3337
You MUST NOT add arbitrary attributes to an *instance* of Domain - you can
34-
however subclass or modify the class itself, which affects its instances:
35-
>>> d = Domain('d', 0, 100)
36-
>>> Domain.x = 78
37-
>>> d.x
38-
78
38+
however subclass or modify the class itself. If you REALLY have to add attributes,
39+
make sure to "whitelist" it in _allowed_attrs first.
3940
4041
Use the Domain by calling it with the value in question. This returns a
4142
dictionary with the degrees of membership per set. You MAY override __call__
4243
in a subclass to enable concurrent evaluation for performance improvement.
43-
>>> temp.cold = ~temp.hot
44+
>>> temp.cold = not_hot
4445
45-
# >>> result = temp(3)
46-
# >>> {'temperature.hot': 0, 'temperature.cold': 1} == result
46+
# >>> temp(3) == {'temperature.hot': 0, 'temperature.cold': 1}
4747
# True
4848
"""
49-
_sets = set()
49+
_allowed_attrs = ['name', 'low', 'high', 'res', '_sets']
5050

5151
def __init__(self, name, low, high, res=1):
52-
if high < low:
53-
raise AttributeError("higher bound must not be less than lower.")
52+
assert low < high, "higher bound must be greater than lower."
5453
self.name = name
5554
self.high = high
5655
self.low = low
5756
self.res = res
57+
# one should not access (especially add things) directly
58+
self._sets = {}
5859

5960

6061
def __call__(self, x):
61-
return NotImplemented
62-
# self._sets isn't properly defined
62+
if not(self.low <= x <= self.high):
63+
warn(f"{x} is outside of domain!")
6364
set_memberships = {}
6465
for setname, s in self._sets.items():
6566
set_memberships["{0}.{1}".format(self.name, setname)] = s(x)
6667
return set_memberships
6768

6869
def __str__(self):
69-
return "Domain(%s)" % self.name
70+
return self.name
7071

7172
def __getattr__(self, name):
7273
if name in self._sets:
7374
return self._sets[name]
7475
else:
75-
raise AttributeError("not a set and not an attribute")
76-
77-
"""
76+
raise AttributeError(f"{name} is not a set or attribute")
77+
7878
def __setattr__(self, name, value):
7979
# it's a domain attr
80-
if name in ['name', 'low', 'high', 'res', '_sets']:
80+
if name in self._allowed_attrs:
8181
object.__setattr__(self, name, value)
82-
# we've got a fuzzyset within this domain and the value is a func
82+
# we've got a fuzzyset
8383
else:
8484
if not isinstance(value, Set):
85-
raise ValueError("only a fuzzy.Set may be assigned.")
85+
raise ValueError(f"({name}) {value} must be a Set")
8686
self._sets[name] = value
8787
value.domain = self
88-
"""
88+
value.name = name
89+
90+
def __delattr__(self, name):
91+
if name in self._sets:
92+
del self._sets[name]
93+
else:
94+
raise FuzzyWarning("Trying to delete a regular attr, this needs extra care.")
8995

9096
class Set:
9197
"""
9298
A fuzzyset defines a 'region' within a domain.
9399
The associated membership function defines 'how much' a given value is
94100
inside this region - how 'true' the value is.
95-
A set is identified by the name and attribute of its domain,
96-
therefor there is no need for an extra identifier.
97101
98102
Sets and functions MUST NOT be mixed because functions don't have
99103
the methods of the sets needed for the logic.
100104
101-
The combination operations SHOULD only be applied with sets of the same
102-
domain as ranges may not overlap and thus give unexpected results,
103-
however it is possible to reuse the set under different names because
104-
it is simply a function that may mean different things under different
105-
domains, yet have the same graph (like 'cold' could use the same function
106-
as 'dry' or 'close' if the ranges are the same.)
107-
108105
Sets that are returned from one of the operations are 'derived sets' or
109106
'Superfuzzysets' according to Zadeh.
110107
111108
Note that most checks are merely assertions that can be optimized away.
112109
DO NOT RELY on these checks and use tests to make sure that only valid calls are made.
113110
"""
114-
def __init__(self, domain:Domain, func:callable, ops:dict=None):
111+
_domain = None
112+
_name = None
113+
114+
def __init__(self, func:callable, *, domain=None, name=None):
115+
assert callable(func)
115116
self.func = func
116-
assert self.func is not None
117-
self.ops = ops
118-
assert isinstance(domain, Domain), "Must be used with a Domain."
117+
# either both must be given or none
118+
assert (domain is None) == (name is None)
119119
self.domain = domain
120-
self.domain._sets.add(self)
120+
self.name = name
121+
122+
def name_():
123+
"""Name of the fuzzy set."""
124+
def fget(self):
125+
return self._name
126+
def fset(self, value):
127+
if self._name is None:
128+
self._name = value
129+
else:
130+
raise FuzzyWarning("Can't change name once assigned.")
131+
return locals()
132+
name = property(**name_())
133+
del name_
134+
135+
def domain_():
136+
"""Domain of the fuzzy set."""
137+
def fget(self):
138+
return self._domain
139+
def fset(self, value):
140+
if self._domain is None:
141+
self._domain = value
142+
else:
143+
# maybe could be solved by copy()?
144+
raise FuzzyWarning("Can't change domain once assigned.")
145+
return locals()
146+
domain = property(**domain_())
147+
del domain_
121148

122149
def __call__(self, x):
123-
assert self.domain.low <= x <= self.domain.high, "value outside domain"
124150
return self.func(x)
125151

126152
def __invert__(self):
127-
return Set(self.domain, inv(self.func))
153+
return Set(inv(self.func))
128154

129155
def __and__(self, other):
130-
assert self.domain is other.domain, "Cannot combine sets of different domains."
131-
return Set(self.domain, MIN(self.func, other.func))
156+
return Set(MIN(self.func, other.func))
132157

133158
def __or__(self, other):
134-
assert self.domain is other.domain, "Cannot combine sets of different domains."
135-
return Set(self.domain, MAX(self.func, other.func))
159+
return Set(MAX(self.func, other.func))
136160

137161
def __mul__(self, other):
138-
assert self.domain is other.domain, "Cannot combine sets of different domains."
139-
return Set(self.domain, product(self.func, other.func))
162+
return Set(product(self.func, other.func))
140163

141164
def __add__(self, other):
142-
assert self.domain is other.domain, "Cannot combine sets of different domains."
143-
return Set(self.domain, bounded_sum(self.func, other.func))
165+
return Set(bounded_sum(self.func, other.func))
144166

145167
def __pow__(self, power):
146168
"""pow is used with hedges"""
147-
return Set(self.domain, lambda x: pow(self.func(x), power))
169+
return Set(lambda x: pow(self.func(x), power))
148170

149171
def plot(self, low=None, high=None, res=None):
150172
"""Graph the set.
151173
Use the bounds and resolution of the domain to display the set
152174
unless specified otherwise.
153175
"""
176+
if self.domain is None:
177+
raise FuzzyWarning("No domain assigned, cannot plot.")
178+
154179
low = self.domain.low if low is None else low
155180
high = self.domain.high if high is None else high
156181
res = self.domain.res if res is None else res
@@ -159,12 +184,22 @@ def plot(self, low=None, high=None, res=None):
159184
plt.plot(R, V)
160185

161186
def array(self):
162-
R = arange(self.domain.low, self.domain.high, self.domain.res)
163187
# arange may not be ideal for this
188+
if self.domain is None:
189+
raise FuzzyWarning("No domain assigned.")
164190
return fromiter((self.func(x) for x in arange(self.domain.low,
165191
self.domain.high,
166192
self.domain.res)), float)
167193

194+
def __repr__(self):
195+
return f"Set({self.func}, domain={self.domain}, name={self.name})"
196+
197+
def __str__(self):
198+
if self.name is None and self.domain is None:
199+
return f"dangling Set({self.func})"
200+
else:
201+
return f"{self.domain}.{self.name}"
202+
168203

169204
class Rule:
170205
"""
@@ -173,9 +208,9 @@ class Rule:
173208
174209
It works like this:
175210
>>> temp = Domain("temperature", 0, 100)
176-
>>> temp.hot = Set(temp, lambda x: 1)
211+
>>> temp.hot = Set(lambda x: 1)
177212
>>> dist = Domain("distance", 0, 300)
178-
>>> dist.close = Set(dist, lambda x: 0)
213+
>>> dist.close = Set(lambda x: 0)
179214
180215
#>>> r = Rule(min, ["distance.close", "temperature.hot"])
181216
#>>> d1 = temp(32) # {'temperature.hot': 1}

fuzzy/functions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,15 +360,23 @@ def bounded_sigmoid(low, high):
360360
k = -(4. * log(3)) / (low - high)
361361
# yay for limits! .. and for goddamn hidden divisions by zero thanks to floats >:/
362362
try:
363-
o = 9 if isinf(k) else 9 * exp(low * k)
363+
m = exp(low * k)
364+
# possibility of an underflow! m -> 0
365+
o = 9 if isinf(k) else 9 * m
366+
# o can be 0!
364367
except OverflowError:
365368
o = float("inf")
366369

367370
def f(x):
371+
if o == 0 and x == float("-inf"):
372+
return 1/2
368373
try:
374+
# if o == 0 we get nan if x is -inf and k is positive
375+
# then e^(inf*0) = 1
369376
return 0.1 if isinf(k) else 1. / (1. + exp(x * -k) * o)
370377
except OverflowError:
371378
return 0.0
379+
372380
return f
373381

374382

fuzzy/hedges.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,31 @@
44
HEDGES
55
------
66
Lingual hedges modify curves describing truthvalues.
7-
These work with sets only. Meta-functionality is deprecated.
7+
These work with sets only. It's more trouble than it is worth making
8+
these work with pure functions, so meta-functionality was removed.
89
"""
910

1011
from fuzzy.classes import Set
11-
from warnings import warn
1212

1313
def very(g):
14-
def f(x):
15-
return g(x) ** 2
16-
1714
def s_f(g):
1815
def f(x):
1916
return g(x) ** 2
2017
return f
21-
22-
if isinstance(g, Set):
23-
return Set(g.domain, s_f(g.func))
24-
warn("deprecated", DeprecationWarning)
25-
return f
18+
return Set(s_f(g.func))
2619

2720

2821
def plus(g):
29-
def f(x):
30-
return g(x) ** 1.25
31-
3222
def s_f(g):
3323
def f(x):
3424
return g(x) ** 1.25
3525
return f
36-
37-
if isinstance(g, Set):
38-
return Set(g.domain, s_f(g.func))
39-
warn("deprecated", DeprecationWarning)
40-
return f
26+
return Set(s_f(g.func))
4127

4228

4329
def minus(g):
44-
def f(x):
45-
return g(x) ** 0.75
46-
4730
def s_f(g):
4831
def f(x):
4932
return g(x) ** 0.75
50-
return f
51-
52-
if isinstance(g, Set):
53-
return Set(g.domain, s_f(g.func))
54-
warn("deprecated", DeprecationWarning)
55-
return f
33+
return f
34+
return Set(s_f(g.func))

test_fuzzy_functionality.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def setUp(self):
1313
"""
1414
Lucy tells her robot that it is good to heat when it's cold and not when it's hot."""
1515
self.temp = Domain('temperature', -100, 100, 1) # in Celsius
16-
self.temp.cold = Set(self.temp, S(0, 15))
17-
self.temp.hot = Set(self.temp, R(10, 20))
16+
self.temp.cold = Set(S(0, 15))
17+
self.temp.hot = Set(R(10, 20))
1818

1919
def test_temp(self):
2020
# we need to define a dictionary-aggregation func here

0 commit comments

Comments
 (0)