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: GPL-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 """
20 - def __new__(cls, arg = (0.0, 0.0)):
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 string representation of a trapezoidal fuzzy number.
146
147 @return: String representation.
148 @rtype: C{string}
149 """
150 return '%s: kernel %s, support %s' % \
151 (self.__class__.__name__, str(self.kernel), str(self.support))
152
153 __str__ = __repr__
154
155 - def mu(self, value):
156 """\
157 Return the membership level of a value in the universal set domain of
158 the fuzzy number.
159
160 @param value: A value in the universal set.
161 @type value: C{float}
162 """
163 raise NotImplementedError("mu method must be overridden")
164
166 """\
167 Normalize this fuzzy number, so that its height is equal to 1.0.
168 """
169 if not self.height == 1.0:
170 raise NotImplementedError("normalize method must be overridden")
171
172 kernel = None
173 support = None
174 height = None
175
178 """\
179 Polygonal fuzzy number class.
180 """
182 """\
183 Constructor.
184
185 @param points: A set of points from which to generate the polygon.
186 @type points: C{list} of C{tuple}
187 """
188 if not points[0][1] == 0.0 or not points[-1][1] == 0.0:
189 raise ValueError("points must start and end with mu = 0")
190 for i in range(1, len(points)):
191 if not points[i][0] > points[i - 1][0]:
192 raise ValueError("points must be in increasing order")
193 self.points = points
194 FuzzyNumber.__init__(self)
195
196 @staticmethod
198 """\
199 Check that the other argument to a binary operation is also a
200 polygonal fuzzy number, raising a TypeError otherwise.
201
202 @param other: The other argument.
203 @type other: L{PolygonalFuzzyNumber}
204 """
205 if not isinstance(other, PolygonalFuzzyNumber):
206 raise TypeError("binary operation only permitted between \
207 polygonal fuzzy numbers")
208
209 - def mu(self, value):
210 """\
211 Return the membership level of a value in the universal set domain of
212 the fuzzy number.
213
214 @param value: A value in the universal set.
215 @type value: C{float}
216 """
217 if not True in [value in subrange for subrange in self.support]:
218 return 0.0
219 for i in range(1, len(self.points)):
220 if self.points[i][0] > value:
221 return ((value - self.points[i - 1][0]) / (self.points[i][0] \
222 - self.points[i - 1][0])) * (self.points[i][1] - \
223 self.points[i - 1][1]) + self.points[i - 1][1]
224 return 0.0
225
226 @property
228 """\
229 Return the kernel of the fuzzy number (range of values in the
230 universal set where membership degree is equal to one).
231
232 @rtype: C{list} of L{RealRange}
233 """
234 kernel = []
235 start = None
236 for i in range(1, len(self.points)):
237 if start is None and self.points[i][1] == 1.0:
238 start = i
239 elif start is not None and self.points[i][1] < 1.0:
240 kernel.append(RealRange((self.points[start][0],
241 self.points[i - 1][0])))
242 start = None
243 return kernel
244
245 @property
247 """\
248 Return the support of the fuzzy number (range of values in the
249 universal set where membership degree is nonzero).
250
251 @rtype: C{list} of L{RealRange}
252 """
253 support = []
254 start = None
255 for i in range(1, len(self.points)):
256 if start is None and self.points[i][1] > 0.0:
257 start = i - 1
258 elif start is not None and self.points[i][1] == 0.0:
259 support.append(RealRange((self.points[start][0],
260 self.points[i][0])))
261 start = None
262 return support
263
264 @property
266 """\
267 Return the height of the fuzzy number (maximum membership degree
268 value).
269
270 @rtype: C{float}
271 """
272 return max([point[1] for point in self.points])
273
274 @staticmethod
276 """\
277 Return the point of intersection of line segments pq and rs. Helper
278 function for union and intersection.
279
280 @return: The point of intersection.
281 @rtype: C{tuple} of C{float}
282 """
283 ua = ((s[0] - r[0]) * (p[1] - r[1]) - (s[1] - r[1]) * (p[0] - r[0])) / \
284 ((s[1] - r[1]) * (q[0] - p[0]) - (s[0] - r[0]) * (q[1] - p[1]))
285 return(p[0] + ua * (q[0] - p[0]), p[1] + ua * (q[1] - p[1]))
286
288 """\
289 Return the standard fuzzy union of two polygonal fuzzy numbers as a new
290 polygonal fuzzy number.
291
292 @param other: The other fuzzy number.
293 @type other: L{PolygonalFuzzyNumber}
294 @return: The fuzzy union.
295 @rtype: L{PolygonalFuzzyNumber}
296 """
297 return self.union(other)
298
300 """\
301 In-place standard fuzzy union.
302
303 @param other: The other fuzzy number.
304 @type other: L{PolygonalFuzzyNumber}
305 @return: The fuzzy union (self).
306 @rtype: L{PolygonalFuzzyNumber}
307 """
308 self = self.union(other)
309 return self
310
312 """\
313 Return the standard fuzzy union of two polygonal fuzzy numbers as a new
314 polygonal fuzzy number.
315
316 @param other: The other fuzzy number.
317 @type other: L{PolygonalFuzzyNumber}
318 @return: The fuzzy union.
319 @rtype: L{PolygonalFuzzyNumber}
320 """
321 self._binary_sanity_check(other)
322 points = []
323 for i in range(len(self.points)):
324 if self.points[i][1] >= other.mu(self.points[i][0]):
325 points.append((self.points[i], self, i))
326 for i in range(len(other.points)):
327 if other.points[i][1] >= self.mu(other.points[i][0]):
328 points.append((other.points[i], other, i))
329 points.sort()
330 i = 0
331 while(True):
332 try:
333 if points[i][0][0] == points[i + 1][0][0]:
334 del points[i]
335 continue
336 if points[i][1] is not points[i + 1][1]:
337 points.insert(i + 1, (self._line_intersection(points[i][0],
338 points[i][1].points[points[i][2] + 1], points[i + 1][0],
339 points[i + 1][1].points[points[i + 1][2] - 1]),
340 None, None))
341 if points[i + 1][0][0] == points[i + 2][0][0]:
342 del points[i + 1]
343 else:
344 i += 1
345 i += 1
346 except IndexError:
347 break
348 return PolygonalFuzzyNumber([point[0] for point in points])
349
351 """\
352 Return the standard fuzzy intersection of two polygonal fuzzy numbers
353 as a new polygonal fuzzy number.
354
355 @param other: The other fuzzy number.
356 @type other: L{PolygonalFuzzyNumber}
357 @return: The fuzzy intersection.
358 @rtype: L{PolygonalFuzzyNumber}
359 """
360 return self.intersection(other)
361
363 """\
364 In-place standard fuzzy intersection.
365
366 @param other: The other fuzzy number.
367 @type other: L{PolygonalFuzzyNumber}
368 @return: The fuzzy intersection (self).
369 @rtype: L{PolygonalFuzzyNumber}
370 """
371 self = self.intersection(other)
372 return self
373
375 """\
376 Return the standard fuzzy intersection of two polygonal fuzzy numbers
377 as a new polygonal fuzzy number.
378
379 @param other: The other fuzzy number.
380 @type other: L{PolygonalFuzzyNumber}
381 @return: The fuzzy intersection.
382 @rtype: L{PolygonalFuzzyNumber}
383 """
384 self._binary_sanity_check(other)
385 points = []
386 for i in range(len(self.points)):
387 if self.points[i][1] <= other.mu(self.points[i][0]):
388 points.append((self.points[i], self, i))
389 for i in range(len(other.points)):
390 if other.points[i][1] <= self.mu(other.points[i][0]):
391 points.append((other.points[i], other, i))
392 points.sort()
393 i = 0
394 while(True):
395 try:
396 if points[i][0][0] == points[i + 1][0][0] \
397 or points[i][0][1] == 0.0 and points[i + 1][0][1] == 0.0:
398 del points[i]
399 continue
400 if points[i][1] is not points[i + 1][1]:
401 points.insert(i + 1, (self._line_intersection(points[i][0],
402 points[i][1].points[points[i][2] + 1], points[i + 1][0],
403 points[i + 1][1].points[points[i + 1][2] - 1]),
404 None, None))
405 i += 1
406 i += 1
407 except IndexError:
408 break
409 return PolygonalFuzzyNumber([point[0] for point in points])
410
412 """\
413 Normalize this fuzzy number, so that its height is equal to 1.0.
414 """
415 self.points = [(point[0], point[1] * (1.0 / self.height)) \
416 for point in self.points]
417
419 """\
420 Convert this polygonal fuzzy number to a discrete fuzzy set at the
421 specified sample points. If no sample points are specified, the
422 vertices of the polygonal fuzzy number will be used.
423
424 @param samplepoints: Set of points at which to sample the number.
425 @type samplepoints: C{set} of C{float}
426 @return: Result fuzzy set.
427 @rtype: L{fset.FuzzySet}
428 """
429 if samplepoints is None:
430 samplepoints = [point[0] for point in self.points]
431 F = FuzzySet()
432 for point in samplepoints:
433 F.add_fuzzy(point, self.mu(point))
434 return F
435
438 """\
439 Trapezoidal fuzzy number class.
440 """
441 - def __init__(self, kernel = (0.0, 0.0), support = (0.0, 0.0)):
442 """\
443 Constructor.
444
445 @param kernel: The kernel of the fuzzy number.
446 @type kernel: C{tuple}
447 @param support: The support of the fuzzy number.
448 @type support: C{tuple}
449 """
450 if not (isinstance(kernel, tuple) and len(kernel) == 2) \
451 or not (isinstance(support, tuple) and len(support) == 2):
452 raise TypeError("kernel and support must be 2-tuples")
453 self.kernel = RealRange(kernel)
454 self.support = RealRange(support)
455 if not self.kernel <= self.support:
456 raise ValueError("kernel range must be within support range")
457 self.height = 1.0
458 FuzzyNumber.__init__(self)
459
460 @property
462 """\
463 Report if this is a triangular fuzzy number (kernel has zero size).
464
465 @rtype: C{bool}
466 """
467 return self.kernel.size == 0
468
469 @staticmethod
471 """\
472 Check that the other argument to a binary operation is also a
473 trapezoidal fuzzy number, raising a TypeError otherwise.
474
475 @param other: The other argument.
476 @type other: L{TrapezoidalFuzzyNumber}
477 """
478 if not isinstance(other, TrapezoidalFuzzyNumber):
479 raise TypeError("binary operation only permitted between \
480 trapezoidal fuzzy numbers")
481
483 """\
484 Addition operation.
485
486 @param other: The other trapezoidal fuzzy number.
487 @type other: L{TrapezoidalFuzzyNumber}
488 @return: Sum of the trapezoidal fuzzy numbers.
489 @rtype: L{TrapezoidalFuzzyNumber}
490 """
491 self._binary_sanity_check(other)
492 return self.__class__(self.kernel + other.kernel,
493 self.support + other.support)
494
496 """\
497 Subtraction operation.
498
499 @param other: The other trapezoidal fuzzy number.
500 @type other: L{TrapezoidalFuzzyNumber}
501 @return: Difference of the trapezoidal fuzzy numbers.
502 @rtype: L{TrapezoidalFuzzyNumber}
503 """
504 self._binary_sanity_check(other)
505 return self.__class__(self.kernel - other.kernel,
506 self.support - other.support)
507
508 - def mu(self, value):
509 """\
510 Return the membership level of a value in the universal set domain of
511 the fuzzy number.
512
513 @param value: A value in the universal set.
514 @type value: C{float}
515 """
516 if value in self.kernel:
517 return 1.
518 elif value > self.support[0] and value < self.kernel[0]:
519 return (value - self.support[0]) / \
520 (self.kernel[0] - self.support[0])
521 elif value < self.support[1] and value > self.kernel[1]:
522 return (self.support[1] - value) / \
523 (self.support[1] - self.kernel[1])
524 else:
525 return 0.
526
528 """\
529 Alpha cut function. Returns the interval within the fuzzy number whose
530 membership levels meet or exceed the alpha value.
531
532 @param alpha: The alpha value for the cut in [0, 1].
533 @type alpha: C{float}
534 @return: The alpha cut interval.
535 @rtype: L{RealRange}
536 """
537 return RealRange(((self.kernel[0] - self.support[0]) * alpha \
538 + self.support[0], self.support[1] - \
539 (self.support[1] - self.kernel[1]) * alpha))
540
542 """\
543 Convert this trapezoidal fuzzy number into a polygonal fuzzy number.
544
545 @return: Result polygonal fuzzy number.
546 @rtype: L{PolygonalFuzzyNumber}
547 """
548 points = [(self.support[0], 0.0),
549 (self.kernel[0], 1.0),
550 (self.kernel[1], 1.0),
551 (self.support[1], 0.0)]
552 return PolygonalFuzzyNumber(points)
553
556 """\
557 Triangular fuzzy number class (special case of trapezoidal fuzzy number).
558 """
559 - def __init__(self, kernel = 0.0, support = (0.0, 0.0)):
560 """\
561 Constructor.
562
563 @param kernel: The kernel value of the fuzzy number.
564 @type kernel: C{float}
565 @param support: The support of the fuzzy number.
566 @type support: C{tuple}
567 """
568 TrapezoidalFuzzyNumber.__init__((kernel, kernel), support)
569
572 """\
573 Gaussian fuzzy number class.
574 """
576 """\
577 Constructor.
578
579 @param mean: The mean (central value) of the Gaussian.
580 @type mean: C{float}
581 @param stddev: The standard deviation of the Gaussian.
582 @type stddev: C{float}
583 """
584 self.mean = mean
585 self.stddev = stddev
586 self.height = 1.0
587 FuzzyNumber.__init__(self)
588
589 - def mu(self, value):
590 """\
591 Return the membership level of a value in the universal set domain of
592 the fuzzy number.
593
594 @param value: A value in the universal set.
595 @type value: C{float}
596 """
597 if value not in self.support:
598 return 0.0
599 return e ** -((value - self.mean) ** 2 / (2.0 * self.stddev ** 2))
600
601 @property
603 """\
604 Return the kernel of the fuzzy number (range of values in the
605 universal set where membership degree is equal to one).
606
607 @rtype: L{RealRange}
608 """
609 return RealRange((self.mean, self.mean))
610
611 @property
613 """\
614 Return the support of the fuzzy number (range of values in the
615 universal set where membership degree is nonzero).
616
617 @rtype: L{RealRange}
618 """
619 return self.alpha(1e-10)
620
622 """\
623 Alpha cut function. Returns the interval within the fuzzy number whose
624 membership levels meet or exceed the alpha value.
625
626 @param alpha: The alpha value for the cut in [0, 1].
627 @type alpha: C{float}
628 @return: The alpha cut interval.
629 @rtype: L{RealRange}
630 """
631 if alpha < 1e-10:
632 alpha = 1e-10
633 edge = sqrt(-2.0 * (self.stddev ** 2) * log(alpha))
634 return RealRange((self.mean - edge, self.mean + edge))
635
637 """\
638 Convert this Gaussian fuzzy number into a polygonal fuzzy number
639 (approximate).
640
641 @param np: The number of points to interpolate per side.
642 @type np: C{int}
643 @return: Result polygonal fuzzy number.
644 @rtype: L{PolygonalFuzzyNumber}
645 """
646 if np < 0:
647 raise ValueError("number of points must be positive")
648 points = []
649 start, end = self.support
650 increment = (self.mean - start) / float(np + 1)
651 points.append((start, 0.0))
652 for i in range(1, np + 1):
653 value = start + i * increment
654 points.append((value, self.mu(value)))
655 points.append((self.mean, 1.0))
656 for i in range(1, np + 1):
657 value = self.mean + i * increment
658 points.append((value, self.mu(value)))
659 points.append((end, 0.0))
660 print points
661 return PolygonalFuzzyNumber(points)
662