Skip to content

Commit d16a75c

Browse files
authored
vector notation (#17)
* allow to pass vectors as inputs to fis * support vector notation for variables * for loops * update changelog
1 parent 2a33351 commit d16a75c

File tree

6 files changed

+127
-28
lines changed

6 files changed

+127
-28
lines changed

docs/src/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
## Unreleased
66

77
- ![](https://img.shields.io/badge/new%20feature-green.svg) support for weighted rules.
8+
- ![](https://img.shields.io/badge/new%20feature-green.svg) allow to specify input and output variables as vectors (e.g. `x[1:10]`) and support for loops to avoid repetitive code.
89

910
## v0.1.1 -- 2023-02-25
1011

src/evaluation.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ function (fis::MamdaniFuzzySystem)(inputs::T) where {T <: NamedTuple}
4949
end
5050

5151
(fis::MamdaniFuzzySystem)(; inputs...) = fis(values(inputs))
52+
@inline function (fis::MamdaniFuzzySystem)(inputs::Union{AbstractVector, Tuple})
53+
fis((; zip(collect(keys(fis.inputs)), inputs)...))
54+
end
5255

5356
function (fis::SugenoFuzzySystem)(inputs::T) where {T <: NamedTuple}
5457
S = float(eltype(T))
@@ -66,3 +69,6 @@ function (fis::SugenoFuzzySystem)(inputs::T) where {T <: NamedTuple}
6669
end
6770

6871
(fis::SugenoFuzzySystem)(; inputs...) = fis(values(inputs))
72+
@inline function (fis::SugenoFuzzySystem)(inputs::Union{AbstractVector, Tuple})
73+
fis((; zip(collect(keys(fis.inputs)), inputs)...))
74+
end

src/parser.jl

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ function _fis(ex::Expr, type)
166166
@capture ex function name_(argsin__)::({argsout__} | argsout__)
167167
body_
168168
end
169+
argsin, argsout = process_args(argsin), process_args(argsout)
169170
inputs, outputs, opts, rules = parse_body(body, argsin, argsout, type)
170171

171172
fis = :($type(; name = $(QuoteNode(name)), inputs = $inputs,
@@ -174,6 +175,29 @@ function _fis(ex::Expr, type)
174175
return fis
175176
end
176177

178+
process_args(x::Symbol) = [x]
179+
function process_args(ex::Expr)
180+
if @capture(ex, x_[start_:stop_])
181+
[Symbol(x, i) for i in start:stop]
182+
else
183+
throw(ArgumentError("invalid expression $ex"))
184+
end
185+
end
186+
process_args(v::Vector) = mapreduce(process_args, vcat, v)
187+
188+
"""
189+
convert a symbol or expression to variable name. A symbol is returned as such.
190+
An expression in the form `:(x[i])` is converted to a symbol `:xi`.
191+
"""
192+
to_var_name(ex::Symbol) = ex
193+
function to_var_name(ex::Expr)
194+
if @capture(ex, x_[i_])
195+
return Symbol(x, eval(i))
196+
else
197+
throw(ArgumentError("Invalid variable name $ex"))
198+
end
199+
end
200+
177201
function parse_variable(var, args)
178202
mfs = :(dictionary([]))
179203
ex = :(Variable())
@@ -197,32 +221,46 @@ function parse_body(body, argsin, argsout, type)
197221
outputs = :(dictionary([]))
198222
for line in body.args
199223
line isa LineNumberNode && continue
200-
if @capture(line, var_:=begin args__ end)
201-
if var in argsin
202-
push!(inputs.args[2].args, parse_variable(var, args))
203-
elseif var in argsout
204-
# TODO: makes this more scalable
205-
push!(outputs.args[2].args,
206-
type == :SugenoFuzzySystem ? parse_sugeno_output(var, args, argsin) :
207-
parse_variable(var, args))
208-
else
209-
throw(ArgumentError("Undefined variable $var"))
210-
end
211-
elseif @capture(line, var_=value_)
212-
var in SETTINGS[type] ||
213-
throw(ArgumentError("Invalid keyword $var in line $line"))
214-
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))
219-
elseif @capture(line, ant_-->(cons__,) | cons__)
220-
push!(rules.args, parse_rule(ant, cons))
224+
parse_line!(inputs, outputs, rules, opts, line, argsin, argsout, type)
225+
end
226+
return inputs, outputs, opts, rules
227+
end
228+
229+
function parse_line!(inputs, outputs, rules, opts, line, argsin, argsout, type)
230+
if @capture(line, var_:=begin args__ end)
231+
var = to_var_name(var)
232+
if var in argsin
233+
push!(inputs.args[2].args, parse_variable(var, args))
234+
elseif var in argsout
235+
# TODO: makes this more scalable
236+
push!(outputs.args[2].args,
237+
type == :SugenoFuzzySystem ? parse_sugeno_output(var, args, argsin) :
238+
parse_variable(var, args))
221239
else
222-
throw(ArgumentError("Invalid expression $line"))
240+
throw(ArgumentError("Undefined variable $var"))
223241
end
242+
elseif @capture(line, for i_ in start_:stop_
243+
sts__
244+
end)
245+
for j in start:stop
246+
for st in sts
247+
ex = MacroTools.postwalk(x -> x == i ? j : x, st)
248+
parse_line!(inputs, outputs, rules, opts, ex, argsin, argsout, type)
249+
end
250+
end
251+
elseif @capture(line, var_=value_)
252+
var in SETTINGS[type] ||
253+
throw(ArgumentError("Invalid keyword $var in line $line"))
254+
push!(opts, Expr(:kw, var, value isa Symbol ? :($value()) : value))
255+
elseif @capture(line, ant_-->(cons__,) * w_Number)
256+
push!(rules.args, parse_rule(ant, cons, w))
257+
elseif @capture(line, ant_-->p_ == q_ * w_Number)
258+
push!(rules.args, parse_rule(ant, [:($p == $q)], w))
259+
elseif @capture(line, ant_-->(cons__,) | cons__)
260+
push!(rules.args, parse_rule(ant, cons))
261+
else
262+
throw(ArgumentError("Invalid expression $line"))
224263
end
225-
return inputs, outputs, opts, rules
226264
end
227265

228266
#################
@@ -243,9 +281,11 @@ function parse_antecedent(ant)
243281
elseif @capture(ant, left_||right_)
244282
return Expr(:call, :FuzzyOr, parse_antecedent(left), parse_antecedent(right))
245283
elseif @capture(ant, subj_==prop_)
246-
return Expr(:call, :FuzzyRelation, QuoteNode(subj), QuoteNode(prop))
284+
return Expr(:call, :FuzzyRelation, QuoteNode(to_var_name(subj)),
285+
QuoteNode(to_var_name(prop)))
247286
elseif @capture(ant, subj_!=prop_)
248-
return Expr(:call, :FuzzyNegation, QuoteNode(subj), QuoteNode(prop))
287+
return Expr(:call, :FuzzyNegation, QuoteNode(to_var_name(subj)),
288+
QuoteNode(to_var_name(prop)))
249289
else
250290
throw(ArgumentError("Invalid premise $ant"))
251291
end
@@ -254,7 +294,8 @@ end
254294
function parse_consequents(cons)
255295
newcons = map(cons) do c
256296
@capture(c, subj_==prop_) || throw(ArgumentError("Invalid consequence $c"))
257-
Expr(:call, :FuzzyRelation, QuoteNode(subj), QuoteNode(prop))
297+
Expr(:call, :FuzzyRelation, QuoteNode(to_var_name(subj)),
298+
QuoteNode(to_var_name(prop)))
258299
end
259300
return Expr(:vect, newcons...)
260301
end

src/rules.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ struct FuzzyRule{T <: AbstractFuzzyProposition} <: AbstractRule
5959
"consequences of the inference rule."
6060
consequent::Vector{FuzzyRelation}
6161
end
62-
Base.show(io::IO, r::FuzzyRule) = print(io, r.antecedent, " --> ", r.consequent...)
62+
Base.show(io::IO, r::FuzzyRule) = print(io, r.antecedent, " --> ", join(r.consequent, ", "))
6363

6464
"""
6565
Weighted fuzzy rule. In Mamdani systems, the result of implication is scaled by the weight.
@@ -75,7 +75,7 @@ struct WeightedFuzzyRule{T <: AbstractFuzzyProposition, S <: Real} <: AbstractRu
7575
end
7676

7777
function Base.show(io::IO, r::WeightedFuzzyRule)
78-
print(io, r.antecedent, " --> ", r.consequent..., " (", r.weight, ")")
78+
print(io, r.antecedent, " --> ", join(r.consequent, ", "), " (", r.weight, ")")
7979
end
8080

8181
@inline scale(w, ::FuzzyRule) = w

test/test_evaluation.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ using FuzzyLogic, Test
6060
end
6161

6262
@test fis(service = 2, food = 7)[:tip]9.36 atol=1e-2
63+
@test fis([2, 7])[:tip]9.36 atol=1e-2
6364
end
6465

6566
@testset "test Sugeno evaluation" begin
@@ -116,4 +117,5 @@ end
116117
service == excellent || food == delicious --> tip == generous
117118
end
118119
@test fis2(service = 7, food = 8)[:tip]19.639 atol=1e-3
120+
@test fis2((7, 8))[:tip]19.639 atol=1e-3
119121
end

test/test_parser.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,52 @@ end
122122

123123
@test fis.outputs[:tip].mfs == mfs
124124
end
125+
126+
@testset "parse vector-like notation" begin
127+
fis = @mamfis function denoise(x[1:3])::y[1:2]
128+
for i in 1:3
129+
x[i] := begin
130+
domain = -1000:1000
131+
POS = TriangularMF(-255.0, 255.0, 765.0)
132+
NEG = TriangularMF(-765.0, -255.0, 255.0)
133+
end
134+
end
135+
136+
y1 := begin
137+
domain = -1000:1000
138+
POS = TriangularMF(-255.0, 255.0, 765.0)
139+
NEG = TriangularMF(-765.0, -255.0, 255.0)
140+
end
141+
142+
y2 := begin
143+
domain = -1000:1000
144+
POS = TriangularMF(-255.0, 255.0, 765.0)
145+
NEG = TriangularMF(-765.0, -255.0, 255.0)
146+
end
147+
148+
for i in 1:2
149+
x[i] == POS && x[i + 1] == NEG --> (y[i] == POS, y[i % 2 + 1] == NEG)
150+
end
151+
end
152+
153+
var = Variable(Domain(-1000, 1000),
154+
Dictionary([:POS, :NEG],
155+
[
156+
TriangularMF(-255.0, 255.0, 765.0),
157+
TriangularMF(-765.0, -255.0, 255.0),
158+
]))
159+
for x in (:x1, :x2, :x3)
160+
@test fis.inputs[x] == var
161+
end
162+
163+
for y in (:y1, :y2)
164+
@test fis.outputs[y] == var
165+
end
166+
167+
@test fis.rules == [
168+
FuzzyRule(FuzzyAnd(FuzzyRelation(:x1, :POS), FuzzyRelation(:x2, :NEG)),
169+
[FuzzyRelation(:y1, :POS), FuzzyRelation(:y2, :NEG)]),
170+
FuzzyRule(FuzzyAnd(FuzzyRelation(:x2, :POS), FuzzyRelation(:x3, :NEG)),
171+
[FuzzyRelation(:y2, :POS), FuzzyRelation(:y1, :NEG)]),
172+
]
173+
end

0 commit comments

Comments
 (0)