Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ name: Build

on:
push:
branches: [main]
branches: [ main ]
pull_request:
branches: [main, feature/*]
branches: [ main, feature/* ]
schedule: # monthly, first day
- cron: 0 0 1 * *

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9"] #, "3.7", "3.8", "3.9", "3.10"]
python-version: [ "3.9" ] #, "3.7", "3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v4
Expand All @@ -26,7 +28,10 @@ jobs:
run: pip install -r requirements-dev.txt

- name: Check
run: nox -e check
run: |
nox --version
poetry --version
nox -e check

- name: Install
run: nox -e install
Expand All @@ -45,6 +50,8 @@ jobs:

- name: Coverage
uses: coverallsapp/github-action@v2
with:
fail-on-error: false

- name: Benchmark
uses: CodSpeedHQ/action@v3
Expand Down
3 changes: 1 addition & 2 deletions fuzzylite/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ def defuzzify(self) -> None:
f"expected a defuzzifier in output variable '{self.name}', but found None"
)
# value at t+1
value = self.defuzzifier.defuzzify(self.fuzzy, self.minimum, self.maximum)
value = np.atleast_1d(self.defuzzifier.defuzzify(self.fuzzy, self.minimum, self.maximum))

# previous value is the last element of the value at t
self.previous_value = np.take(self.value, -1).astype(float)
Expand All @@ -532,7 +532,6 @@ def defuzzify(self) -> None:

# Applying default values
if not np.isnan(self.default_value):
value = np.atleast_1d(value)
value[np.isnan(value)] = self.default_value

# Committing the value
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ mkdocs-material = "^9.1.21" # Documentation with material theme for mkdocs
mkdocstrings = {extras = ["python"], version = "^0.26.1"}
mkdocstrings-python = "^1.11.1"
mypy = "^1.10.0" # Static code analysis
poetry-bumpversion = "^0.3.0" # Version management
# poetry-bumpversion = "^0.3.0" # Version management; manual installation to avoid modifying poetry
pymarkdownlnt = "^0.9.12" # Markdown linter
pyright = "^1.1.362" # Static code analysis
pytest = "^7.3.1" # Test driven development
Expand Down
106 changes: 102 additions & 4 deletions tests/test_issues.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import fuzzylite as fl
import unittest

import numpy as np

import fuzzylite as fl


class TestIssue91(unittest.TestCase):
"""
Test case for https://github.com/fuzzylite/pyfuzzylite/issues/91
"""
"""Test case for https://github.com/fuzzylite/pyfuzzylite/issues/91"""

def engine(self) -> fl.Engine:
fll = """
Expand Down Expand Up @@ -84,3 +84,101 @@ def test_issue_91(self) -> None:
engine.process()
np.testing.assert_almost_equal(engine.variable("mTip").value, 25.00104979)
np.testing.assert_almost_equal(engine.variable("tsTip").value, 25)

def test_issue_92(self) -> None:
class Approximation:
def __init__(self) -> None:
self.engine = fl.Engine(
name="approximation",
input_variables=[
fl.InputVariable(
name="inputX",
minimum=0.0,
maximum=10.0,
lock_range=False,
terms=[
fl.Triangle("NEAR_1", 0.0, 1.0, 2.0),
fl.Triangle("NEAR_2", 1.0, 2.0, 3.0),
fl.Triangle("NEAR_3", 2.0, 3.0, 4.0),
fl.Triangle("NEAR_4", 3.0, 4.0, 5.0),
fl.Triangle("NEAR_5", 4.0, 5.0, 6.0),
fl.Triangle("NEAR_6", 5.0, 6.0, 7.0),
fl.Triangle("NEAR_7", 6.0, 7.0, 8.0),
fl.Triangle("NEAR_8", 7.0, 8.0, 9.0),
fl.Triangle("NEAR_9", 8.0, 9.0, 10.0),
],
)
],
output_variables=[
fl.OutputVariable(
name="outputFx",
minimum=-1.0,
maximum=1.0,
lock_range=False,
lock_previous=True,
default_value=fl.nan,
aggregation=None,
defuzzifier=fl.WeightedAverage(type="TakagiSugeno"),
terms=[
fl.Constant("f1", 0.84),
fl.Constant("f2", 0.45),
fl.Constant("f3", 0.04),
fl.Constant("f4", -0.18),
fl.Constant("f5", -0.19),
fl.Constant("f6", -0.04),
fl.Constant("f7", 0.09),
fl.Constant("f8", 0.12),
fl.Constant("f9", 0.04),
],
),
fl.OutputVariable(
name="trueFx",
minimum=-1.0,
maximum=1.0,
lock_range=False,
lock_previous=True,
default_value=fl.nan,
aggregation=None,
defuzzifier=fl.WeightedAverage(),
terms=[fl.Function("fx", "sin(inputX)/inputX")],
),
fl.OutputVariable(
name="diffFx",
minimum=-1.0,
maximum=1.0,
lock_range=False,
lock_previous=False,
default_value=fl.nan,
aggregation=None,
defuzzifier=fl.WeightedAverage(),
terms=[fl.Function("diff", "fabs(outputFx-trueFx)")],
),
],
rule_blocks=[
fl.RuleBlock(
name="",
conjunction=None,
disjunction=None,
implication=None,
activation=fl.General(),
rules=[
fl.Rule.create("if inputX is NEAR_1 then outputFx is f1"),
fl.Rule.create("if inputX is NEAR_2 then outputFx is f2"),
fl.Rule.create("if inputX is NEAR_3 then outputFx is f3"),
fl.Rule.create("if inputX is NEAR_4 then outputFx is f4"),
fl.Rule.create("if inputX is NEAR_5 then outputFx is f5"),
fl.Rule.create("if inputX is NEAR_6 then outputFx is f6"),
fl.Rule.create("if inputX is NEAR_7 then outputFx is f7"),
fl.Rule.create("if inputX is NEAR_8 then outputFx is f8"),
fl.Rule.create("if inputX is NEAR_9 then outputFx is f9"),
fl.Rule.create(
"if inputX is any then trueFx is fx and diffFx is diff"
),
],
)
],
)

test = Approximation()
test.engine.input_variable("inputX").value = 5.0
test.engine.process()
129 changes: 100 additions & 29 deletions tests/test_vectorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,37 @@
class AssertIntegration:
"""Asserts integration of a Mamdani or Takagi-Sugeno system with a defuzzifier."""

def __init__(self, engine: fl.Engine, vectorize: bool = True) -> None:
def __init__(self, engine: fl.Engine) -> None:
"""Constructor.

@param engine is the engine to test on.
@param vectorize is whether to test vectorization.
"""
self.engine = copy.deepcopy(engine)
self.vectorize = vectorize

def assert_that(self, defuzzifier: fl.Defuzzifier, input_expected: dict[float, float]) -> None:
def assert_that(
self,
defuzzifier: fl.Defuzzifier,
input_expected: dict[float, float] | list[tuple[float, float]],
) -> None:
"""Asserts integration of a Mamdani or Takagi-Sugeno system with a defuzzifier."""
for output in self.engine.output_variables:
output.defuzzifier = defuzzifier
if isinstance(input_expected, dict):
input_expected = list(input_expected.items())
inputs = fl.array([x[0] for x in input_expected])
expected_outputs = fl.array([x[1] for x in input_expected])

self.engine.restart()
for input, expected_output in input_expected.items():
self.engine.input_variables[0].value = input
self.engine.process()
obtained = self.engine.output_variables[0].value
np.testing.assert_allclose(
obtained,
expected_output,
err_msg=f"{fl.Op.class_name(defuzzifier)}({input}) = {obtained}, but expected {expected_output}",
atol=fl.settings.atol,
rtol=fl.settings.rtol,
)
if self.vectorize:
self.engine.restart()
inputs = fl.array([x for x in input_expected])
expected_outputs = fl.array([x for x in input_expected.values()])
self.engine.input_variables[0].value = inputs
self.engine.process()
obtained = self.engine.output_variables[0].value
np.testing.assert_allclose(
obtained,
expected_outputs,
err_msg=f"{fl.Op.class_name(defuzzifier)}([{inputs}]) = {obtained}, but expected {expected_outputs}",
atol=fl.settings.atol,
rtol=fl.settings.rtol,
)
self.engine.input_variables[0].value = inputs
self.engine.process()
obtained = self.engine.output_variables[0].value
np.testing.assert_allclose(
obtained,
expected_outputs,
err_msg=f"{fl.Op.class_name(defuzzifier)}([{inputs}]) = {obtained}, but expected {expected_outputs}",
atol=fl.settings.atol,
rtol=fl.settings.rtol,
)


class TestMamdani(unittest.TestCase):
Expand All @@ -84,6 +75,46 @@ def test_simple_mamdani_bisector_integration(self) -> None:
},
)

def test_simple_mamdani_bisector_integration_with_locking(self) -> None:
"""Test a simple integration with Bisector."""
engine = mamdani.simple_dimmer.SimpleDimmer().engine
for output_variable in engine.output_variables:
output_variable.lock_previous = True
output_variable.default_value = 0.2468
output_variable.value = np.array([1.234, 9.876])

AssertIntegration(engine).assert_that(
fl.Bisector(),
[
# replace first nans with last value
(0.0, 9.876), # (0.0, fl.nan),
(1.0, 9.876), # (1.0, fl.nan),
(0.25, 0.75),
(0.375, 0.625),
(0.5, 0.5),
# replace nans with previous values
(0.0, 0.5), # (0.0, fl.nan),
(1.0, 0.5), # (1.0, fl.nan),
],
)

engine.restart()

AssertIntegration(engine).assert_that(
fl.Bisector(),
[
# replace first nans default value
(0.0, 0.2468), # 0.0: fl.nan,
(1.0, 0.2468), # 1.0: fl.nan,
(0.25, 0.75),
(0.375, 0.625),
(0.5, 0.5),
# replace nans with previous values
(0.0, 0.5), # (0.0, fl.nan),
(1.0, 0.5), # (1.0, fl.nan),
],
)

def test_simple_mamdani_centroid_integration(self) -> None:
"""Test a simple integration with Centroid."""
AssertIntegration(mamdani.simple_dimmer.SimpleDimmer().engine).assert_that(
Expand Down Expand Up @@ -168,6 +199,46 @@ def test_simple_takagisugeno_avg_integration(self) -> None:
},
)

def test_simple_takagisugeno_avg_integration_locking_previous(self) -> None:
"""Test a simple integration with WeightedAverage."""
engine = takagi_sugeno.simple_dimmer.SimpleDimmer().engine
for output_variable in engine.output_variables:
output_variable.lock_previous = True
output_variable.default_value = 0.2468
output_variable.value = np.array([1.234, 9.876])

AssertIntegration(engine).assert_that(
fl.WeightedAverage(),
[
# replace first nans with last value
(0.0, 9.876), # 0.0: fl.nan,
(1.0, 9.876), # 1.0: fl.nan,
(0.25, 0.75),
(0.375, 0.625),
(0.5, 0.5),
# replace nans with previous values
(0.0, 0.5),
(1.0, 0.5),
],
)

engine.restart()

AssertIntegration(engine).assert_that(
fl.WeightedAverage(),
[
# replace first nans with default value
(0.0, 0.2468), # 0.0: fl.nan,
(1.0, 0.2468), # 1.0: fl.nan,
(0.25, 0.75),
(0.375, 0.625),
(0.5, 0.5),
# replace nans with previous values
(0.0, 0.5),
(1.0, 0.5),
],
)

def test_simple_takagisugeno_sum_integration(self) -> None:
"""Test a simple integration with WeightedSum."""
AssertIntegration(takagi_sugeno.simple_dimmer.SimpleDimmer().engine).assert_that(
Expand Down