Skip to content

Commit

Permalink
matlab parser (#15)
Browse files Browse the repository at this point in the history
* matlab parser initial implementation

* add documentation

* negated rules, sugeno and more tests and docs

* update patch version and changelog

* typo fix
  • Loading branch information
lucaferranti authored Feb 25, 2023
1 parent b44d246 commit 0fd0eca
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "FuzzyLogic"
uuid = "271df9f8-4390-4196-9d4f-bdd0b67035b3"
authors = ["Luca Ferranti"]
version = "0.1.0"
version = "0.1.1"

[deps]
Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4"
Expand Down
7 changes: 7 additions & 0 deletions docs/src/api/readwrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ readfis
parse_fcl
@fcl_str
```

## Parse Matlab

```@docs
parse_matlabfis
@matlabfis_str
```
17 changes: 9 additions & 8 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

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).

## unreleased
## v0.1.1 -- 2023-02-25

- ![][badge-feature] Added fuzzy c-means
- ![][badge-enhancement] added support Lukasiewicz, drastic, nilpotent and Hamacher T-norms and corresponding S-norms.
- ![][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.
- ![][badge-feature] added piecewise linear membership functions
![][badge-feature] added parser for Fuzzy Control Language.
## v0.1.0 -- 2023-01-10
[view release on GitHub](https://github.com/lucaferranti/FuzzyLogic.jl/releases/tag/v0.1.1)

- ![](https://img.shields.io/badge/new%20feature-green.svg) Added fuzzy c-means
- ![](https://img.shields.io/badge/enhancement-blue.svg) added Lukasiewicz, drastic, nilpotent and Hamacher T-norms and corresponding S-norms.
- ![](https://img.shields.io/badge/enhancement-blue.svg) dont build anonymous functions during mamdani inference, but evaluate output directly. Now defuzzifiers don't take a function as input, but an array.
- ![](https://img.shields.io/badge/enhancement-blue.svg) added piecewise linear membership function
- ![](https://img.shields.io/badge/new%20feature-green.svg) added parser for Fuzzy Control Language and matlab fis.

[view release on GitHub](https://github.com/lucaferranti/FuzzyLogic.jl/releases/tag/v0.1.0)
## v0.1.0 -- 2023-01-10

**initial public release**

Expand Down
3 changes: 2 additions & 1 deletion src/FuzzyLogic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export DifferenceSigmoidMF, LinearMF, GeneralizedBellMF, GaussianMF, ProductSigm
## parsers

include("parsers/fcl.jl")

include("parsers/matlab_fis.jl")
@reexport using .FCLParser
@reexport using .MatlabParser

end
144 changes: 144 additions & 0 deletions src/parsers/matlab_fis.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
module MatlabParser

using Dictionaries
using ..FuzzyLogic
using ..FuzzyLogic: FuzzyAnd, FuzzyOr, FuzzyRule, FuzzyRelation, FuzzyNegation, Domain,
Variable, memberships, AbstractMembershipFunction

export parse_matlabfis, @matlabfis_str

const MATLAB_JULIA = Dict("'mamdani'" => MamdaniFuzzySystem,
"'sugeno'" => SugenoFuzzySystem,
"and'min'" => MinAnd(), "and'prod'" => ProdAnd(),
"or'max'" => MaxOr(), "or'probor'" => ProbSumOr(),
"imp'min'" => MinImplication(), "imp'prod'" => ProdImplication(),
"agg'max'" => MaxAggregator(),
"agg'probor'" => ProbSumAggregator(),
"'centroid'" => CentroidDefuzzifier(),
"'bisector'" => BisectorDefuzzifier(),
"'trapmf'" => TrapezoidalMF,
"'trimf'" => TriangularMF,
"'gaussmf'" => GaussianMF,
"'gbellmf'" => GeneralizedBellMF,
"'sigmf'" => SigmoidMF,
"'dsigmf'" => DifferenceSigmoidMF,
"'psigmf'" => ProductSigmoidMF,
"'zmf'" => ZShapeMF,
"'smf'" => SShapeMF,
"'pimf'" => PiShapeMF,
"'linzmf'" => LinearMF,
"'linsmf'" => LinearMF,
"'constant'" => ConstantSugenoOutput,
"'linear'" => LinearSugenoOutput)

# Handle special cases where FuzzyLogic.jl and matlab dont store parameters the same way.
function preprocess_params(mftype, mfparams; inputs = nothing)
mftype in ("'gaussmf'", "'linzmf'") && return reverse(mfparams)
mftype == "'linear'" &&
return [Dictionary(inputs, mfparams[1:(end - 1)]), mfparams[end]]
mfparams
end

function parse_mf(line::AbstractString; inputs = nothing)
mfname, mftype, mfparams = split(line, r"[:,]")
mfname = Symbol(mfname[2:(end - 1)])
mfparams = parse.(Float64, split(mfparams[2:(end - 1)]))
mfparams = preprocess_params(mftype, mfparams; inputs)
mfname, MATLAB_JULIA[mftype](mfparams...)
end

function parse_var(var; inputs = nothing)
dom = Domain(parse.(Float64, split(var["Range"][2:(end - 1)]))...)
name = Symbol(var["Name"][2:(end - 1)])
mfs = map(1:parse(Int, var["NumMFs"])) do i
mfname, mf = parse_mf(var["MF$i"]; inputs)
mfname => mf
end |> dictionary
name, Variable(dom, mfs)
end

function parse_rule(line, inputnames, outputnames, inputmfs, outputmfs)
ants, cons, op = split(line, r"[,:] ")
antsidx = filter!(!iszero, parse.(Int, split(ants)))
considx = filter!(!iszero, parse.(Int, split(cons)[1:length(outputnames)]))
# TODO: weighted rules
op = op == "1" ? FuzzyAnd : FuzzyOr
length(antsidx) == 1 && (op = identity)
ant = mapreduce(op, enumerate(antsidx)) do (var, mf)
if mf > 0
FuzzyRelation(inputnames[var], inputmfs[var][mf])
else
FuzzyNegation(inputnames[var], inputmfs[var][-mf])
end
end
con = map(enumerate(considx)) do (var, mf)
FuzzyRelation(outputnames[var], outputmfs[var][mf])
end
FuzzyRule(ant, con)
end

function parse_rules(lines, inputs, outputs)
inputnames = collect(keys(inputs))
outputnames = collect(keys(outputs))
inputmfs = collect.(keys.(memberships.(collect(inputs))))
outputmfs = collect.(keys.(memberships.(collect(outputs))))
FuzzyRule[parse_rule(line, inputnames, outputnames, inputmfs, outputmfs)
for line in lines]
end

"""
parse_matlabfis(s::AbstractString)
Parse a fuzzy inference system from a string in Matlab FIS format.
"""
function parse_matlabfis(s::AbstractString)
lines = strip.(split(s, "\n"))
key = ""
fis = Dict()
for line in lines
if occursin(r"\[[a-zA-Z0-9_]+\]", line)
key = line
fis[key] = ifelse(key == "[Rules]", [], Dict())
elseif !isempty(line)
if key != "[Rules]"
k, v = split(line, "=")
fis[key][k] = v
else
push!(fis[key], line)
end
end
end
sysinfo = fis["[System]"]
inputs = Dictionary{Symbol, Variable}()
for i in 1:parse(Int, sysinfo["NumInputs"])
varname, var = parse_var(fis["[Input$i]"])
insert!(inputs, varname, var)
end
outputs = Dictionary{Symbol, Variable}()
for i in 1:parse(Int, sysinfo["NumOutputs"])
varname, var = parse_var(fis["[Output$i]"]; inputs = collect(keys(inputs)))
insert!(outputs, varname, var)
end
rules = parse_rules(fis["[Rules]"], inputs, outputs)
opts = (; name = Symbol(sysinfo["Name"][2:(end - 1)]), inputs = inputs,
outputs = outputs, rules = rules,
and = MATLAB_JULIA["and" * sysinfo["AndMethod"]],
or = MATLAB_JULIA["or" * sysinfo["OrMethod"]])

if sysinfo["Type"] == "'mamdani'"
opts = (; opts..., implication = MATLAB_JULIA["imp" * sysinfo["ImpMethod"]],
aggregator = MATLAB_JULIA["agg" * sysinfo["AggMethod"]],
defuzzifier = MATLAB_JULIA[sysinfo["DefuzzMethod"]])
end

MATLAB_JULIA[sysinfo["Type"]](; opts...)
end

"""
String macro to parse Matlab fis formats. See [`parse_matlabfis`](@ref) for more details.
"""
macro matlabfis_str(s::AbstractString)
parse_matlabfis(s)
end

end
7 changes: 6 additions & 1 deletion src/readwrite.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ Read a fuzzy system from a file using a specified format.
Supported formats are
- `:fcl` (corresponding file extension `.fcl`)
- `:fcl` -- Fuzzy Control Language (corresponding file extension `.fcl`)
- `:matlab` -- Matlab fis (corresponding file extension `.fis`)
"""
function readfis(file::String, fmt::Union{Symbol, Nothing} = nothing)::AbstractFuzzySystem
if isnothing(fmt)
ex = split(file, ".")[end]
fmt = if ex == "fcl"
:fcl
elseif ex == "fis"
:matlab
else
throw(ArgumentError("Unrecognized extension $ex."))
end
Expand All @@ -23,6 +26,8 @@ function readfis(file::String, fmt::Union{Symbol, Nothing} = nothing)::AbstractF
s = read(file, String)
if fmt === :fcl
parse_fcl(s)
elseif fmt === :matlab
parse_matlabfis(s)
else
throw(ArgumentError("Unknown format $fmt."))
end
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ testfiles = [
"test_plotting.jl",
"test_genfis.jl",
"test_parsers/test_fcl.jl",
"test_parsers/test_matlab.jl",
"test_aqua.jl",
"test_doctests.jl",
]
Expand Down
39 changes: 39 additions & 0 deletions test/test_parsers/data/tipper.fis
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[System]
Name='tipper'
Type='mamdani'
NumInputs=2
NumOutputs=1
NumRules=3
AndMethod='min'
OrMethod='max'
ImpMethod='min'
AggMethod='max'
DefuzzMethod='centroid'

[Input1]
Name='service'
Range=[0 10]
NumMFs=3
MF1='poor':'gaussmf',[1.5 0]
MF2='good':'gaussmf',[1.5 5]
MF3='excellent':'gaussmf',[1.5 10]

[Input2]
Name='food'
Range=[0 10]
NumMFs=2
MF1='rancid':'trapmf',[-2 0 1 3]
MF2='delicious':'trapmf',[7 9 10 12]

[Output1]
Name='tip'
Range=[0 30]
NumMFs=3
MF1='cheap':'trimf',[0 5 10]
MF2='average':'trimf',[10 15 20]
MF3='generous':'trimf',[20 25 30]

[Rules]
1 1, 1 (1) : 2
2 0, 2 (1) : 1
3 2, 3 (1) : 2
Loading

2 comments on commit 0fd0eca

@lucaferranti
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/78507

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.1 -m "<description of version>" 0fd0ecafa83776365f33701694b0c89f8cc5a284
git push origin v0.1.1

Please sign in to comment.