Skip to content

Commit c0a8521

Browse files
authored
parsing of fuzzy control language (#13)
* initial draft for FCL parser * piecewise linear mf and parsing for variables and defuzzifier * finish first FCL implementation * add fcl string macro * fix workflow on 1.6 * support arbitrary propositions in premise * add more tests and documentation
1 parent ce5b435 commit c0a8521

File tree

11 files changed

+470
-4
lines changed

11 files changed

+470
-4
lines changed

Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ version = "0.1.0"
77
Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4"
88
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
99
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
10+
PEG = "12d937ae-5f68-53be-93c9-3a6f997a20a8"
1011
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
12+
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
1113

1214
[compat]
1315
Aqua = "0.6"
1416
Dictionaries = "0.3"
1517
DocStringExtensions = "0.9"
1618
MacroTools = "0.5"
19+
PEG = "1.0"
1720
RecipesBase = "1.0"
21+
Reexport = "1.0"
1822
julia = "1.6"
1923

2024
[extras]

docs/src/api/readwrite.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,17 @@
55
```@docs
66
@mamfis
77
@sugfis
8-
```
8+
```
9+
10+
## Read from file
11+
12+
```@docs
13+
readfis
14+
```
15+
16+
## Parse FCL
17+
18+
```@docs
19+
parse_fcl
20+
@fcl_str
21+
```

docs/src/changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
- ![][badge-feature] Added fuzzy c-means
88
- ![][badge-enhancement] added support Lukasiewicz, drastic, nilpotent and Hamacher T-norms and corresponding S-norms.
99
- ![][badge-enhancement] dont build anonymous functions during mamdani inference, but evaluate output directly. Now defuzzifiers don't take a function as input, but an array.
10-
10+
- ![][badge-feature] added piecewise linear membership functions
11+
![][badge-feature] added parser for Fuzzy Control Language.
1112
## v0.1.0 -- 2023-01-10
1213

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

src/FuzzyLogic.jl

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module FuzzyLogic
22

3-
using Dictionaries
3+
using Dictionaries, Reexport
44

55
include("docstrings.jl")
66
include("membership_functions.jl")
@@ -12,14 +12,24 @@ include("parser.jl")
1212
include("evaluation.jl")
1313
include("plotting.jl")
1414
include("genfis.jl")
15+
include("readwrite.jl")
1516

1617
export DifferenceSigmoidMF, LinearMF, GeneralizedBellMF, GaussianMF, ProductSigmoidMF,
1718
SigmoidMF, TrapezoidalMF, TriangularMF, SShapeMF, ZShapeMF, PiShapeMF,
19+
PiecewiseLinearMF,
1820
ProdAnd, MinAnd, LukasiewiczAnd, DrasticAnd, NilpotentAnd, HamacherAnd,
1921
ProbSumOr, MaxOr, BoundedSumOr, DrasticOr, NilpotentOr, EinsteinOr,
2022
MinImplication, ProdImplication,
2123
MaxAggregator, ProbSumAggregator, CentroidDefuzzifier, BisectorDefuzzifier,
2224
@mamfis, MamdaniFuzzySystem, @sugfis, SugenoFuzzySystem,
2325
LinearSugenoOutput, ConstantSugenoOutput,
24-
fuzzy_cmeans
26+
fuzzy_cmeans,
27+
readfis
28+
29+
## parsers
30+
31+
include("parsers/fcl.jl")
32+
33+
@reexport using .FCLParser
34+
2535
end

src/membership_functions.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,37 @@ function (p::PiShapeMF)(x::T) where {T <: Real}
277277
x <= (p.c + p.d) / 2 && return 1 - 2 * ((x - p.c) / (p.d - p.c))^2
278278
return 2 * ((x - p.d) / (p.d - p.c))^2
279279
end
280+
281+
"""
282+
Piecewise linear membership function.
283+
284+
### Fields
285+
286+
$(TYPEDFIELDS)
287+
288+
### Notes
289+
290+
If the input is between two points, its membership degree is computed by linear interpolation.
291+
If the input is before the first point, it has the same membership degree of the first point.
292+
If the input is after the last point, it has the same membership degree of the first point.
293+
294+
### Example
295+
296+
```julia
297+
mf = PiecewiseLinearMF([(1, 0), (2, 1), (3, 0), (4, 0.5), (5, 0), (6, 1)])
298+
```
299+
"""
300+
struct PiecewiseLinearMF{T <: Real, S <: Real} <: AbstractMembershipFunction
301+
points::Vector{Tuple{T, S}}
302+
end
303+
function (plmf::PiecewiseLinearMF)(x::Real)
304+
x <= plmf.points[1][1] && return plmf.points[1][2]
305+
x >= plmf.points[end][1] && return plmf.points[end][2]
306+
idx = findlast(p -> x >= p[1], plmf.points)
307+
x1, y1 = plmf.points[idx]
308+
x2, y2 = plmf.points[idx + 1]
309+
(y2 - y1) / (x2 - x1) * (x - x1) + y1
310+
end
311+
312+
# TODO: more robust soultion for all mfs
313+
Base.:(==)(mf1::PiecewiseLinearMF, mf2::PiecewiseLinearMF) = mf1.points == mf2.points

src/parsers/fcl.jl

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
2+
module FCLParser
3+
4+
using PEG, Dictionaries
5+
using ..FuzzyLogic
6+
using ..FuzzyLogic: Variable, Domain
7+
8+
export parse_fcl, @fcl_str
9+
10+
const FCL_JULIA = Dict("COG" => CentroidDefuzzifier(),
11+
"COGS" => "COGS", # dummy since hardcoded for sugeno
12+
"COA" => BisectorDefuzzifier(),
13+
"ANDMIN" => MinAnd(),
14+
"ANDPROD" => ProdAnd(),
15+
"ANDBDIF" => LukasiewiczAnd(),
16+
"ORMAX" => MaxOr(),
17+
"ORASUM" => ProbSumOr(),
18+
"ORBSUM" => BoundedSumOr(),
19+
"ACTPROD" => ProdImplication(),
20+
"ACTMIN" => MinImplication())
21+
22+
function fcl_julia(s::AbstractString)
23+
haskey(FCL_JULIA, s) ? FCL_JULIA[s] : throw(ArgumentError("Option $s not supported."))
24+
end
25+
26+
@rule id = r"[a-zA-Z_]+[a-zA-z0-9_]*"p |> Symbol
27+
@rule function_block = r"FUNCTION_BLOCK"p & id & var_input_block & var_output_block &
28+
fuzzify_block[1:end] & defuzzify_block[1:end] & rule_block &
29+
r"END_FUNCTION_BLOCK"p |> x -> x[2:7]
30+
31+
@rule var_input_block = r"VAR_INPUT"p & var_def[1:end] & r"END_VAR"p |>
32+
x -> Vector{Symbol}(x[2])
33+
@rule var_output_block = r"VAR_OUTPUT"p & var_def[1:end] & r"END_VAR"p |>
34+
x -> Vector{Symbol}(x[2])
35+
@rule var_def = id & r":"p & r"REAL"p & r";"p |> first
36+
37+
@rule fuzzify_block = r"FUZZIFY"p & id & linguistic_term[1:end] & range_term &
38+
r"END_FUZZIFY"p |> x -> x[2] => Variable(x[4], dictionary(x[3]))
39+
40+
@rule range_term = r"RANGE\s*:=\s*\("p & numeral & r".."p & numeral & r"\)\s*;"p |>
41+
x -> Domain(x[2], x[4])
42+
43+
@rule linguistic_term = r"TERM"p & id & r":="p & membership_function & r";"p |>
44+
x -> x[2] => x[4]
45+
@rule membership_function = singleton, points
46+
@rule singleton = r"[+-]?\d+\.?\d*([eE][+-]?\d+)?"p |>
47+
ConstantSugenoOutput Base.Fix1(parse, Float64)
48+
49+
@rule numeral = r"[+-]?\d+(\.\d+)?([eE][+-]?\d+)?"p |> Base.Fix1(parse, Float64)
50+
@rule point = r"\("p & numeral & r","p & numeral & r"\)"p |> x -> tuple(x[2], x[4])
51+
@rule points = point[2:end] |> PiecewiseLinearMF Vector{Tuple{Float64, Float64}}
52+
53+
@rule defuzzify_block = r"DEFUZZIFY"p & id & linguistic_term[1:end] & defuzzify_method &
54+
range_term & r"END_DEFUZZIFY"p |>
55+
(x -> (x[2] => Variable(x[5], dictionary(x[3])), x[4]))
56+
57+
@rule defuzzify_method = r"METHOD\s*:"p & (r"COGS"p, r"COG"p, r"COA"p, r"LM"p, r"RM"p) &
58+
r";"p |> x -> fcl_julia(x[2])
59+
60+
@rule rule_block = r"RULEBLOCK"p & id & operator_definition & activation_method[:?] &
61+
rule[1:end] & r"END_RULEBLOCK"p |>
62+
x -> (x[3], x[4], Vector{FuzzyLogic.FuzzyRule}(x[5]))
63+
64+
@rule or_definition = r"OR"p & r":"p & (r"MAX"p, r"ASUM"p, r"BSUM"p) & r";"p
65+
@rule and_definition = r"AND"p & r":"p & (r"MIN"p, r"PROD"p, r"BDIF"p) & r";"p
66+
@rule operator_definition = (and_definition, or_definition) |>
67+
x -> fcl_julia(join([x[1], x[3]]))
68+
@rule activation_method = r"ACT"p & r":"p & (r"MIN"p, r"PROD"p) & r";"p |>
69+
x -> fcl_julia(join([x[1], x[3]]))
70+
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])
73+
@rule relation = negrel, posrel
74+
@rule posrel = id & r"IS"p & id |> x -> FuzzyLogic.FuzzyRelation(x[1], x[3])
75+
@rule negrel = (id & r"IS\s+NOT"p & id |> x -> FuzzyLogic.FuzzyNegation(x[1], x[3])),
76+
r"NOT\s*\("p & id & r"IS"p & id & r"\)"p |>
77+
x -> FuzzyLogic.FuzzyNegation(x[2], x[4])
78+
@rule conclusion = posrel & (r","p & posrel)[*] |> x -> append!([x[1]], map(last, x[2]))
79+
80+
@rule condition = orcondition
81+
@rule orcondition = andcondition & (r"OR"p & andcondition)[*] |>
82+
x -> reduce(FuzzyLogic.FuzzyOr, vcat(x[1], map(last, x[2])))
83+
@rule andcondition = term & (r"AND"p & term)[*] |>
84+
x -> reduce(FuzzyLogic.FuzzyAnd, vcat(x[1], map(last, x[2])))
85+
@rule term = relation, r"\("p & condition & r"\)"p |> x -> x[2]
86+
87+
"""
88+
parse_fcl(s::String)::AbstractFuzzySystem
89+
90+
Parse a fuzzy inference system from a string representation in Fuzzy Control Language (FCL).
91+
92+
### Inputs
93+
94+
- `s::String` -- string describing a fuzzy system in FCL conformant to the IEC 1131-7 standard.
95+
96+
### Notes
97+
98+
The parsers can read FCL comformant to IEC 1131-7, with the following remarks:
99+
100+
- Weighted rules are not supported.
101+
- Sugeno (system with singleton outputs) shall use COGS as defuzzifier.
102+
- the `RANGE` keyword is required for both fuzzification and defuzzification blocks.
103+
- Only the required `MAX` accumulator is supported.
104+
- Default value for defuzzification not supported.
105+
- Optional local variables are not supported.
106+
107+
With the exceptions above, the parser supports all required and optional features of the standard (tables 6.1-1 and 6.1-2).
108+
In addition, it also supports the following features:
109+
110+
- Piecewise linear functions can have any number of points.
111+
- Membership degrees in piecewise linear functions points can be any number between ``0`` and ``1``.
112+
"""
113+
function parse_fcl(s::String)::FuzzyLogic.AbstractFuzzySystem
114+
name, inputs, outputs, inputsmfs, outputsmf, (op, imp, rules) = parse_whole(function_block,
115+
s)
116+
varsin = dictionary(inputsmfs)
117+
@assert sort(collect(keys(varsin)))==sort(inputs) "Mismatch between declared and fuzzified input variables."
118+
119+
varsout = dictionary(first.(outputsmf))
120+
@assert sort(collect(keys(varsout)))==sort(outputs) "Mismatch between declared and defuzzified output variables."
121+
122+
@assert all(==(outputsmf[1][2]), last.(outputsmf)) "All output variables should use the same defuzzification method."
123+
defuzzifier = outputsmf[1][2]
124+
and, or = ops_pairs(op)
125+
if defuzzifier == "COGS" # sugeno
126+
SugenoFuzzySystem(name, varsin, varsout, rules, and, or)
127+
else # mamdani
128+
imp = isempty(imp) ? MinImplication() : first(imp)
129+
MamdaniFuzzySystem(name, varsin, varsout, rules, and, or, imp, MaxAggregator(),
130+
defuzzifier)
131+
end
132+
end
133+
134+
ops_pairs(::MinAnd) = MinAnd(), MaxOr()
135+
ops_pairs(::ProdAnd) = ProdAnd(), ProbSumOr()
136+
ops_pairs(::LukasiewiczAnd) = LukasiewiczAnd(), BoundedSumOr()
137+
ops_pairs(::MaxOr) = MinAnd(), MaxOr()
138+
ops_pairs(::ProbSumOr) = ProdAnd(), ProbSumOr()
139+
ops_pairs(::BoundedSumOr) = LukasiewiczAnd(), BoundedSumOr()
140+
141+
"""
142+
String macro to parse Fuzzy Control Language (FCL). See [`parse_fcl`](@ref) for more details.
143+
"""
144+
macro fcl_str(s::AbstractString)
145+
parse_fcl(s)
146+
end
147+
148+
end

src/readwrite.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Read a fuzzy system from a file using a specified format.
3+
4+
### Inputs
5+
6+
- `file::String` -- path to the file to read.
7+
- `fmt::Union{Symbol,Nothing}` -- input format of the file. If `nothing`, it is inferred from the file extension.
8+
9+
Supported formats are
10+
11+
- `:fcl` (corresponding file extension `.fcl`)
12+
"""
13+
function readfis(file::String, fmt::Union{Symbol, Nothing} = nothing)::AbstractFuzzySystem
14+
if isnothing(fmt)
15+
ex = split(file, ".")[end]
16+
fmt = if ex == "fcl"
17+
:fcl
18+
else
19+
throw(ArgumentError("Unrecognized extension $ex."))
20+
end
21+
end
22+
23+
s = read(file, String)
24+
if fmt === :fcl
25+
parse_fcl(s)
26+
else
27+
throw(ArgumentError("Unknown format $fmt."))
28+
end
29+
end

src/rules.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Base.show(io::IO, r::FuzzyRule) = print(io, r.antecedent, " --> ", r.consequent.
6262
# comparisons (for testing)
6363

6464
Base.:(==)(r1::FuzzyRelation, r2::FuzzyRelation) = r1.subj == r2.subj && r1.prop == r2.prop
65+
Base.:(==)(r1::FuzzyNegation, r2::FuzzyNegation) = r1.subj == r2.subj && r1.prop == r2.prop
6566

6667
function Base.:(==)(p1::T, p2::T) where {T <: AbstractFuzzyProposition}
6768
p1.left == p2.left && p1.right == p2.right

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ testfiles = [
77
"test_evaluation.jl",
88
"test_plotting.jl",
99
"test_genfis.jl",
10+
"test_parsers/test_fcl.jl",
1011
"test_aqua.jl",
1112
"test_doctests.jl",
1213
]
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
FUNCTION_BLOCK container_crane
2+
3+
VAR_INPUT
4+
distance: REAL;
5+
angle: REAL;
6+
END_VAR
7+
8+
VAR_OUTPUT
9+
power: REAL;
10+
END_VAR
11+
12+
FUZZIFY distance
13+
TERM too_far:= (-5, 1) ( 0, 0);
14+
TERM zero := (-5, 0) ( 0, 1) ( 5,0);
15+
TERM close := ( 0, 0) ( 5, 1) (10,0);
16+
TERM medium := ( 5, 0) (10, 1) (22,0);
17+
TERM far := (10, 0) (22,1);
18+
RANGE := (-7 .. 23);
19+
END_FUZZIFY
20+
21+
FUZZIFY angle
22+
TERM neg_big := (-50, 1) (-5, 0);
23+
TERM neg_small := (-50, 0) (-5, 1) ( 0,0);
24+
TERM zero := ( -5, 0) ( 0, 1) ( 5,0);
25+
TERM pos_small := ( 0, 0) ( 5, 1) (50,0);
26+
TERM pos_big := ( 5, 0) (50, 1);
27+
RANGE := (-50..50);
28+
END_FUZZIFY
29+
30+
DEFUZZIFY power
31+
TERM neg_high := -27;
32+
TERM neg_medium := -12;
33+
TERM zero := 0;
34+
TERM pos_medium := 12;
35+
TERM pos_high := 27;
36+
METHOD : COGS;
37+
RANGE := (-27..27);
38+
END_DEFUZZIFY
39+
40+
RULEBLOCK No1
41+
AND: MIN;
42+
RULE 1: IF distance IS far AND angle IS zero THEN power IS pos_medium;
43+
RULE 2: IF distance IS far AND angle IS neg_small THEN power IS pos_big;
44+
RULE 3: IF distance IS far AND angle IS neg_big THEN power IS pos_medium;
45+
RULE 4: IF distance IS medium AND angle IS neg_small THEN power IS neg_medium;
46+
RULE 5: IF distance IS close AND angle IS pos_small THEN power IS pos_medium;
47+
RULE 6: IF distance IS zero AND angle IS zero THEN power IS zero;
48+
END_RULEBLOCK
49+
50+
END_FUNCTION_BLOCK

0 commit comments

Comments
 (0)