11
22import matplotlib .pyplot as plt
33from numpy import arange , fromiter
4+ from logging import warn
45
56from fuzzy .functions import inv
67from fuzzy .combinators import MAX , MIN , product , bounded_sum
78
9+ class FuzzyWarning (UserWarning ):
10+ pass
811
912
1013class 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
9096class 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
169204class 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}
0 commit comments