Skip to content

Commit 1f84faa

Browse files
authored
FML parser (#20)
* fml parser initial implementation * add support for more T- and S- norms * fix small typos in docs
1 parent eaee0e4 commit 1f84faa

File tree

13 files changed

+534
-5
lines changed

13 files changed

+534
-5
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ version = "0.1.1"
66
[deps]
77
Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4"
88
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
9+
LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179"
910
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1011
PEG = "12d937ae-5f68-53be-93c9-3a6f997a20a8"
1112
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
@@ -15,6 +16,7 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
1516
Aqua = "0.6"
1617
Dictionaries = "0.3"
1718
DocStringExtensions = "0.9"
19+
LightXML = "0.9"
1820
MacroTools = "0.5"
1921
PEG = "1.0"
2022
RecipesBase = "1.0"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ using FuzzyLogic
2525
## Features
2626

2727
- **Rich!** Mamdani and Sugeno inference systems, both Type-1 and Type-2, several [membership functions](https://lucaferranti.github.io/FuzzyLogic.jl/stable/api/memberships) and [algoritms options](https://lucaferranti.github.io/FuzzyLogic.jl/stable/api/fis) available.
28-
- **Compatible!** Read your models from [IEC 61131-7 Fuzzy Control Language](https://ffll.sourceforge.net/fcl.htm) and Matlab Fuzzy toolbox `.fis` files.
28+
- **Compatible!** Read your models from [IEC 61131-7 Fuzzy Control Language](https://ffll.sourceforge.net/fcl.htm), [IEEE 1855-2016 Fuzzy Markup Language](https://en.wikipedia.org/wiki/Fuzzy_markup_language) and Matlab Fuzzy toolbox `.fis` files.
2929
- **Expressive!** Clear Domain Specific Language to write your model as human readable Julia code
3030
- **Productive!** Several visualization tools to help debug and tune your model.
3131
## Quickstart example

docs/src/api/readwrite.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ parse_fcl
2020
@fcl_str
2121
```
2222

23+
## Parse FML
24+
25+
```@docs
26+
parse_fml
27+
@fml_str
28+
```
29+
2330
## Parse Matlab
2431

2532
```@docs

docs/src/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
- ![](https://img.shields.io/badge/new%20feature-green.svg) support for weighted rules.
88
- ![](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.
99
- ![](https://img.shields.io/badge/new%20feature-green.svg) added support for type-2 membership functions and type-2 systems.
10+
- ![](https://img.shields.io/badge/new%20feature-green.svg) added parser for Fuzzy Markup Language.
1011

1112
## v0.1.1 -- 2023-02-25
1213

docs/src/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ using FuzzyLogic
2525
## Features
2626

2727
- **Rich!** Mamdani and Sugeno inference systems, both Type-1 and Type-2, several [membership functions](https://lucaferranti.github.io/FuzzyLogic.jl/stable/api/memberships) and [algoritms options](https://lucaferranti.github.io/FuzzyLogic.jl/stable/api/fis) available.
28-
- **Compatible!** Read your models from [IEC 61131-7 Fuzzy Control Language](https://ffll.sourceforge.net/fcl.htm) and Matlab Fuzzy toolbox `.fis` files.
28+
- **Compatible!** Read your models from [IEC 61131-7 Fuzzy Control Language](https://ffll.sourceforge.net/fcl.htm), [IEEE 1855-2016 Fuzzy Markup Language](https://en.wikipedia.org/wiki/Fuzzy_markup_language) and Matlab Fuzzy toolbox `.fis` files.
2929
- **Expressive!** Clear Domain Specific Language to write your model as human readable Julia code
3030
- **Productive!** Several visualization tools to help debug and tune your model.
3131

src/FuzzyLogic.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ include("readwrite.jl")
1818
export DifferenceSigmoidMF, LinearMF, GeneralizedBellMF, GaussianMF, ProductSigmoidMF,
1919
SigmoidMF, TrapezoidalMF, TriangularMF, SShapeMF, ZShapeMF, PiShapeMF,
2020
PiecewiseLinearMF, WeightedMF, Type2MF, ..,
21-
ProdAnd, MinAnd, LukasiewiczAnd, DrasticAnd, NilpotentAnd, HamacherAnd,
22-
ProbSumOr, MaxOr, BoundedSumOr, DrasticOr, NilpotentOr, EinsteinOr,
21+
ProdAnd, MinAnd, LukasiewiczAnd, DrasticAnd, NilpotentAnd, HamacherAnd, EinsteinAnd,
22+
ProbSumOr, MaxOr, BoundedSumOr, DrasticOr, NilpotentOr, EinsteinOr, HamacherOr,
2323
MinImplication, ProdImplication,
2424
MaxAggregator, ProbSumAggregator, CentroidDefuzzifier, BisectorDefuzzifier,
2525
KarnikMendelDefuzzifier, EKMDefuzzifier, IASCDefuzzifier, EIASCDefuzzifier,
@@ -32,7 +32,9 @@ export DifferenceSigmoidMF, LinearMF, GeneralizedBellMF, GaussianMF, ProductSigm
3232

3333
include("parsers/fcl.jl")
3434
include("parsers/matlab_fis.jl")
35+
include("parsers/fml.jl")
3536
@reexport using .FCLParser
3637
@reexport using .MatlabParser
38+
@reexport using .FMLParser
3739

3840
end

src/options.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function (na::NilpotentAnd)(x::T, y::S) where {T <: Real, S <: Real}
4747
end
4848

4949
"""
50-
Hamacher T-norm defining conjuction as ``A ∧ B = \\frac{AB}{A + B - AB}`` if ``A ≂̸ 0 ≂̸ B``
50+
Hamacher T-norm defining conjuction as ``A ∧ B = \\frac{AB}{A + B - AB}`` if ``A \\neq 0 \\neq B``
5151
and ``A ∧ B = 0`` otherwise.
5252
"""
5353
struct HamacherAnd <: AbstractAnd end
@@ -56,6 +56,12 @@ function (ha::HamacherAnd)(x::T, y::S) where {T <: Real, S <: Real}
5656
(x * y) / (x + y - x * y)
5757
end
5858

59+
"""
60+
Einstein T-norm defining conjuction as ``A ∧ B = \\frac{AB}{2 - A - B + AB}``.
61+
"""
62+
struct EinsteinAnd <: AbstractAnd end
63+
(ha::EinsteinAnd)(x::Real, y::Real) = (x * y) / (2 - x - y + x * y)
64+
5965
## S-Norms
6066

6167
abstract type AbstractOr <: AbstractFISSetting end
@@ -106,6 +112,16 @@ Einstein S-norm defining disjunction as ``A ∨ B = \\frac{A + B}{1 + AB}``.
106112
struct EinsteinOr <: AbstractOr end
107113
(ha::EinsteinOr)(x, y) = (x + y) / (1 + x * y)
108114

115+
"""
116+
Hamacher S-norm defining conjuction as ``A ∨ B = \\frac{A + B - AB}{1 - AB}`` if ``A \\neq 1 \\neq B``
117+
and ``A ∨ B = 1`` otherwise.
118+
"""
119+
struct HamacherOr <: AbstractOr end
120+
function (ha::HamacherOr)(x::T, y::S) where {T <: Real, S <: Real}
121+
isone(x) && isone(y) && return one(float(promote_type(T, S)))
122+
(x + y - 2 * x * y) / (1 - x * y)
123+
end
124+
109125
## Implication
110126

111127
abstract type AbstractImplication <: AbstractFISSetting end

src/parsers/fml.jl

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
module FMLParser
2+
3+
using Dictionaries, LightXML
4+
using ..FuzzyLogic
5+
using ..FuzzyLogic: FuzzyAnd, FuzzyOr, FuzzyRule, FuzzyRelation, FuzzyNegation, Domain,
6+
WeightedFuzzyRule, Variable, memberships, AbstractMembershipFunction,
7+
AbstractSugenoOutputFunction
8+
9+
export parse_fml, @fml_str
10+
11+
XML_JULIA = Dict("mamdaniRuleBase" => MamdaniFuzzySystem,
12+
"tskRuleBase" => SugenoFuzzySystem,
13+
"triangularShape" => TriangularMF,
14+
"trapezoidShape" => TrapezoidalMF,
15+
"piShape" => PiShapeMF,
16+
"sShape" => SShapeMF,
17+
"zShape" => ZShapeMF,
18+
"gaussianShape" => GaussianMF,
19+
"rightGaussianShape" => GaussianMF,
20+
"leftGaussianShape" => GaussianMF,
21+
"rightLinearShape" => LinearMF,
22+
"leftLinearShape" => LinearMF,
23+
"rightLinearShape" => LinearMF,
24+
"COG" => CentroidDefuzzifier(),
25+
"COA" => BisectorDefuzzifier(),
26+
"ACCMAX" => MaxAggregator(),
27+
"andMIN" => MinAnd(),
28+
"andPROD" => ProdAnd(),
29+
"andBDIF" => LukasiewiczAnd(),
30+
"andDRP" => DrasticAnd(),
31+
"andHPROD" => HamacherAnd(),
32+
"andEPROD" => EinsteinAnd(),
33+
"andNILMIN" => DrasticAnd(),
34+
"orMAX" => MaxOr(),
35+
"orPROBOR" => ProbSumOr(),
36+
"orBSUM" => BoundedSumOr(),
37+
"orDRS" => DrasticOr(),
38+
"orNILMAX" => NilpotentOr(),
39+
"orESUM" => EinsteinOr(),
40+
"orHSUM" => HamacherOr(),
41+
"impMIN" => MinImplication(),
42+
"and" => FuzzyAnd,
43+
"or" => FuzzyOr)
44+
45+
get_attribute(x, s, d) = has_attribute(x, s) ? attribute(x, s) : d
46+
to_key(s, pre) = isempty(pre) ? s : pre * uppercasefirst(s)
47+
48+
function process_params(mf, params)
49+
if mf == "rightLinearShape"
50+
reverse(params)
51+
else
52+
params
53+
end
54+
end
55+
56+
# Parse variable from xml. FML does not have negated relations, but negated mfs,
57+
# so we store in the set `negated` what relations will have to be `FuzzyNegation`.
58+
function parse_variable!(var, negated::Set)
59+
varname = Symbol(attribute(var, "name"))
60+
dom = Domain(parse(Float64, attribute(var, "domainleft")),
61+
parse(Float64, attribute(var, "domainright")))
62+
mfs = AbstractMembershipFunction[]
63+
mfnames = Symbol[]
64+
for term in get_elements_by_tagname(var, "fuzzyTerm")
65+
mfname = Symbol(attribute(term, "name"))
66+
push!(mfnames, mfname)
67+
if has_attribute(term, "complement") &&
68+
lowercase(attribute(term, "complement")) == "true"
69+
push!(negated, (varname, mfname))
70+
end
71+
mf = only(child_elements(term))
72+
params = process_params(name(mf), parse.(Float64, value.(collect(attributes(mf)))))
73+
push!(mfs, XML_JULIA[name(mf)](params...))
74+
end
75+
return varname => Variable(dom, Dictionary(mfnames, identity.(mfs)))
76+
end
77+
78+
function parse_sugeno_output(var, input_names)
79+
varname = Symbol(attribute(var, "name"))
80+
dom = Domain(parse(Float64, get_attribute(var, "domainleft", "-Inf")),
81+
parse(Float64, get_attribute(var, "domainright", "Inf")))
82+
mfs = AbstractSugenoOutputFunction[]
83+
mfnames = Symbol[]
84+
for term in get_elements_by_tagname(var, "tskTerm")
85+
push!(mfnames, Symbol(attribute(term, "name")))
86+
mf = if attribute(term, "order") == "0"
87+
ConstantSugenoOutput(parse(Float64, content(find_element(term, "tskValue"))))
88+
elseif attribute(term, "order") == "1"
89+
coeffs = map(get_elements_by_tagname(term, "tskValue")) do t
90+
parse(Float64, content(t))
91+
end
92+
LinearSugenoOutput(Dictionary(input_names, coeffs[1:(end - 1)]), coeffs[end])
93+
end
94+
push!(mfs, mf)
95+
end
96+
return varname => Variable(dom, Dictionary(mfnames, identity.(mfs)))
97+
end
98+
99+
function parse_knowledgebase(kb, settings)
100+
negated = Set{Tuple{Symbol, Symbol}}()
101+
inputs = Pair{Symbol, Variable}[]
102+
outputs = Pair{Symbol, Variable}[]
103+
for var in get_elements_by_tagname(kb, "fuzzyVariable")
104+
if attribute(var, "type") == "input"
105+
push!(inputs, parse_variable!(var, negated))
106+
elseif attribute(var, "type") == "output"
107+
settings[:defuzzifier] = XML_JULIA[get_attribute(var, "defuzzifier", "COG")]
108+
settings[:aggregator] = XML_JULIA["ACC" * get_attribute(var, "accumulation",
109+
"MAX")]
110+
push!(outputs, parse_variable!(var, negated))
111+
end
112+
end
113+
114+
for var in get_elements_by_tagname(kb, "tskVariable")
115+
push!(outputs, parse_sugeno_output(var, first.(inputs)))
116+
end
117+
settings[:inputs] = dictionary(identity.(inputs))
118+
settings[:outputs] = dictionary(identity.(outputs))
119+
return negated
120+
end
121+
122+
function parse_rules!(settings, rulebase, negated)
123+
pre = name(rulebase) == "tskRuleBase" ? "tsk" : ""
124+
settings[:and] = XML_JULIA["and" * get_attribute(rulebase, "andMethod", "MIN")]
125+
settings[:or] = XML_JULIA["or" * get_attribute(rulebase, "orMethod", "MAX")]
126+
127+
if isempty(pre)
128+
settings[:implication] = XML_JULIA["imp" * get_attribute(rulebase,
129+
"activationMethod",
130+
"MIN")]
131+
end
132+
133+
settings[:rules] = identity.([parse_rule(rule, negated, pre)
134+
for rule in get_elements_by_tagname(rulebase,
135+
to_key("rule", pre))])
136+
end
137+
138+
function parse_rule(rule, negated, pre = "")
139+
op = XML_JULIA[lowercase(get_attribute(rule, "connector", "and"))]
140+
141+
ant = find_element(rule, "antecedent")
142+
ant = mapreduce(op, get_elements_by_tagname(ant, "clause")) do clause
143+
var = Symbol(content(find_element(clause, "variable")))
144+
t = Symbol(content(find_element(clause, "term")))
145+
(var, t) in negated ? FuzzyNegation(var, t) : FuzzyRelation(var, t)
146+
end
147+
148+
cons = find_element(find_element(rule, to_key("consequent", pre)), to_key("then", pre))
149+
cons = map(get_elements_by_tagname(cons, to_key("clause", pre))) do clause
150+
var = Symbol(content(find_element(clause, "variable")))
151+
t = Symbol(content(find_element(clause, "term")))
152+
(var, t) in negated ? FuzzyNegation(var, t) : FuzzyRelation(var, t)
153+
end
154+
155+
w = parse(Float64, get_attribute(rule, "weight", "1.0"))
156+
if isone(w)
157+
FuzzyRule(ant, cons)
158+
else
159+
WeightedFuzzyRule(ant, cons, w)
160+
end
161+
end
162+
163+
function get_rulebase(f)
164+
rules = find_element(f, "mamdaniRuleBase")
165+
isnothing(rules) || return rules
166+
rules = find_element(f, "tskRuleBase")
167+
isnothing(rules) || return rules
168+
end
169+
170+
"""
171+
parse_fml(s::String)::AbstractFuzzySystem
172+
173+
Parse a fuzzy inference system from a string representation in Fuzzy Markup Language (FML).
174+
175+
### Inputs
176+
177+
- `s::String` -- string describing a fuzzy system in FML conformant to the IEEE 1855-2016 standard.
178+
179+
### Notes
180+
181+
The parsers can read FML comformant to IEEE 1855-2016, with the following remarks:
182+
183+
- only Mamdani and Sugeno are supported.
184+
- Operators `and` and `or` definitions should be set in the rule base block.
185+
Definitions at individual rules are ignored.
186+
- Modifiers are not supported.
187+
"""
188+
function parse_fml(s::String)
189+
parse_fml(root(parse_string(s)))
190+
end
191+
192+
function parse_fml(f::XMLElement)
193+
settings = Dict()
194+
kb = only(get_elements_by_tagname(f, "knowledgeBase"))
195+
settings[:name] = Symbol(attribute(f, "name"))
196+
negated = parse_knowledgebase(kb, settings)
197+
rulebase = get_rulebase(f)
198+
parse_rules!(settings, rulebase, negated)
199+
settings
200+
201+
XML_JULIA[name(rulebase)](; settings...)
202+
end
203+
204+
"""
205+
String macro to parse Fuzzy Markup Language (FML). See [`parse_fml`](@ref) for more details.
206+
"""
207+
macro fml_str(s)
208+
parse_fml(s)
209+
end
210+
211+
end

src/readwrite.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Read a fuzzy system from a file using a specified format.
99
Supported formats are
1010
1111
- `:fcl` -- Fuzzy Control Language (corresponding file extension `.fcl`)
12+
- `:fml` -- Fuzzy Markup Language (corresponding file extension `.xml`)
1213
- `:matlab` -- Matlab fis (corresponding file extension `.fis`)
1314
"""
1415
function readfis(file::String, fmt::Union{Symbol, Nothing} = nothing)::AbstractFuzzySystem
@@ -18,6 +19,8 @@ function readfis(file::String, fmt::Union{Symbol, Nothing} = nothing)::AbstractF
1819
:fcl
1920
elseif ex == "fis"
2021
:matlab
22+
elseif ex == "xml"
23+
:fml
2124
else
2225
throw(ArgumentError("Unrecognized extension $ex."))
2326
end
@@ -28,6 +31,8 @@ function readfis(file::String, fmt::Union{Symbol, Nothing} = nothing)::AbstractF
2831
parse_fcl(s)
2932
elseif fmt === :matlab
3033
parse_matlabfis(s)
34+
elseif fmt === :fml
35+
parse_fml(s)
3136
else
3237
throw(ArgumentError("Unknown format $fmt."))
3338
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ testfiles = [
1010
"test_genfis.jl",
1111
"test_parsers/test_fcl.jl",
1212
"test_parsers/test_matlab.jl",
13+
"test_parsers/test_fml.jl",
1314
"test_aqua.jl",
1415
"test_doctests.jl",
1516
]

0 commit comments

Comments
 (0)