Skip to content

Commit

Permalink
Begin uploading code
Browse files Browse the repository at this point in the history
  • Loading branch information
johnHostetter committed Aug 15, 2024
1 parent d753ee7 commit 8570491
Show file tree
Hide file tree
Showing 39 changed files with 4,128 additions and 0 deletions.
Empty file added examples/__init__.py
Empty file.
Empty file added examples/discrete/__init__.py
Empty file.
69 changes: 69 additions & 0 deletions examples/discrete/age.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Demo of working with discrete fuzzy sets for a toy task regarding age.
"""

from sympy import Symbol, Interval, oo # oo is infinity

from fuzzy.relations.discrete.snorm import StandardUnion
from fuzzy.relations.discrete.tnorm import StandardIntersection
from fuzzy.relations.discrete.complement import standard_complement
from fuzzy.sets.discrete import DiscreteFuzzySet, FuzzyVariable


def a_1():
"""
A sample construction of a Fuzzy Set called 'A1'.
"""
formulas = []
element = Symbol("x")
formulas.append((1, Interval.Lopen(-oo, 20)))
formulas.append(((35 - element) / 15, Interval.open(20, 35)))
formulas.append((0, Interval.Ropen(35, oo)))
return DiscreteFuzzySet(formulas, "A1")


def a_2():
"""
A sample construction of a Fuzzy Set called 'A2'.
"""
formulas = []
element = Symbol("x")
formulas.append((0, Interval.Lopen(-oo, 20)))
formulas.append(((element - 20) / 15, Interval.open(20, 35)))
formulas.append((1, Interval(35, 45)))
formulas.append(((60 - element) / 15, Interval.open(45, 60)))
formulas.append((0, Interval.Ropen(60, oo)))
return DiscreteFuzzySet(formulas, "A2")


def a_3():
"""
A sample construction of a Fuzzy Set called 'A3'.
"""
formulas = []
element = Symbol("x")
formulas.append((0, Interval.Lopen(-oo, 45)))
formulas.append(((element - 45) / 15, Interval.open(45, 60)))
formulas.append((1, Interval.Ropen(60, oo)))
return DiscreteFuzzySet(formulas, "A3")


a1 = a_1()
a2 = a_2()
a3 = a_3()

FuzzyVariable([a1, a2, a3], "Age").plot(0, 80)
b = StandardIntersection([a1, a2], "B")
b.plot(0, 80)
c = StandardIntersection([a2, a3], "C")
c.plot(0, 80)
StandardUnion([b, c], "B Union C").plot(0, 80)
standard_complement(a1)
a1.plot(0, 80)
standard_complement(a3)
a3.plot(0, 80)
StandardIntersection([a1, a3], "Not(A1) Intersection Not(A3)").plot(0, 80)
standard_complement(a1)
standard_complement(a3)
# doesn't work yet
# StandardComplement(StandardUnion([b, c], 'Not (B Union C)')).graph(0, 80)
119 changes: 119 additions & 0 deletions examples/discrete/student.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Demo of working with discrete fuzzy sets for a toy task regarding knowledge of material.
"""

from sympy import Symbol, Interval, oo # oo is infinity

from fuzzy.relations.discrete.tnorm import StandardIntersection
from fuzzy.relations.discrete.extension import AlphaCut, SpecialFuzzySet
from fuzzy.sets.discrete import DiscreteFuzzySet, FuzzyVariable


# https://www-sciencedirect-com.prox.lib.ncsu.edu/science/article/pii/S0957417412008056


def unknown():
"""
Create a fuzzy set for the linguistic term 'unknown'.
Returns:
OrdinaryDiscreteFuzzySet
"""
formulas = []
element = Symbol("x")
formulas.append((1, Interval.Lopen(-oo, 55)))
formulas.append((1 - (element - 55) / 5, Interval.open(55, 60)))
formulas.append((0, Interval.Ropen(60, oo)))
return DiscreteFuzzySet(formulas, "Unknown")


def known():
"""
Create a fuzzy set for the linguistic term 'known'.
Returns:
OrdinaryDiscreteFuzzySet
"""
formulas = []
element = Symbol("x")
formulas.append(((element - 70) / 5, Interval.open(70, 75)))
formulas.append((1, Interval(75, 85)))
formulas.append((1 - (element - 85) / 5, Interval.open(85, 90)))
formulas.append((0, Interval.Lopen(-oo, 70)))
formulas.append((0, Interval.Ropen(90, oo)))
return DiscreteFuzzySet(formulas, "Known")


def unsatisfactory_unknown():
"""
Create a fuzzy set for the linguistic term 'unsatisfactory unknown'.
Returns:
OrdinaryDiscreteFuzzySet
"""
formulas = []
element = Symbol("x")
formulas.append(((element - 55) / 5, Interval.open(55, 60)))
formulas.append((1, Interval(60, 70)))
formulas.append((1 - (element - 70) / 5, Interval.open(70, 75)))
formulas.append((0, Interval.Lopen(-oo, 55)))
formulas.append((0, Interval.Ropen(75, oo)))
return DiscreteFuzzySet(formulas, "Unsatisfactory Unknown")


def learned():
"""
Create a fuzzy set for the linguistic term 'learned'.
Returns:
OrdinaryDiscreteFuzzySet
"""
formulas = []
element = Symbol("x")
formulas.append(((element - 85) / 5, Interval.open(85, 90)))
formulas.append((1, Interval(90, 100)))
formulas.append((0, Interval.Lopen(-oo, 85)))
return DiscreteFuzzySet(formulas, "Learned")


if __name__ == "__main__":
terms = [unknown(), known(), unsatisfactory_unknown(), learned()]

fuzzy_variable = FuzzyVariable(fuzzy_sets=terms, name="Student Knowledge")
fig, _ = fuzzy_variable.plot(samples=150)
fig.show()

# --- DEMO --- Classify 'element'

example_element_membership = fuzzy_variable.degree(element=73)

alpha_cut = AlphaCut(known(), 0.6, "AlphaCut")
fig, _ = alpha_cut.plot(samples=250)
fig.show()
special_fuzzy_set = SpecialFuzzySet(known(), 0.5, "Special")
fig, _ = special_fuzzy_set.plot()
fig.show()

alpha_cuts = []
idx, MAX_HEIGHT, IDX_OF_MAX = 0, 0, 0
for idx, membership_to_fuzzy_term in enumerate(example_element_membership):
if example_element_membership[idx] > 0:
special_fuzzy_set = SpecialFuzzySet(
terms[idx], membership_to_fuzzy_term, terms[idx].name
)
alpha_cuts.append(
StandardIntersection(
[special_fuzzy_set, terms[idx]], name=f"A{idx + 1}"
)
)

# maximum membership principle
if membership_to_fuzzy_term > MAX_HEIGHT:
MAX_HEIGHT, IDX_OF_MAX = membership_to_fuzzy_term, idx

confluence: FuzzyVariable = FuzzyVariable(fuzzy_sets=alpha_cuts, name="Confluence")
fig, _ = confluence.plot()
fig.show()

# maximum membership principle
print(f"Maximum Membership Principle: {terms[idx].name}")
43 changes: 43 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "fuzzy-theory"
version = "0.0.1"
authors = [
{ name="John Wesley Hostetter", email="[email protected]" },
]
description = "The fuzzy-theory library provides a PyTorch interface to fuzzy set theory and fuzzy logic operations."
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]

[project.urls]
Homepage = "https://github.com/johnHostetter/fuzzy-theory"
Issues = "https://github.com/johnHostetter/fuzzy-theory/issues"

[tool.hatch.build]
include = [
"src/fuzzy/**",
"README.md",
"LICENSE",
]
exclude = [
"tests/**",
"*.pyc",
"*.pyo",
".git/**",
"build/**",
"dist/**",
".venv/**",
]
# Ignore VCS
ignore = ["*.git", "*.hg", ".git/**", ".hg/**"]

[tool.hatch.build.targets.wheel]
packages = ["src/fuzzy"]
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
numpy==1.26.4 # new is 2.0.1 (update when possible - tests fail otherwise)
sympy==1.13.2 # for torch
torch==2.4.0
torchquad==0.4.0
matplotlib==3.9.1 # for torchquad
SciencePlots==2.1.1 # for scientific publication plots
natsort==8.4.0 # for natural sorting
Empty file added src/fuzzy/__init__.py
Empty file.
Empty file added src/fuzzy/relations/__init__.py
Empty file.
Empty file.
77 changes: 77 additions & 0 deletions src/fuzzy/relations/continuous/aggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Implements aggregation operators in fuzzy theory.
"""

import torch


class OrderedWeightedAveraging(torch.nn.Module):
"""
Yager's On Ordered Weighted Averaging Aggregation Operators in
Multicriteria Decisionmaking (1988)
An operator that lies between the 'anding' or the 'oring' of multiple criteria.
The weight vector allows us to easily adjust the degree of 'anding' and 'oring'
implicit in the aggregation.
"""

def __init__(self, in_features, weights):
super().__init__()
self.in_features = in_features
if self.in_features != len(weights):
raise AttributeError(
"The number of input features expected in the Ordered Weighted Averaging operator "
"is expected to equal the number of elements in the weight vector."
)
with torch.no_grad():
if weights.sum() == 1.0:
self.weights = torch.nn.parameter.Parameter(torch.abs(weights))
else:
raise AttributeError(
"The weight vector of the Ordered Weighted Averaging operator must sum to 1.0."
)

def orness(self):
"""
A degree of 1 means the OWA operator is the 'or' operator,
and this occurs when the first element of the weight vector is equal to 1
and all other elements in the weight vector are zero.
Returns:
The degree to which the Ordered Weighted Averaging operator is an 'or' operator.
"""
return (1 / (self.in_features - 1)) * torch.tensor(
[
(self.in_features - i) * self.weights[i - 1]
for i in range(1, self.in_features + 1)
]
).sum()

def dispersion(self):
"""
The measure of dispersion; essentially, it is a measure of entropy that is related to the
Shannon information concept. The more disperse the weight vector, the more information
is being used in the aggregation of the aggregate value.
Returns:
The amount of dispersion in the weight vector.
"""
# there is exactly one entry where it is equal to one
if len(torch.where(self.weights == 1.0)[0]) == 1:
return torch.zeros(1)
return -1 * (self.weights * torch.log(self.weights)).sum()

def forward(self, input_observation):
"""
Applies the Ordered Weighted Averaging operator. First, it will sort the argument
in descending order, then multiply by the weight vector, and finally sum over the entries.
Args:
input_observation: Argument vector, unordered.
Returns:
The aggregation of the ordered argument vector with the weight vector.
"""
# namedtuple with 'values' and 'indices' properties
ordered_argument_vector = torch.sort(input_observation, descending=True)
return (self.weights * ordered_argument_vector.values).sum()
68 changes: 68 additions & 0 deletions src/fuzzy/relations/continuous/tnorm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Implements the t-norm fuzzy relations.
"""

from enum import Enum

import torch


class TNorm(Enum):
"""
Enumerates the types of t-norms.
"""

PRODUCT = "product" # i.e., algebraic product
MINIMUM = "minimum"
ACZEL_ALSINA = "aczel_alsina" # not yet implemented
SOFTMAX_SUM = "softmax_sum"
SOFTMAX_MEAN = "softmax_mean"
LUKASIEWICZ = "generalized_lukasiewicz"
# the following are to be implemented
DRASTIC = "drastic"
NILPOTENT = "nilpotent"
HAMACHER = "hamacher"
EINSTEIN = "einstein"
YAGER = "yager"
DUBOIS = "dubois"
DIF = "dif"


class AlgebraicProduct(torch.nn.Module): # TODO: remove this class
"""
Implementation of the Algebraic Product t-norm (Fuzzy AND).
"""

def __init__(self, in_features=None, importance=None):
"""
Initialization.
INPUT:
- in_features: shape of the input
- centers: trainable parameter
- sigmas: trainable parameter
importance is initialized to a one vector by default
"""
super().__init__()
self.in_features = in_features

# initialize antecedent importance
if importance is None:
self.importance = torch.nn.parameter.Parameter(torch.tensor(1.0))
self.importance.requires_grad = False
else:
if not isinstance(importance, torch.Tensor):
importance = torch.Tensor(importance)
self.importance = torch.nn.parameter.Parameter(
torch.abs(importance)
) # importance can only be [0, 1]
self.importance.requires_grad = True

def forward(self, elements):
"""
Forward pass of the function.
Applies the function to the input elementwise.
"""
self.importance = torch.nn.parameter.Parameter(
torch.abs(self.importance)
) # importance can only be [0, 1]
return torch.prod(torch.mul(elements, self.importance))
Empty file.
Loading

0 comments on commit 8570491

Please sign in to comment.