1 """\
2 Fuzzy number module. Contains basic fuzzy number class definitions.
3
4 @author: Aaron Mavrinac
5 @organization: University of Windsor
6 @contact: mavrin1@uwindsor.ca
7 @license: LGPL-3
8 """
9
10 from math import e, sqrt, log
11 from numbers import Number
12
13 from .fset import FuzzySet
17 """\
18 Real range class.
19 """
21 """\
22 Instatiation method. Verifies the validity of the range argument
23 before returning the range object.
24 """
25 if not len(arg) == 2:
26 raise ValueError('range must consist of two values')
27 if not isinstance(arg[0], Number) \
28 or not isinstance(arg[1], Number):
29 raise TypeError('range values must be numeric')
30 if arg[0] > arg[1]:
31 raise ValueError('range may not have negative size')
32 return tuple.__new__(cls, arg)
33
34 @property
36 """\
37 Return the size of the range.
38
39 @rtype: C{float}
40 """
41 return float(self[1] - self[0])
42
44 """\
45 Addition operation.
46
47 @param other: The other operand.
48 @type other: L{RealRange}
49 @return: Sum of ranges.
50 @rtype: L{RealRange}
51 """
52 return RealRange((self[0] + other[0], self[1] + other[1]))
53
55 """\
56 Subtraction operation.
57
58 @param other: The other operand.
59 @type other: L{RealRange}
60 @return: Difference of ranges.
61 @rtype: L{RealRange}
62 """
63 return RealRange((self[0] - other[1], self[1] - other[0]))
64
66 """\
67 Report whether a given value is within this range.
68
69 @param value: The value.
70 @type value: C{float}
71 @return: True if within the range, false otherwise.
72 @rtype: C{bool}
73 """
74 return value >= self[0] and value <= self[1]
75
77 """\
78 Report whether another range contains this range.
79
80 @param other: The other range.
81 @type other: L{RealRange}
82 @return: True if a subset, false otherwise.
83 @rtype: C{bool}
84 """
85 if not isinstance(other, RealRange):
86 raise TypeError('argument must be a RealRange')
87 if other[0] <= self[0] and other[1] >= self[1]:
88 return True
89 return False
90
92 """\
93 Report whether this range contains another range.
94
95 @param other: The other range.
96 @type other: L{RealRange}
97 @return: True if a superset, false otherwise.
98 @rtype: C{bool}
99 """
100 if not isinstance(other, RealRange):
101 raise TypeError('argument must be a RealRange')
102 if self[0] <= other[0] and self[1] >= other[1]:
103 return True
104 return False
105
106 __le__ = issubset
107 __ge__ = issuperset
108
110 """\
111 Report whether another range strictly contains this range.
112
113 @param other: The other range.
114 @type other: L{RealRange}
115 @return: True if a strict subset, false otherwise.
116 @rtype: C{bool}
117 """
118 return self.issubset(other) and not self == other
119
121 """\
122 Report whether this range strictly contains another range.
123
124 @param other: The other range.
125 @type other: L{RealRange}
126 @return: True if a strict superset, false otherwise.
127 @rtype: C{bool}
128 """
129 return self.issuperset(other) and not self == other
130
133 """\
134 Fuzzy number class (abstract base class for all fuzzy numbers).
135 """
137 """\
138 Constructor. Not to be instantiated directly.
139 """
140 if self.__class__ is FuzzyNumber:
141 raise NotImplementedError('please use one of the subclasses')
142
144 """\
145 Return the canonical representation of a fuzzy number.
146
147 @return: Canonical representation.
148 @rtype: C{str}
149 """
150 return '<%s>' % self.__class__.__name__
151
153 """\
154 Return the string representation of a fuzzy number.
155
156 @return: String representation.
157 @rtype: C{str}
158 """
159 return '%s: kernel %s, support %s' % \
160 (self.__class__.__name__, str(self.kernel), str(self.support))
161
162 @staticmethod
164 """\
165 Check that the other argument to a binary operation is also a
166 fuzzy number, raising a TypeError otherwise.
167
168 @param other: The other argument.
169 @type other: L{FuzzyNumber}
170 """
171 if not isinstance(other, FuzzyNumber):
172 raise TypeError('operation only permitted between fuzzy numbers')
173
174 - def mu(self, value):
175 """\
176 Return the membership level of a value in the universal set domain of
177 the fuzzy number.
178
179 @param value: A value in the universal set.
180 @type value: C{float}
181 """
182 raise NotImplementedError('mu method must be overridden')
183
185 """\
186 Normalize this fuzzy number, so that its height is equal to 1.0.
187 """
188 if not self.height == 1.0:
189 raise NotImplementedError('normalize method must be overridden')
190
192 """\
193 Convert this fuzzy number into a polygonal fuzzy number.
194
195 @return: Result polygonal fuzzy number.
196 @rtype: L{PolygonalFuzzyNumber}
197 """
198 raise NotImplementedError('to_polygonal method must be overridden')
199
200 kernel = None
201 support = None
202 height = None
203
205 """\
206 Return the standard fuzzy union of two polygonal fuzzy numbers.
207
208 @param other: The other fuzzy number.
209 @type other: L{FuzzyNumber}
210 @return: The fuzzy union.
211 @rtype: L{FuzzyNumber}
212 """
213 return self.union(other)
214
216 """\
217 In-place standard fuzzy union.
218
219 @param other: The other fuzzy number.
220 @type other: L{FuzzyNumber}
221 @return: The fuzzy union (self).
222 @rtype: L{FuzzyNumber}
223 """
224 self = self.union(other)
225 return self
226
228 """\
229 Return the standard fuzzy union of two fuzzy numbers as a new polygonal
230 fuzzy number.
231
232 @param other: The other fuzzy number.
233 @type other: L{FuzzyNumber}
234 @return: The fuzzy union.
235 @rtype: L{PolygonalFuzzyNumber}
236 """
237 self._binary_sanity_check(other)
238 return self.to_polygonal() | other.to_polygonal()
239
241 """\
242 Return the standard fuzzy intersection of two fuzzy numbers.
243
244 @param other: The other fuzzy number.
245 @type other: L{FuzzyNumber}
246 @return: The fuzzy intersection.
247 @rtype: L{FuzzyNumber}
248 """
249 return self.intersection(other)
250
252 """\
253 In-place standard fuzzy intersection.
254
255 @param other: The other fuzzy number.
256 @type other: L{FuzzyNumber}
257 @return: The fuzzy intersection (self).
258 @rtype: L{FuzzyNumber}
259 """
260 self = self.intersection(other)
261 return self
262
264 """\
265 Return the standard fuzzy intersection of two fuzzy numbers as a new
266 polygonal fuzzy number.
267
268 @param other: The other fuzzy number.
269 @type other: L{FuzzyNumber}
270 @return: The fuzzy union.
271 @rtype: L{PolygonalFuzzyNumber}
272 """
273 self._binary_sanity_check(other)
274 return self.to_polygonal() & other.to_polygonal()
275
278 """\
279 Polygonal fuzzy number class.
280 """
282 """\
283 Constructor.
284
285 @param points: A set of points from which to generate the polygon.
286 @type points: C{list} of C{tuple}
287 """
288 if not points[0][1] == 0.0 or not points[-1][1] == 0.0:
289 raise ValueError('points must start and end with mu = 0')
290 for i in range(1, len(points)):
291 if not points[i][0] >= points[i - 1][0]:
292 raise ValueError('points must be in increasing order')
293 self.points = points
294 super(PolygonalFuzzyNumber, self).__init__()
295
297 """\
298 Return the canonical string representation of this polygonal fuzzy
299 number.
300
301 @return: Canonical string representation.
302 @rtype: C{str}
303 """
304 return 'PolygonalFuzzyNumber(%s)' % self.points
305
307 """\
308 Return whether this polygonal fuzzy number is equal to another fuzzy
309 number.
310
311 @param other: The other fuzzy number.
312 @type other: L{FuzzyNumber}
313 @return: True if equal.
314 @rtype: C{bool}
315 """
316 return self.points == other.to_polygonal().points
317
318 - def mu(self, value):
319 """\
320 Return the membership level of a value in the universal set domain of
321 the fuzzy number.
322
323 @param value: A value in the universal set.
324 @type value: C{float}
325 """
326 if not True in [value in subrange for subrange in self.support]:
327 return 0.0
328 for i in range(1, len(self.points)):
329 if self.points[i][0] > value:
330 return ((value - self.points[i - 1][0]) / (self.points[i][0] \
331 - self.points[i - 1][0])) * (self.points[i][1] - \
332 self.points[i - 1][1]) + self.points[i - 1][1]
333 return 0.0
334
335 @property
337 """\
338 Return the kernel of the fuzzy number (range of values in the
339 universal set where membership degree is equal to one).
340
341 @rtype: C{list} of L{RealRange}
342 """
343 kernel = []
344 start = None
345 for i in range(1, len(self.points)):
346 if start is None and self.points[i][1] == 1.0:
347 start = i
348 elif start is not None and self.points[i][1] < 1.0:
349 kernel.append(RealRange((self.points[start][0],
350 self.points[i - 1][0])))
351 start = None
352 return kernel
353
354 @property
356 """\
357 Return the support of the fuzzy number (range of values in the
358 universal set where membership degree is nonzero).
359
360 @rtype: C{list} of L{RealRange}
361 """
362 support = []
363 start = None
364 for i in range(1, len(self.points)):
365 if start is None and self.points[i][1] > 0.0:
366 start = i - 1
367 elif start is not None and self.points[i][1] == 0.0:
368 support.append(RealRange((self.points[start][0],
369 self.points[i][0])))
370 start = None
371 return support
372
373 @property
375 """\
376 Return the height of the fuzzy number (maximum membership degree
377 value).
378
379 @rtype: C{float}
380 """
381 return max([point[1] for point in self.points])
382
383 @staticmethod
385 """\
386 Return the point of intersection of line segments pq and rs. Helper
387 function for union and intersection.
388
389 @return: The point of intersection.
390 @rtype: C{tuple} of C{float}
391 """
392 try:
393 ua = ((s[0] - r[0]) * (p[1] - r[1]) - \
394 (s[1] - r[1]) * (p[0] - r[0])) / \
395 ((s[1] - r[1]) * (q[0] - p[0]) - \
396 (s[0] - r[0]) * (q[1] - p[1]))
397 except ZeroDivisionError:
398 return None
399 return(p[0] + ua * (q[0] - p[0]), p[1] + ua * (q[1] - p[1]))
400
402 """\
403 Return the standard fuzzy union of two polygonal fuzzy numbers as a new
404 polygonal fuzzy number.
405
406 @param other: The other fuzzy number.
407 @type other: L{FuzzyNumber}
408 @return: The fuzzy union.
409 @rtype: L{PolygonalFuzzyNumber}
410 """
411 other = other.to_polygonal()
412
413 points = [[point, i, self] for i, point in enumerate(self.points)] \
414 + [[point, i, other] for i, point in enumerate(other.points)]
415 points.sort()
416
417 i = 0
418 while True:
419 try:
420 if points[i][0][0] == points[i + 1][0][0]:
421 if points[i][0][1] < points[i + 1][0][1]:
422 del points[i]
423 else:
424 del points[i + 1]
425 continue
426 i += 1
427 except IndexError:
428 break
429
430 i = 0
431 while True:
432 try:
433 if points[i][2] is not points[i + 1][2]:
434 int = self._line_intersection(points[i][0],
435 points[i][2].points[points[i][1] + 1], points[i + 1][0],
436 points[i + 1][2].points[points[i + 1][1] - 1])
437 if int and int[1] > 0 and int[0] > points[i][0][0] \
438 and int[0] < points[i + 1][0][0]:
439 points.insert(i + 1, [int, None, None])
440 i += 1
441 i += 1
442 except IndexError:
443 break
444
445 for point in points:
446 point[0] = (point[0][0], max(self.mu(point[0][0]), other.mu(point[0][0])))
447
448 while points[1][0][1] == 0.0:
449 del points[0]
450 while points[-2][0][1] == 0.0:
451 del points[-1]
452 i = 1
453 while True:
454 try:
455 if points[i][0][1] == points[i - 1][0][1] \
456 and points[i][0][1] == points[i + 1][0][1]:
457 del points[i]
458 continue
459 i += 1
460 except IndexError:
461 break
462 return PolygonalFuzzyNumber([point[0] for point in points])
463
465 """\
466 Return the standard fuzzy intersection of two polygonal fuzzy numbers
467 as a new polygonal fuzzy number.
468
469 @param other: The other fuzzy number.
470 @type other: L{FuzzyNumber}
471 @return: The fuzzy intersection.
472 @rtype: L{PolygonalFuzzyNumber}
473 """
474 other = other.to_polygonal()
475
476 points = [[point, i, self] for i, point in enumerate(self.points)] \
477 + [[point, i, other] for i, point in enumerate(other.points)]
478 points.sort()
479
480 i = 0
481 while True:
482 try:
483 if points[i][0][0] == points[i + 1][0][0]:
484 if points[i][0][1] > points[i + 1][0][1]:
485 del points[i]
486 else:
487 del points[i + 1]
488 continue
489 i += 1
490 except IndexError:
491 break
492
493 i = 0
494 while True:
495 try:
496 if points[i][2] is not points[i + 1][2]:
497 int = self._line_intersection(points[i][0],
498 points[i][2].points[points[i][1] + 1], points[i + 1][0],
499 points[i + 1][2].points[points[i + 1][1] - 1])
500 if int and int[1] > 0 and int[0] > points[i][0][0] \
501 and int[0] < points[i + 1][0][0]:
502 points.insert(i + 1, [int, None, None])
503 i += 1
504 i += 1
505 except IndexError:
506 break
507
508 for point in points:
509 point[0] = (point[0][0], min(self.mu(point[0][0]), other.mu(point[0][0])))
510
511 while points[1][0][1] == 0.0:
512 del points[0]
513 while points[-2][0][1] == 0.0:
514 del points[-1]
515 i = 1
516 while True:
517 try:
518 if points[i][0][1] == points[i - 1][0][1] \
519 and points[i][0][1] == points[i + 1][0][1]:
520 del points[i]
521 continue
522 i += 1
523 except IndexError:
524 break
525 return PolygonalFuzzyNumber([point[0] for point in points])
526
528 """\
529 Normalize this fuzzy number, so that its height is equal to 1.0.
530 """
531 self.points = [(point[0], point[1] * (1.0 / self.height)) \
532 for point in self.points]
533
535 """\
536 Return this polygonal fuzzy number.
537
538 @return: This polygonal fuzzy number.
539 @rtype: L{PolygonalFuzzyNumber}
540 """
541 return self
542
544 """\
545 Convert this polygonal fuzzy number to a discrete fuzzy set at the
546 specified sample points. If no sample points are specified, the
547 vertices of the polygonal fuzzy number will be used.
548
549 @param samplepoints: Set of points at which to sample the number.
550 @type samplepoints: C{set} of C{float}
551 @return: Result fuzzy set.
552 @rtype: L{fset.FuzzySet}
553 """
554 if samplepoints is None:
555 samplepoints = [point[0] for point in self.points]
556 F = FuzzySet()
557 for point in samplepoints:
558 F.add(point, self.mu(point))
559 return F
560
563 """\
564 Trapezoidal fuzzy number class.
565 """
566 - def __init__(self, kernel=(0.0, 0.0), support=(0.0, 0.0)):
567 """\
568 Constructor.
569
570 @param kernel: The kernel of the fuzzy number.
571 @type kernel: C{tuple}
572 @param support: The support of the fuzzy number.
573 @type support: C{tuple}
574 """
575 if not (isinstance(kernel, tuple) and len(kernel) == 2) \
576 or not (isinstance(support, tuple) and len(support) == 2):
577 raise TypeError('kernel and support must be 2-tuples')
578 self.kernel = RealRange(kernel)
579 self.support = RealRange(support)
580 if not self.kernel <= self.support:
581 raise ValueError('kernel range must be within support range')
582 self.height = 1.0
583 super(TrapezoidalFuzzyNumber, self).__init__()
584
585 @property
587 """\
588 Report if this is a triangular fuzzy number (kernel has zero size).
589
590 @rtype: C{bool}
591 """
592 return self.kernel.size == 0
593
595 """\
596 Addition operation.
597
598 @param other: The other trapezoidal fuzzy number.
599 @type other: L{TrapezoidalFuzzyNumber}
600 @return: Sum of the trapezoidal fuzzy numbers.
601 @rtype: L{TrapezoidalFuzzyNumber}
602 """
603 if not isinstance(other, TrapezoidalFuzzyNumber):
604 raise TypeError('operation only permitted between trapezoidal '
605 'fuzzy numbers')
606 return self.__class__(self.kernel + other.kernel,
607 self.support + other.support)
608
610 """\
611 Subtraction operation.
612
613 @param other: The other trapezoidal fuzzy number.
614 @type other: L{TrapezoidalFuzzyNumber}
615 @return: Difference of the trapezoidal fuzzy numbers.
616 @rtype: L{TrapezoidalFuzzyNumber}
617 """
618 if not isinstance(other, TrapezoidalFuzzyNumber):
619 raise TypeError('operation only permitted between trapezoidal '
620 'fuzzy numbers')
621 return self.__class__(self.kernel - other.kernel,
622 self.support - other.support)
623
624 - def mu(self, value):
625 """\
626 Return the membership level of a value in the universal set domain of
627 the fuzzy number.
628
629 @param value: A value in the universal set.
630 @type value: C{float}
631 """
632 if value in self.kernel:
633 return 1.
634 elif value > self.support[0] and value < self.kernel[0]:
635 return (value - self.support[0]) / \
636 (self.kernel[0] - self.support[0])
637 elif value < self.support[1] and value > self.kernel[1]:
638 return (self.support[1] - value) / \
639 (self.support[1] - self.kernel[1])
640 else:
641 return 0.
642
644 """\
645 Alpha cut function. Returns the interval within the fuzzy number whose
646 membership levels meet or exceed the alpha value.
647
648 @param alpha: The alpha value for the cut in [0, 1].
649 @type alpha: C{float}
650 @return: The alpha cut interval.
651 @rtype: L{RealRange}
652 """
653 return RealRange(((self.kernel[0] - self.support[0]) * alpha \
654 + self.support[0], self.support[1] - \
655 (self.support[1] - self.kernel[1]) * alpha))
656
658 """\
659 Convert this trapezoidal fuzzy number into a polygonal fuzzy number.
660
661 @return: Result polygonal fuzzy number.
662 @rtype: L{PolygonalFuzzyNumber}
663 """
664 points = [(self.support[0], 0.0),
665 (self.kernel[0], 1.0),
666 (self.kernel[1], 1.0),
667 (self.support[1], 0.0)]
668 return PolygonalFuzzyNumber(points)
669
672 """\
673 Triangular fuzzy number class (special case of trapezoidal fuzzy number).
674 """
675 - def __init__(self, kernel=0.0, support=(0.0, 0.0)):
686
689 """\
690 Gaussian fuzzy number class.
691 """
693 """\
694 Constructor.
695
696 @param mean: The mean (central value) of the Gaussian.
697 @type mean: C{float}
698 @param stddev: The standard deviation of the Gaussian.
699 @type stddev: C{float}
700 """
701 self.mean = mean
702 self.stddev = stddev
703 self.height = 1.0
704 super(GaussianFuzzyNumber, self).__init__()
705
707 """\
708 Addition operation.
709
710 @param other: The other gaussian fuzzy number.
711 @type other: L{GaussianFuzzyNumber}
712 @return: Sum of the gaussian fuzzy numbers.
713 @rtype: L{GaussianFuzzyNumber}
714 """
715 if not isinstance(other, GaussianFuzzyNumber):
716 raise TypeError('operation only permitted between Gaussian '
717 'fuzzy numbers')
718 return self.__class__(self.mean + other.mean,
719 self.stddev + other.stddev)
720
722 """\
723 Subtraction operation.
724
725 @param other: The other gaussian fuzzy number.
726 @type other: L{GaussianFuzzyNumber}
727 @return: Difference of the gaussian fuzzy numbers.
728 @rtype: L{GaussianFuzzyNumber}
729 """
730 if not isinstance(other, GaussianFuzzyNumber):
731 raise TypeError('operation only permitted between Gaussian '
732 'fuzzy numbers')
733 return self.__class__(self.mean - other.mean,
734 self.stddev + other.stddev)
735
736 - def mu(self, value):
737 """\
738 Return the membership level of a value in the universal set domain of
739 the fuzzy number.
740
741 @param value: A value in the universal set.
742 @type value: C{float}
743 """
744 return e ** -((value - self.mean) ** 2 / (2.0 * self.stddev ** 2)) \
745 if value in self.support else 0.0
746
747 @property
749 """\
750 Return the kernel of the fuzzy number (range of values in the
751 universal set where membership degree is equal to one).
752
753 @rtype: L{RealRange}
754 """
755 return RealRange((self.mean, self.mean))
756
757 @property
759 """\
760 Return the support of the fuzzy number (range of values in the
761 universal set where membership degree is nonzero).
762
763 @rtype: L{RealRange}
764 """
765 return self.alpha(1e-10)
766
768 """\
769 Alpha cut function. Returns the interval within the fuzzy number whose
770 membership levels meet or exceed the alpha value.
771
772 @param alpha: The alpha value for the cut in [0, 1].
773 @type alpha: C{float}
774 @return: The alpha cut interval.
775 @rtype: L{RealRange}
776 """
777 if alpha < 1e-10:
778 alpha = 1e-10
779 edge = sqrt(-2.0 * (self.stddev ** 2) * log(alpha))
780 return RealRange((self.mean - edge, self.mean + edge))
781
783 """\
784 Convert this Gaussian fuzzy number into a polygonal fuzzy number
785 (approximate).
786
787 @param np: The number of points to interpolate per side (optional).
788 @type np: C{int}
789 @return: Result polygonal fuzzy number.
790 @rtype: L{PolygonalFuzzyNumber}
791 """
792 if np < 0:
793 raise ValueError('number of points must be positive')
794 points = []
795 start, end = self.support
796 increment = (self.mean - start) / float(np + 1)
797 points.append((start, 0.0))
798 for i in range(1, np + 1):
799 value = start + i * increment
800 points.append((value, self.mu(value)))
801 points.append((self.mean, 1.0))
802 for i in range(1, np + 1):
803 value = self.mean + i * increment
804 points.append((value, self.mu(value)))
805 points.append((end, 0.0))
806 return PolygonalFuzzyNumber(points)
807