|
| 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 |
0 commit comments