Skip to content

Commit 2a33351

Browse files
authored
add support for weighted rules (#16)
* add weighted rule * parse weighted rule in DSL * parse weighted rules from FCL and matlab * rename FuzzyWeightedRule to WeightedFuzzyRule * test evaluation with weighted rules * update changelog
1 parent 0fd0eca commit 2a33351

File tree

11 files changed

+134
-37
lines changed

11 files changed

+134
-37
lines changed

docs/src/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
## Unreleased
6+
7+
- ![](https://img.shields.io/badge/new%20feature-green.svg) support for weighted rules.
8+
59
## v0.1.1 -- 2023-02-25
610

711
[view release on GitHub](https://github.com/lucaferranti/FuzzyLogic.jl/releases/tag/v0.1.1)

src/InferenceSystem.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,16 @@ fis(service=1, food=2)
5656
Base.@kwdef struct MamdaniFuzzySystem{And <: AbstractAnd, Or <: AbstractOr,
5757
Impl <: AbstractImplication,
5858
Aggr <: AbstractAggregator,
59-
Defuzz <: AbstractDefuzzifier} <:
60-
AbstractFuzzySystem
59+
Defuzz <: AbstractDefuzzifier,
60+
R <: AbstractRule} <: AbstractFuzzySystem
6161
"name of the system."
6262
name::Symbol
6363
"input variables and corresponding domain."
6464
inputs::Dictionary{Symbol, Variable} = Dictionary{Symbol, Variable}()
6565
"output variables and corresponding domain."
6666
outputs::Dictionary{Symbol, Variable} = Dictionary{Symbol, Variable}()
6767
"inference rules."
68-
rules::Vector{FuzzyRule} = FuzzyRule[]
68+
rules::Vector{R} = FuzzyRule[]
6969
"method used to compute conjuction in rules, default [`MinAnd`](@ref)."
7070
and::And = MinAnd()
7171
"method used to compute disjunction in rules, default [`MaxOr`](@ref)."
@@ -134,16 +134,16 @@ The inputs should be given as keyword arguments.
134134
135135
$(TYPEDFIELDS)
136136
"""
137-
Base.@kwdef struct SugenoFuzzySystem{And <: AbstractAnd, Or <: AbstractOr} <:
138-
AbstractFuzzySystem
137+
Base.@kwdef struct SugenoFuzzySystem{And <: AbstractAnd, Or <: AbstractOr,
138+
R <: AbstractRule} <: AbstractFuzzySystem
139139
"name of the system."
140140
name::Symbol
141141
"input variables and corresponding domain."
142142
inputs::Dictionary{Symbol, Variable} = Dictionary{Symbol, Variable}()
143143
"output variables and corresponding domain."
144144
outputs::Dictionary{Symbol, Variable} = Dictionary{Symbol, Variable}()
145145
"inference rules."
146-
rules::Vector{FuzzyRule} = FuzzyRule[]
146+
rules::Vector{R} = FuzzyRule[]
147147
"method used to compute conjuction in rules, default [`MinAnd`](@ref)."
148148
and::And = ProdAnd()
149149
"method used to compute disjunction in rules, default [`MaxOr`](@ref)."

src/evaluation.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function (fis::MamdaniFuzzySystem)(inputs::T) where {T <: NamedTuple}
3838
var = fis.outputs[con.subj]
3939
l, h = low(var.domain), high(var.domain)
4040
mf = map(var.mfs[con.prop], LinRange(l, h, Npoints))
41-
ruleres = broadcast(implication(fis), w, mf)
41+
ruleres = scale(broadcast(implication(fis), w, mf), rule)
4242
res[con.subj] = broadcast(fis.aggregator, res[con.subj], ruleres)
4343
end
4444
end
@@ -56,7 +56,7 @@ function (fis::SugenoFuzzySystem)(inputs::T) where {T <: NamedTuple}
5656
zeros(float(eltype(T)), length(fis.outputs)))
5757
weights_sum = zero(S)
5858
for rule in fis.rules
59-
w = rule.antecedent(fis, inputs)::S
59+
w = scale(rule.antecedent(fis, inputs), rule)::S
6060
weights_sum += w
6161
for con in rule.consequent
6262
res[con.subj] += w * memberships(fis.outputs[con.subj])[con.prop](inputs)

src/parser.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ end
192192

193193
function parse_body(body, argsin, argsout, type)
194194
opts = Expr[]
195-
rules = :(FuzzyRule[])
195+
rules = Expr(:vect)
196196
inputs = :(dictionary([]))
197197
outputs = :(dictionary([]))
198198
for line in body.args
@@ -212,6 +212,10 @@ function parse_body(body, argsin, argsout, type)
212212
var in SETTINGS[type] ||
213213
throw(ArgumentError("Invalid keyword $var in line $line"))
214214
push!(opts, Expr(:kw, var, value isa Symbol ? :($value()) : value))
215+
elseif @capture(line, ant_-->(cons__,) * w_Number)
216+
push!(rules.args, parse_rule(ant, cons, w))
217+
elseif @capture(line, ant_-->p_ == q_ * w_Number)
218+
push!(rules.args, parse_rule(ant, [:($p == $q)], w))
215219
elseif @capture(line, ant_-->(cons__,) | cons__)
216220
push!(rules.args, parse_rule(ant, cons))
217221
else
@@ -225,6 +229,10 @@ end
225229
# RULES PARSING #
226230
#################
227231

232+
function parse_rule(ant, cons, w)
233+
Expr(:call, :WeightedFuzzyRule, parse_antecedent(ant), parse_consequents(cons), w)
234+
end
235+
228236
function parse_rule(ant, cons)
229237
Expr(:call, :FuzzyRule, parse_antecedent(ant), parse_consequents(cons))
230238
end

src/parsers/fcl.jl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ function fcl_julia(s::AbstractString)
2323
haskey(FCL_JULIA, s) ? FCL_JULIA[s] : throw(ArgumentError("Option $s not supported."))
2424
end
2525

26+
function parse_rule(x)
27+
if isempty(x[5])
28+
FuzzyLogic.FuzzyRule(x[2], x[4])
29+
else
30+
FuzzyLogic.WeightedFuzzyRule(x[2], x[4], x[5][1][2])
31+
end
32+
end
33+
2634
@rule id = r"[a-zA-Z_]+[a-zA-z0-9_]*"p |> Symbol
2735
@rule function_block = r"FUNCTION_BLOCK"p & id & var_input_block & var_output_block &
2836
fuzzify_block[1:end] & defuzzify_block[1:end] & rule_block &
@@ -59,7 +67,7 @@ end
5967

6068
@rule rule_block = r"RULEBLOCK"p & id & operator_definition & activation_method[:?] &
6169
rule[1:end] & r"END_RULEBLOCK"p |>
62-
x -> (x[3], x[4], Vector{FuzzyLogic.FuzzyRule}(x[5]))
70+
x -> (x[3], x[4], identity.(x[5]))
6371

6472
@rule or_definition = r"OR"p & r":"p & (r"MAX"p, r"ASUM"p, r"BSUM"p) & r";"p
6573
@rule and_definition = r"AND"p & r":"p & (r"MIN"p, r"PROD"p, r"BDIF"p) & r";"p
@@ -68,8 +76,9 @@ end
6876
@rule activation_method = r"ACT"p & r":"p & (r"MIN"p, r"PROD"p) & r";"p |>
6977
x -> fcl_julia(join([x[1], x[3]]))
7078

71-
@rule rule = r"RULE\s+\d+\s*:\s*IF"p & condition & r"THEN"p & conclusion & r";"p |>
72-
x -> FuzzyLogic.FuzzyRule(x[2], x[4])
79+
@rule rule = r"RULE\s+\d+\s*:\s*IF"p & condition & r"THEN"p & conclusion &
80+
(r"WITH"p & numeral)[:?] & r";"p |> parse_rule
81+
7382
@rule relation = negrel, posrel
7483
@rule posrel = id & r"IS"p & id |> x -> FuzzyLogic.FuzzyRelation(x[1], x[3])
7584
@rule negrel = (id & r"IS\s+NOT"p & id |> x -> FuzzyLogic.FuzzyNegation(x[1], x[3])),
@@ -97,7 +106,6 @@ Parse a fuzzy inference system from a string representation in Fuzzy Control Lan
97106
98107
The parsers can read FCL comformant to IEC 1131-7, with the following remarks:
99108
100-
- Weighted rules are not supported.
101109
- Sugeno (system with singleton outputs) shall use COGS as defuzzifier.
102110
- the `RANGE` keyword is required for both fuzzification and defuzzification blocks.
103111
- Only the required `MAX` accumulator is supported.

src/parsers/matlab_fis.jl

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module MatlabParser
33
using Dictionaries
44
using ..FuzzyLogic
55
using ..FuzzyLogic: FuzzyAnd, FuzzyOr, FuzzyRule, FuzzyRelation, FuzzyNegation, Domain,
6-
Variable, memberships, AbstractMembershipFunction
6+
WeightedFuzzyRule, Variable, memberships, AbstractMembershipFunction
77

88
export parse_matlabfis, @matlabfis_str
99

@@ -58,10 +58,12 @@ function parse_var(var; inputs = nothing)
5858
end
5959

6060
function parse_rule(line, inputnames, outputnames, inputmfs, outputmfs)
61-
ants, cons, op = split(line, r"[,:] ")
61+
ants, consw, op = split(line, r"[,:] ")
62+
consw = split(consw)
63+
considx = parse.(Int, consw[1:length(outputnames)])
64+
w = parse(Float64, consw[end][2:(end - 1)])
6265
antsidx = filter!(!iszero, parse.(Int, split(ants)))
63-
considx = filter!(!iszero, parse.(Int, split(cons)[1:length(outputnames)]))
64-
# TODO: weighted rules
66+
filter!(!iszero, considx)
6567
op = op == "1" ? FuzzyAnd : FuzzyOr
6668
length(antsidx) == 1 && (op = identity)
6769
ant = mapreduce(op, enumerate(antsidx)) do (var, mf)
@@ -74,16 +76,20 @@ function parse_rule(line, inputnames, outputnames, inputmfs, outputmfs)
7476
con = map(enumerate(considx)) do (var, mf)
7577
FuzzyRelation(outputnames[var], outputmfs[var][mf])
7678
end
77-
FuzzyRule(ant, con)
79+
if isone(w)
80+
FuzzyRule(ant, con)
81+
else
82+
WeightedFuzzyRule(ant, con, w)
83+
end
7884
end
7985

8086
function parse_rules(lines, inputs, outputs)
8187
inputnames = collect(keys(inputs))
8288
outputnames = collect(keys(outputs))
8389
inputmfs = collect.(keys.(memberships.(collect(inputs))))
8490
outputmfs = collect.(keys.(memberships.(collect(outputs))))
85-
FuzzyRule[parse_rule(line, inputnames, outputnames, inputmfs, outputmfs)
86-
for line in lines]
91+
identity.([parse_rule(line, inputnames, outputnames, inputmfs, outputmfs)
92+
for line in lines])
8793
end
8894

8995
"""

src/rules.jl

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,39 @@ struct FuzzyOr{T <: AbstractFuzzyProposition, S <: AbstractFuzzyProposition} <:
4848
end
4949
Base.show(io::IO, fo::FuzzyOr) = print(io, '(', fo.left, "", fo.right, ')')
5050

51+
abstract type AbstractRule end
52+
5153
"""
5254
Describes a fuzzy implication rule IF antecedent THEN consequent.
5355
"""
54-
struct FuzzyRule{T <: AbstractFuzzyProposition}
56+
struct FuzzyRule{T <: AbstractFuzzyProposition} <: AbstractRule
5557
"premise of the inference rule."
5658
antecedent::T
57-
"consequences of the premise rule."
59+
"consequences of the inference rule."
5860
consequent::Vector{FuzzyRelation}
5961
end
6062
Base.show(io::IO, r::FuzzyRule) = print(io, r.antecedent, " --> ", r.consequent...)
6163

64+
"""
65+
Weighted fuzzy rule. In Mamdani systems, the result of implication is scaled by the weight.
66+
In Sugeno systems, the result of the antecedent is scaled by the weight.
67+
"""
68+
struct WeightedFuzzyRule{T <: AbstractFuzzyProposition, S <: Real} <: AbstractRule
69+
"premise of the inference rule."
70+
antecedent::T
71+
"consequences of the inference rule."
72+
consequent::Vector{FuzzyRelation}
73+
"weight of the rule."
74+
weight::S
75+
end
76+
77+
function Base.show(io::IO, r::WeightedFuzzyRule)
78+
print(io, r.antecedent, " --> ", r.consequent..., " (", r.weight, ")")
79+
end
80+
81+
@inline scale(w, ::FuzzyRule) = w
82+
@inline scale(w, r::WeightedFuzzyRule) = w * r.weight
83+
6284
# comparisons (for testing)
6385

6486
Base.:(==)(r1::FuzzyRelation, r2::FuzzyRelation) = r1.subj == r2.subj && r1.prop == r2.prop
@@ -73,6 +95,11 @@ function Base.:(==)(r1::FuzzyRule, r2::FuzzyRule)
7395
r1.antecedent == r2.antecedent && r1.consequent == r1.consequent
7496
end
7597

98+
function Base.:(==)(r1::WeightedFuzzyRule, r2::WeightedFuzzyRule)
99+
r1.antecedent == r2.antecedent && r1.consequent == r1.consequent &&
100+
r1.weight == r2.weight
101+
end
102+
76103
# utilities
77104
leaves(fr::Union{FuzzyNegation, FuzzyRelation}) = (fr,)
78105
leaves(fp::AbstractFuzzyProposition) = [leaves(fp.left)..., leaves(fp.right)...]

test/test_evaluation.jl

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ using FuzzyLogic, Test
3131
@test fis(service = 3, food = 5)[:tip]12.21 atol=1e-2
3232
@test fis(service = 2, food = 7)[:tip]7.79 atol=1e-2
3333
@test fis(service = 3, food = 1)[:tip]8.95 atol=1e-2
34+
35+
# with weighted rule
36+
fis = @mamfis function tipper(service, food)::tip
37+
service := begin
38+
domain = 0:10
39+
poor = GaussianMF(0.0, 1.5)
40+
good = GaussianMF(5.0, 1.5)
41+
excellent = GaussianMF(10.0, 1.5)
42+
end
43+
44+
food := begin
45+
domain = 0:10
46+
rancid = TrapezoidalMF(-2, 0, 1, 3)
47+
delicious = TrapezoidalMF(7, 9, 10, 12)
48+
end
49+
50+
tip := begin
51+
domain = 0:30
52+
cheap = TriangularMF(0, 5, 10)
53+
average = TriangularMF(10, 15, 20)
54+
generous = TriangularMF(20, 25, 30)
55+
end
56+
57+
service == poor || food == rancid --> tip == cheap * 0.5
58+
service == good --> tip == average
59+
service == excellent || food == delicious --> tip == generous
60+
end
61+
62+
@test fis(service = 2, food = 7)[:tip]9.36 atol=1e-2
3463
end
3564

3665
@testset "test Sugeno evaluation" begin
@@ -55,11 +84,11 @@ end
5584
generous = 24.998
5685
end
5786

58-
service == poor || food == rancid --> tip == cheap
87+
service == poor || food == rancid --> tip == cheap * 0.5
5988
service == good --> tip == average
6089
service == excellent || food == delicious --> tip == generous
6190
end
62-
@test fis(service = 2, food = 3)[:tip]7.478 atol=1e-3
91+
@test fis(service = 2, food = 3)[:tip]8.97 atol=5e-3
6392

6493
fis2 = @sugfis function tipper(service, food)::tip
6594
service := begin

test/test_parser.jl

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Dictionaries, FuzzyLogic, Test
2-
using FuzzyLogic: FuzzyRelation, FuzzyAnd, FuzzyOr, FuzzyRule, Domain, Variable
2+
using FuzzyLogic: FuzzyRelation, FuzzyAnd, FuzzyOr, FuzzyRule, WeightedFuzzyRule, Domain,
3+
Variable
34

45
# TODO: write more low level tests
56

@@ -29,9 +30,10 @@ using FuzzyLogic: FuzzyRelation, FuzzyAnd, FuzzyOr, FuzzyRule, Domain, Variable
2930
or = ProbSumOr
3031
implication = ProdImplication
3132

32-
service == poor && food == rancid --> tip == cheap
33-
service == good --> tip == average
33+
service == poor && food == rancid --> tip == cheap * 0.2
34+
service == good --> (tip == average, tip == average) * 0.3
3435
service == excellent || food == delicious --> tip == generous
36+
service == excellent || food == delicious --> (tip == generous, tip == generous)
3537

3638
aggregator = ProbSumAggregator
3739
defuzzifier = BisectorDefuzzifier
@@ -64,12 +66,18 @@ using FuzzyLogic: FuzzyRelation, FuzzyAnd, FuzzyOr, FuzzyRule, Domain, Variable
6466

6567
@test fis.rules ==
6668
[
67-
FuzzyRule(FuzzyAnd(FuzzyRelation(:service, :poor), FuzzyRelation(:food, :rancid)),
68-
[FuzzyRelation(:tip, :cheap)]),
69-
FuzzyRule(FuzzyRelation(:service, :good), [FuzzyRelation(:tip, :average)]),
69+
WeightedFuzzyRule(FuzzyAnd(FuzzyRelation(:service, :poor),
70+
FuzzyRelation(:food, :rancid)),
71+
[FuzzyRelation(:tip, :cheap)], 0.2),
72+
WeightedFuzzyRule(FuzzyRelation(:service, :good),
73+
[FuzzyRelation(:tip, :average), FuzzyRelation(:tip, :average)],
74+
0.3),
7075
FuzzyRule(FuzzyOr(FuzzyRelation(:service, :excellent),
7176
FuzzyRelation(:food, :delicious)),
7277
[FuzzyRelation(:tip, :generous)]),
78+
FuzzyRule(FuzzyOr(FuzzyRelation(:service, :excellent),
79+
FuzzyRelation(:food, :delicious)),
80+
[FuzzyRelation(:tip, :generous), FuzzyRelation(:tip, :generous)]),
7381
]
7482
end
7583

test/test_parsers/test_fcl.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Dictionaries, FuzzyLogic, PEG, Test
22
using FuzzyLogic: Variable, Domain, FuzzyRelation, FuzzyAnd, FuzzyOr, FuzzyNegation,
3-
FuzzyRule
3+
FuzzyRule, WeightedFuzzyRule
44

55
@testset "parse propositions" begin
66
s = [
@@ -144,7 +144,7 @@ end
144144
RULEBLOCK rules
145145
OR: ASUM;
146146
ACT: PROD;
147-
RULE 1: IF Ix IS zero AND Iy IS zero THEN Iout IS white;
147+
RULE 1: IF Ix IS zero AND Iy IS zero THEN Iout IS white WITH 0.5;
148148
RULE 2: IF Ix IS NOT zero OR Iy IS NOT zero THEN Iout IS black;
149149
END_RULEBLOCK
150150
@@ -167,8 +167,8 @@ end
167167
]))
168168

169169
@test fis.rules == [
170-
FuzzyRule(FuzzyAnd(FuzzyRelation(:Ix, :zero), FuzzyRelation(:Iy, :zero)),
171-
[FuzzyRelation(:Iout, :white)]),
170+
WeightedFuzzyRule(FuzzyAnd(FuzzyRelation(:Ix, :zero), FuzzyRelation(:Iy, :zero)),
171+
[FuzzyRelation(:Iout, :white)], 0.5),
172172
FuzzyRule(FuzzyOr(FuzzyNegation(:Ix, :zero), FuzzyNegation(:Iy, :zero)),
173173
[FuzzyRelation(:Iout, :black)]),
174174
]

0 commit comments

Comments
 (0)