@@ -22,6 +22,8 @@ def plt(*args, **kwargs):
22
22
from .combinators import MAX , MIN , bounded_sum , product , simple_disjoint_sum
23
23
from .functions import inv , normalize
24
24
25
+ type Number = int | float
26
+
25
27
26
28
class FuzzyWarning (UserWarning ):
27
29
"""Extra Exception so that user code can filter exceptions specific to this lib."""
@@ -49,7 +51,7 @@ class Domain:
49
51
It is possible now to call derived sets without assignment first!
50
52
>>> from .hedges import very
51
53
>>> (very(~temp.hot) | ~very(temp.hot))(2)
52
- 1
54
+ 1.0
53
55
54
56
You MUST NOT add arbitrary attributes to an *instance* of Domain - you can
55
57
however subclass or modify the class itself. If you REALLY have to add attributes,
@@ -65,14 +67,16 @@ class Domain:
65
67
def __init__ (
66
68
self ,
67
69
name : str ,
68
- low : float | int ,
69
- high : float | int ,
70
- res : float | int = 1 ,
70
+ low : Number ,
71
+ high : Number ,
72
+ res : Number = 1 ,
71
73
sets : dict | None = None ,
72
74
) -> None :
73
75
"""Define a domain."""
74
76
assert low < high , "higher bound must be greater than lower."
75
77
assert res > 0 , "resolution can't be negative or zero"
78
+ assert isinstance (name , str ), "Name must be a string."
79
+ assert str .isidentifier (name ), "Name must be a valid identifier."
76
80
self ._name = name
77
81
self ._high = high
78
82
self ._low = low
@@ -85,6 +89,10 @@ def __call__(self, x):
85
89
raise FuzzyWarning (f"{ x } is outside of domain!" )
86
90
return {name : s .func (x ) for name , s in self ._sets .items ()}
87
91
92
+ def __len__ (self ):
93
+ """Return the size of the domain, as the actual number of possible values, calculated internally."""
94
+ return len (self .range )
95
+
88
96
def __str__ (self ):
89
97
"""Return a string to print()."""
90
98
return self ._name
@@ -95,15 +103,13 @@ def __repr__(self):
95
103
96
104
def __eq__ (self , other ):
97
105
"""Test equality of two domains."""
98
- return all (
99
- [
100
- self ._name == other ._name ,
101
- self ._low == other ._low ,
102
- self ._high == other ._high ,
103
- self ._res == other ._res ,
104
- self ._sets == other ._sets ,
105
- ]
106
- )
106
+ return all ([
107
+ self ._name == other ._name ,
108
+ self ._low == other ._low ,
109
+ self ._high == other ._high ,
110
+ self ._res == other ._res ,
111
+ self ._sets == other ._sets ,
112
+ ])
107
113
108
114
def __hash__ (self ):
109
115
return id (self )
@@ -188,57 +194,71 @@ class Set:
188
194
189
195
"""
190
196
197
+ type T = Set
191
198
name = None # these are set on assignment to the domain! DO NOT MODIFY
192
199
domain = None
193
200
194
- def __init__ (self , func : Callable , * , name : str | None = None , domain : Domain | None = None ):
195
- self .func = func
196
- self .domain = domain
197
- self .name = name
198
- self .__center_of_gravity = None
199
-
200
- def __call__ (self , x ):
201
- return self .func (x )
201
+ def __init__ (
202
+ self ,
203
+ func : Callable [..., Number ],
204
+ * ,
205
+ name : str | None = None ,
206
+ domain : Domain | None = None ,
207
+ ):
208
+ self .func : Callable [..., Number ] = func
209
+ self .domain : Domain | None = domain
210
+ self .name : str | None = name
211
+ self .__center_of_gravity : np .floating | None = None
212
+
213
+ def __call__ (self , x : Number | np .ndarray ) -> Number | np .ndarray :
214
+ if isinstance (x , np .ndarray ):
215
+ return np .array ([self .func (v ) for v in x ])
216
+ else :
217
+ return self .func (x )
202
218
203
- def __invert__ (self ):
219
+ def __invert__ (self ) -> T :
204
220
"""Return a new set with 1 - function."""
205
221
return Set (inv (self .func ), domain = self .domain )
206
222
207
- def __neg__ (self ):
223
+ def __neg__ (self ) -> T :
208
224
"""Synonyme for invert."""
209
225
return Set (inv (self .func ), domain = self .domain )
210
226
211
- def __and__ (self , other ) :
227
+ def __and__ (self , other : T ) -> T :
212
228
"""Return a new set with modified function."""
213
229
assert self .domain == other .domain
214
230
return Set (MIN (self .func , other .func ), domain = self .domain )
215
231
216
- def __or__ (self , other ) :
232
+ def __or__ (self , other : T ) -> T :
217
233
"""Return a new set with modified function."""
218
234
assert self .domain == other .domain
219
235
return Set (MAX (self .func , other .func ), domain = self .domain )
220
236
221
- def __mul__ (self , other ) :
237
+ def __mul__ (self , other : T ) -> T :
222
238
"""Return a new set with modified function."""
223
239
assert self .domain == other .domain
224
240
return Set (product (self .func , other .func ), domain = self .domain )
225
241
226
- def __add__ (self , other ) :
242
+ def __add__ (self , other : T ) -> T :
227
243
"""Return a new set with modified function."""
228
244
assert self .domain == other .domain
229
245
return Set (bounded_sum (self .func , other .func ), domain = self .domain )
230
246
231
- def __xor__ (self , other ) :
247
+ def __xor__ (self , other : T ) -> T :
232
248
"""Return a new set with modified function."""
233
249
assert self .domain == other .domain
234
250
return Set (simple_disjoint_sum (self .func , other .func ), domain = self .domain )
235
251
236
- def __pow__ (self , power ) :
252
+ def __pow__ (self , power : int ) -> T :
237
253
"""Return a new set with modified function."""
254
+
238
255
# FYI: pow is used with hedges
239
- return Set (lambda x : pow (self .func (x ), power ), domain = self .domain )
256
+ def f (x : float ):
257
+ return pow (self .func (x ), power ) # TODO: test this
240
258
241
- def __eq__ (self , other ):
259
+ return Set (f , domain = self .domain )
260
+
261
+ def __eq__ (self , other : T ) -> bool :
242
262
"""A set is equal with another if both return the same values over the same range."""
243
263
if self .domain is None or other .domain is None :
244
264
# It would require complete AST analysis to check whether both Sets
@@ -251,49 +271,49 @@ def __eq__(self, other):
251
271
# we simply can check if they map to the same values
252
272
return np .array_equal (self .array (), other .array ())
253
273
254
- def __le__ (self , other ) :
274
+ def __le__ (self , other : T ) -> bool :
255
275
"""If this <= other, it means this is a subset of the other."""
256
276
assert self .domain == other .domain
257
277
if self .domain is None or other .domain is None :
258
278
raise FuzzyWarning ("Can't compare without Domains." )
259
279
return all (np .less_equal (self .array (), other .array ()))
260
280
261
- def __lt__ (self , other ) :
281
+ def __lt__ (self , other : T ) -> bool :
262
282
"""If this < other, it means this is a proper subset of the other."""
263
283
assert self .domain == other .domain
264
284
if self .domain is None or other .domain is None :
265
285
raise FuzzyWarning ("Can't compare without Domains." )
266
286
return all (np .less (self .array (), other .array ()))
267
287
268
- def __ge__ (self , other ) :
288
+ def __ge__ (self , other : T ) -> bool :
269
289
"""If this >= other, it means this is a superset of the other."""
270
290
assert self .domain == other .domain
271
291
if self .domain is None or other .domain is None :
272
292
raise FuzzyWarning ("Can't compare without Domains." )
273
293
return all (np .greater_equal (self .array (), other .array ()))
274
294
275
- def __gt__ (self , other ) :
295
+ def __gt__ (self , other : T ) -> bool :
276
296
"""If this > other, it means this is a proper superset of the other."""
277
297
assert self .domain == other .domain
278
298
if self .domain is None or other .domain is None :
279
299
raise FuzzyWarning ("Can't compare without Domains." )
280
300
return all (np .greater (self .array (), other .array ()))
281
301
282
- def __len__ (self ):
302
+ def __len__ (self ) -> int :
283
303
"""Number of membership values in the set, defined by bounds and resolution of domain."""
284
304
if self .domain is None :
285
305
raise FuzzyWarning ("No domain." )
286
306
return len (self .array ())
287
307
288
308
@property
289
- def cardinality (self ):
309
+ def cardinality (self ) -> int :
290
310
"""The sum of all values in the set."""
291
311
if self .domain is None :
292
312
raise FuzzyWarning ("No domain." )
293
313
return sum (self .array ())
294
314
295
315
@property
296
- def relative_cardinality (self ):
316
+ def relative_cardinality (self ) -> np . floating | float :
297
317
"""Relative cardinality is the sum of all membership values by number of all values."""
298
318
if self .domain is None :
299
319
raise FuzzyWarning ("No domain." )
@@ -302,7 +322,7 @@ def relative_cardinality(self):
302
322
raise FuzzyWarning ("The domain has no element." )
303
323
return self .cardinality / len (self )
304
324
305
- def concentrated (self ):
325
+ def concentrated (self ) -> T :
306
326
"""
307
327
Alternative to hedge "very".
308
328
@@ -311,7 +331,7 @@ def concentrated(self):
311
331
"""
312
332
return Set (lambda x : self .func (x ) ** 2 , domain = self .domain )
313
333
314
- def intensified (self ):
334
+ def intensified (self ) -> T :
315
335
"""
316
336
Alternative to hedges.
317
337
@@ -324,11 +344,11 @@ def f(x):
324
344
325
345
return Set (f , domain = self .domain )
326
346
327
- def dilated (self ):
347
+ def dilated (self ) -> T :
328
348
"""Expand the set with more values and already included values are enhanced."""
329
349
return Set (lambda x : self .func (x ) ** 1.0 / 2.0 , domain = self .domain )
330
350
331
- def multiplied (self , n ):
351
+ def multiplied (self , n ) -> T :
332
352
"""Multiply with a constant factor, changing all membership values."""
333
353
return Set (lambda x : self .func (x ) * n , domain = self .domain )
334
354
@@ -340,15 +360,22 @@ def plot(self):
340
360
V = [self .func (x ) for x in R ]
341
361
plt .plot (R , V )
342
362
343
- def array (self ):
363
+ def array (self ) -> np . ndarray :
344
364
"""Return an array of all values for this set within the given domain."""
345
365
if self .domain is None :
346
366
raise FuzzyWarning ("No domain assigned." )
347
367
return np .fromiter ((self .func (x ) for x in self .domain .range ), float )
348
368
349
- def center_of_gravity (self ):
350
- """Return the center of gravity for this distribution, within the given domain."""
369
+ def range (self ) -> np .ndarray :
370
+ """Return the range of the domain."""
371
+ if self .domain is None :
372
+ raise FuzzyWarning ("No domain assigned." )
373
+ return self .domain .range
351
374
375
+ def center_of_gravity (self ) -> np .floating | float :
376
+ """Return the center of gravity for this distribution, within the given domain."""
377
+ if self .__center_of_gravity is not None :
378
+ return self .__center_of_gravity
352
379
assert self .domain is not None , "No center of gravity with no domain."
353
380
weights = self .array ()
354
381
if sum (weights ) == 0 :
@@ -357,7 +384,7 @@ def center_of_gravity(self):
357
384
self .__center_of_gravity = cog
358
385
return cog
359
386
360
- def __repr__ (self ):
387
+ def __repr__ (self ) -> str :
361
388
"""
362
389
Return a string representation of the Set that reconstructs the set with eval().
363
390
@@ -377,9 +404,11 @@ def create_function_closure():
377
404
func = types.FunctionType(*args[:-1] + [closure])
378
405
return func
379
406
"""
380
- return f"Set({ self .func } )"
407
+ if self .domain is not None :
408
+ return f"{ self .domain ._name } ."
409
+ return f"Set({ __name__ } ({ self .func .__qualname__ } )"
381
410
382
- def __str__ (self ):
411
+ def __str__ (self ) -> str :
383
412
"""Return a string for print()."""
384
413
if self .domain is not None :
385
414
return f"{ self .domain ._name } .{ self .name } "
@@ -388,48 +417,50 @@ def __str__(self):
388
417
else :
389
418
return f"dangling Set({ self .name } "
390
419
391
- def normalized (self ):
420
+ def normalized (self ) -> T :
392
421
"""Return a set that is normalized *for this domain* with 1 as max."""
393
422
if self .domain is None :
394
423
raise FuzzyWarning ("Can't normalize without domain." )
395
424
return Set (normalize (max (self .array ()), self .func ), domain = self .domain )
396
425
397
- def __hash__ (self ):
426
+ def __hash__ (self ) -> int :
398
427
return id (self )
399
428
400
429
401
430
class Rule :
402
431
"""
403
- A collection of bound sets that span a multi-dimensional space of their respective domains.
432
+ Collection of bound sets spanning a multi-dimensional space of their domains, mapping to a target domain.
433
+
404
434
"""
405
435
406
- def __init__ (self , conditions , func = None ):
407
- print ("ohalala" )
408
- self .conditions = {frozenset (C ): oth for C , oth in conditions .items ()}
409
- self .func = func
436
+ type T = Rule
437
+
438
+ def __init__ (self , conditions_in : dict [Iterable [Set ] | Set , Set ]):
439
+ self .conditions : dict [frozenset [Set ], Set ] = {}
440
+ for if_sets , then_set in conditions_in .items ():
441
+ if isinstance (if_sets , Set ):
442
+ if_sets = (if_sets ,)
443
+ self .conditions [frozenset (if_sets )] = then_set
410
444
411
- def __add__ (self , other ):
412
- assert isinstance (other , Rule )
445
+ def __add__ (self , other : T ):
413
446
return Rule ({** self .conditions , ** other .conditions })
414
447
415
- def __radd__ (self , other ):
416
- assert isinstance (other , (Rule , int ))
448
+ def __radd__ (self , other : T | int ) -> T :
417
449
# we're using sum(..)
418
450
if isinstance (other , int ):
419
451
return self
420
452
return Rule ({** self .conditions , ** other .conditions })
421
453
422
- def __or__ (self , other ):
423
- assert isinstance (other , Rule )
454
+ def __or__ (self , other : T ):
424
455
return Rule ({** self .conditions , ** other .conditions })
425
456
426
- def __eq__ (self , other ):
457
+ def __eq__ (self , other : T ):
427
458
return self .conditions == other .conditions
428
459
429
460
def __getitem__ (self , key ):
430
461
return self .conditions [frozenset (key )]
431
462
432
- def __call__ (self , args : " dict[Domain, float]" , method = "cog" ):
463
+ def __call__ (self , values : dict [Domain , float | int ] , method = "cog" ) -> np . floating | float | None :
433
464
"""Calculate the infered value based on different methods.
434
465
Default is center of gravity (cog).
435
466
"""
0 commit comments